Giter Site home page Giter Site logo

blogs's People

Contributors

lq782655835 avatar w1301625107 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

blogs's Issues

前端团队规范总结

团队规范对于团队合作非常重要,大大减少项目之间的沟通,提高整体解决方案。有些规范能让lint做强制审核的,尽量用lint;有些需要约定俗成的,则记录成文档形成规范。

Code

规范

推荐

预览

image

前端必备效率工具

工欲善其事,必先利其器,好的工具能给自身带来超高的工作效率。以下是总结出的高效率工具。

双屏

绝对效率提高首选,有机会三屏更佳

Mac

HomeBrew

mac软件包管理,通过命令直接安装工具包,Mac必备。

Afred

只需要键盘简单输入,即可快速查询或打开应用。效率神器,MacOS必备

CheatSheet

长按即可获得当前软件快捷键,初学MacOS同学必备

SourceTree

git gui工具,界面简洁,操作方便

Wunderlist

随时记录思维灵感或备忘录;pc、phone、pad三端统一,方便随时查看

Quick Look插件

MacOS非常人性化的功能之一。只要选中了相应的文件,敲击空格键就可以查看文件的大小、甚至可以可以预览文档,视频、音频等等。但是它还可以变得更加强大,只需要使用 Homebrew 安装一些插件即可。更多ql插件

Chrome

octotree

在线浏览代码,从此告别github下载代码

Octohint

自动选择源代码中所有变量值,配合octotree,prefect

GITHUBER

开发者的新标签页。推送优质github项目

OneTab

技术神器,查阅资料时,很多舍不得删的google tab标签,统一管理

QR Code Generator

实时生成当前pc网址,H5开发必备。

有道词典

在线划词,阅读英文文档资料首选

Advertising Terminator

著名的广告终结者

Momentum

炫酷的新标签页,与githuber不能同用

FireShot

网页截图,一键滚动截取整个网页

终端工具

iTerm2

Mac自带的终端不是特别方便。 iTerm2是Terminal的替代品,但比Terminal优秀太多了。iTerm2可以设置主题,支持画面分隔、各种快捷键。Mac默认使用的shell是bash,我们可以换成zsh(执行命令chsh -s /bin/zsh即可),搭配iTerm2使用,用起来十分顺手。推荐solarized主题皮肤美化外观。iterm2常用快捷键如下:

  • command + T:新建窗口
  • command + d:横向分屏
  • command + shift + d: 竖向分屏
  • command + enter: iterm2全屏
  • option + space:全局热键,打开iterm2,实用。(可在设置中修改)
  • command + f:查询
  • command + 点击文件名: 打开文件
  • 双击文字: 复制

oh my zsh

bash提示和界面美观度不够强大,而zsh有强大的自动补全参数和自定义配置功能等。oh my zsh是zsh的集大成者,帮助我们快速上手zsh。oh my zsh炫酷的外表+iterm分屏=程序员必备。

oh my zsh更强大的功能在于其自周边配置插件,oh my zsh维护了一个插件列表,以下推荐实用的几个插件:

  • zsh-syntax-highlighting。 命令着色插件,帮助你为终端的命令着色。强烈推荐。
    1. 把插件需要的文件克隆到 zsh 默认的插件目录。执行命令:git clone https://github.com/zsh-users/zsh-syntax-highlighting.git $ZSH_COSTOM/plugins/zsh-syntax-highlighting/
    2. 把插件名称加入 oh-my-zsh 插件列表。修改~/.zshrc,添加插件名到后面:plugins=(git zsh-syntax-highlighting)
    3. 执行配置:source ~/.zshrc
  • zsh-autosuggestions
    1. git clone git://github.com/zsh-users/zsh-autosuggestions $ZSH_CUSTOM/plugins/zsh-autosuggestions
    2. 修改~/.zshrc,添加插件名到后面:plugins=(git zsh-syntax-highlighting zsh-autosuggestions)
    3. source ~/.zshrc
  • autojump。厌倦了mac下找文件不断的cd?autojump帮你一键到达想要的文件位置。
    1. git clone https://github.com/wting/autojump.git $ZSH_COSTOM/plugins/autojump/
    2. 修改~/.zshrc,添加插件名到后面:plugins=(git zsh-syntax-highlighting zsh-autosuggestions autojump)
    3. source ~/.zshrc
  • extract。开启命令行x一键解压
    1. extract插件oh-my-zsh自带,只是默认没开启。添加插件名到后面:plugins=(git zsh-syntax-highlighting zsh-autosuggestions extract)
    2. source ~/.zshrc

VSCode插件

界面优化

  • vscode-icons推荐 设置vscode图标。最新vscode已默认集成该插件
  • Bracket Pair Colorizer推荐 着色匹配括号。相似对插件还有Rainbow Brackets
  • Indent-Rainbow 四种不同颜色交替着色文本前面的缩进
  • Trailing Spaces 检测多余空格并高亮
  • TODO Highlight TODO备忘插件
  • Code Spell Checker 代码拼写检查
  • Document This doc注视自动生成

HTML推荐

  • Auto Close Tag 自动闭合标签
  • Auto Rename Tag 自动重命名标签
  • HTML Snippets html自动补全代码片段
  • IntelliSense for CSS class names in HTML 在html中智能提示CSS 类名
  • HTML CSS Support 在style中智能提示css样式

JS推荐

  • npm Intellisense 智能辅助输入npm包。最新vscode已默认集成该插件
  • Path Intellisense 智能辅助输入路径
  • Auto Import 自动识别导入
  • Code Runner 执行测试代码
  • Import Const 自动计算引入包大小
  • Regex Previewer 测试写的正则表达式

Vue/React推荐

  • Vetur Vue-VSCode必备,高亮代码,自动补全等
  • React-Native/React/Redux snippets for es6/es7 react代码片段

扩展工具

  • GitLens推荐 增强了vscode 中内置的 Git 功能。例如 commits 搜索,历史记录和和查看代码作者身份
  • View In Browser 在vscode预览浏览器
  • SVG Viewer 在vscode内查看svg图标
  • Auto-Open Markdown Preview 实时预览markdown
  • Markdown PDF 将markdown文档转为pdf
  • Debugger for Chrome vscode中调试在chrome的js代码
  • Eslint 校验lint工具

reactnative-开发环境搭建

本说明旨在帮助读者快速搭建开发RN应用,考虑到部门大部分是Mac,故奉上Mac-IOS开发环境,Window请戳这里

工具依赖

  • Node/yarn

  • XCode

  • Watchman

  • react-native-cli

安装Node

  1. 推荐使用homebrew
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
  1. 安装node/yarn
brew install node yarn

没有翻墙请使用淘宝镜像以加速下载

npm config set registry https://registry.npm.taobao.org --global
npm config set disturl https://npm.taobao.org/dist --global

XCode

提供IDE和Simulator模拟器,AppStore或官网下载,React Native目前需要Xcode 8.0 或更高版本。

Watchman

监视文件变更工具,热加载需要用到该工具

brew install watchman

react-native-cli

react native的cli工具,通常都使用该工具创建项目模板

npm install -g react-native-cli

Hello World

一般0.44.3版本比较稳定,最新版本如果没有翻墙,很可能有些库无法下载,导致编译错误。所以如果在家没翻墙,建议创建项目使用0.44.3版本,

react-native init HelloRN --0.44.3
cd AwesomeProject
react-native run-ios

运行RN项目

第一种方式: Xcode可视化方式(推荐,速度快/查看报错方便)

  1. 进入RN项目下ios目录,打开.xcodeproj后缀文件,自动会xcode打开

image

  1. 如下选择模拟器机型,启动项目即可预览App

image

第二种方式: 命令行方式

cd HelloRN
react-native run-ios

推荐链接:

React中文网

JS设计模式

单例模式

单例模式定义了一个对象的创建过程,此对象只有一个单独的实例,并提供一个访问它的全局访问点。

ES5闭包实现:

var single = (function(){
    var unique;

    function singleXXX(){
        // ... 生成单例的构造函数的代码
    }

    return {
        getInstance : function() {
            if (unique === undefined) {
                unique = new singleXXX()
            }
            return unique
        }
    }
})();

let singleXXX1 = single.getInstance()
let singleXXX2 = single.getInstance()
console.log(singleXXX1 === singleXXX2) // true

ES5缓存实现:

function SingleXXX() {
    if (typeof SingleXXX.instance === 'object') {
        return SingleXXX.instance
    }

    // ...生成单例的构造函数的代码

    // 缓存实例
    SingleXXX.instance = this
}

let singleXXX1 = new SingleXXX()
let singleXXX2 = new SingleXXX()
console.log(singleXXX1 === singleXXX2) // true

ES6 Object实现:

function singleXXX(){}
let single = {
    unique: null,
    getInstance: function() {
        if (this.unique === undefined) {
            this.unique = new singleXXX()
        }
        return this.unique
    }
}
 // 保证实例不被改写
Object.defineProperty(single, 'unique', {
    writable: false,
    configurable: false
})
// 或Object.freeze(single)

let singleXXX1 = single.getInstance()
let singleXXX2 = single.getInstance()
console.log(singleXXX1 === singleXXX2) // true

ES6 Class实现,跟Java、C#等面向对象语言写法一致:

class SingleXXX {
    constructor() {
        // ...生成单例的构造函数的代码
    }
    static getInstance() {
        if(!this.instance) {
            this.instance = new SingleXXX()
        }
        return this.instance
    }
}
let singleXXX1 = SingleXXX.getInstance()
let singleXXX2 = SingleXXX.getInstance()
console.log(singleXXX1 === singleXXX2) // true

职责链模式

使多个对象都有机会处理请求,从而避免请求的发送者和接受者之间的耦合关系,将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。

优点:解耦了请求发送者和N个接受者之间的复杂关系。

弊端:不能保证某个请求一定会被链中的节点处理。

/* 传统方式实现 */
// orderType:[1:500, 2:200, 3:普通],isPaid:true/false,stock:库存量
var order = function(orderType, isPaid, stock) {
    if(orderType === 1) {
        if(isPaid) {
            console.log("500元定金预购,得到100优惠券");
        } else {
            if(stock > 0) {
                console.log("普通购买,无优惠券");
            }else {
                console.log("库存不足");
            }
        }
    }else if(orderType === 2) {
        if(isPaid) {
            console.log("200元定金预购,得到50优惠券");
        } else {
            if(stock > 0) {
                console.log("普通购买,无优惠券");
            }else {
                console.log("库存不足");
            }
        }
    }else if(orderType === 2) {
        if(stock > 0) {
            console.log("普通购买,无优惠券");
        }else {
            console.log("库存不足");
        }
    }
}

order(1, true, 500);

/*职责链 */
var order500 = function(orderType, isPaid, stock) {
    if(orderType === 1 && isPaid === true) {
        console.log("500元定金预购,得到100优惠券");
    }else {
        return "nextSuccessor";
    }
};

var order200 = function(orderType, isPaid, stock) {
    if(orderType === 2 && isPaid === true) {
        console.log("200元定金预购,得到50优惠券");
    }else {
        return "nextSuccessor";
    }
};

var orderNormal = function(orderType, isPaid, stock) {
    if(stock > 0) {
        console.log("普通购买,无优惠券");
    }else {
        console.log("库存不足");
    }
};

// Function原型链上加入after方法
Function.prototype.after = function(fn) {
    var self = this;
    return function() {
        var ret = self.apply(this, arguments);
        if(ret === "nextSuccessor") {
            return fn.apply(this, arguments);
        }
        return ret;
    };
}

var order = order500.after(order200).after(orderNormal);
order(1, true, 10);

策略模式

定义一系列的算法,把它们一个个封装起来,将不变的部分和变化的部分隔开。策略模式至少2部分组成:策略类和环境类

策略类: 封装具体的算法,可能有很多策略算法,这是变化的部分。

环境类: 调用算法的使用方式,是不变的部分。

/* 传统方式实现 */
function Price(personType, price) {
    if (personType == 'vip') {
        return price * 0.5; //vip 5 折
    } 
    else if (personType == 'old'){
        return price * 0.3; //老客户 3 折
    }
    ... // 每多一次情形,多一次else分支
    else {
        return price; //其他都全价
    }
}

/* 策略模式 */
// 对于vip客户
function vipPrice() {
    this.discount = 0.5;
}
vipPrice.prototype.getPrice = function(price) {
  return price * this.discount;
}

// 对于老客户
function oldPrice() {
    this.discount = 0.3;
}
oldPrice.prototype.getPrice = function(price) {
    return price * this.discount;
}

// 对于普通客户
function Price() {
    this.discount = 1;
}
Price.prototype.getPrice = function(price) {
    return price ;
}

// 环境类,调用方式是固定的。算法策略类可变化
function Context() {
    this.name = '';
    this.strategy = null;
    this.price = 0;
}
Context.prototype.set = function(name, strategy, price) {
    this.name = name;
    this.strategy = strategy;
    this.price = price;
}
Context.prototype.getResult = function() {
    console.log(this.name + ' 的结账价为: ' + this.strategy.getPrice(this.price));
}

var context = new Context();
var vip = new vipPrice();
context.set ('vip客户', vip, 200); // 解耦可变与不可变
context.getResult();

以上写法风格适用于Java、ASP.NET、JS等面向对象语言。考虑到js脚本的动态性,实际应用中通常我们会这样应用策略模式:

var obj = {
        "vip": function(price) {
            return price * 0.5;
        },
        "old" : function(price) {
            return price * 0.3;
        },
        "normal" : function(price) {
            return price;
        }
};
var calculatePrice =function(level,price) {
    return obj[level](price);
};
console.log(calculatePrice('vip',200));

观察者模式

js中最常用的设计模式。在观察者模式中,观察者需要直接订阅目标事件;在目标发出内容改变的事件后,直接接收事件并作出响应。很多库都有该模式的实现,比如vue、redux等。

image

function Dep() {
    this.subs = [];
}
Dep.prototype.addSub = function (sub) {
    this.subs.push(sub);
}
Dep.prototype.notify = function () {
    this.subs.forEach(sub=>sub.update());
}
function Watcher(fn) {
    this.fn = fn;
}
Watcher.prototype.update = function () {
     this.fn();
}

var dep = new Dep(); // 观察者
dep.addSub(new Watcher(function () { // 观察者直接订阅观察者
    console.log('okokok');
}))
dep.notify();

发布订阅模式

发布订阅模式属于广义上的观察者模式,也是最常用的观察者模式实现。在发布订阅模式中,发布者和订阅者之间多了一个发布通道;一方面从发布者接收事件,另一方面向订阅者发布事件;订阅者需要从事件通道订阅事件,以此避免发布者和订阅者之间产生依赖关系。发布者和订阅者不知道对方的存在,所以解耦更彻底。NodeJS的EventEmitter对象即为该模式的实现。

image

简单理解,观察者模式中,发布者和订阅者是知道对方存在的,实现上使用了array;发布订阅模式,发布者和订阅者都不知道对方存在,定义了一个中介对象(可抽离成单独文件),实现上使用了object。

class EmitEvent {
    constructor() {
        this._events = {}
    }

    on(type, callback) {
        if(!this._events[type]) this._events[type] = []

        this._events[type].push(callback)
    }

    emit(type, ...args) {
        if(this._events[type]) {
            this._events[type].forEach(fn => fn.call(this, ...args))
        }
    }
}

// EmitEvent作为事件通道
let emitEvent = new EmitEvent()
emitEvent.on('a', (data) => console.log('123', data))
emitEvent.emit('a', { field: 1 })

参考文档

Vue3 响应式原理

Vue3 响应式原理 - Ref/Reactive/Effect源码分析

目前vue3还没完全稳定下来,许多rfcs都有变化的可能。本文基于目前最新(2019-11-07)fork的 vue源码进行原理分析。官方提供了在Vue2.x尝试最新Vue3功能的插件库:Vue Composition API (以前该库叫vue-function-api,现在叫composition-api)。

众所周知,Vue3使用ES6 Proxy替代ES5 Object.defineProperty实现数据响应式,这也是Vue最为核心的功能之一。Vue3相比Vue2.x,API变化很大,提出了Vue Composition API。但在响应式原理实现方面,源码依然还是依赖收集 + 执行回调,只不过api变化后,形式也有点变化。想了解vue 2.x实现方式,可以看下笔者以前写的 Vue2.x源码分析 - 响应式原理

你必须知道的Vue3 RFCS ChangeLog

如果较少关注vue3征求意见稿vue rfcs,可能大部分人对vue3还停留在Vue Function API。作者尤雨溪专门为这重大改变的API做过详细的叙述,并特意翻译了中文Vue Function-based API RFC。目前Vue 官方发布了最新的3.0 API 修改草案,并在充分采纳社区的意见后,将Vue Function API 更正为 Vue Composition API.

1. 重大变化点

  1. state更名为reactive
    • reactive等价于 Vue 2.x 的Vue.observable()
  2. value更名为ref,并提供isRef和toRefs
    • 使用ref来创建包装对象进行传递
  3. computed可传入get和set,用于定义可更改的计算属性
  4. effect 更名为 watchEffect

Vue官方团队建议在组合函数中都通过返回ref对象。

2. 了解Vue Composition API

import { reactive, computed, toRefs, watchEffect } from "vue";
export default {
  setup() {
    const event = reactive({
      capacity: 4,
      attending: ["Tim", "Bob", "Joe"],
      spacesLeft: computed(() => { return event.capacity - event.attending.length; })
    });
    watchEffect(() => console.log(event.capacity))
    function increaseCapacity() {
      event.capacity++;
    }
    return { ...toRefs(event), increaseCapacity };
  }
};

3. 建议阅读资料

源码解析

1. ref

先从入口ref看起,ref常用于基本类型,reactive用于引用类型。如果ref传入对象,其实内部会自动变为reactive。

ref本质上是把js 基本类型(string/number/bool)包装为引用对象,使得具有响应式特性。

export function ref(raw: unknown) {
  if (isRef(raw)) {
    return raw
  }
  // ref常用于基本类型,reactive用于引用类型。如果ref传入对象,其实内部会自动变为reactive
  raw = convert(raw)

  // 基本类型,转为含有getter/setter的对象
  const r = {
    _isRef: true, // 判断isRef
    // 基本类型无法被追踪,所以使用ref包装为object,使得可以被追踪
    get value() {
      track(r, OperationTypes.GET, '')
      return raw
    },
    set value(newVal) {
      raw = convert(newVal)
      trigger(r, OperationTypes.SET, '')
    }
  }
  return r as Ref
}

const convert = <T extends unknown>(val: T): T =>
  isObject(val) ? reactive(val) : val

同时ref支持把reactive转为refs对象 - toRefs

export function toRefs<T extends object>(
  object: T
): { [K in keyof T]: Ref<T[K]> } {
  const ret: any = {}
  for (const key in object) { // for in 展开一层
    ret[key] = toProxyRef(object, key)
  }
  return ret
}

function toProxyRef<T extends object, K extends keyof T>(
  object: T,
  key: K
): Ref<T[K]> {
  return {
    _isRef: true,
    get value(): any {
      return object[key]
    },
    set value(newVal) {
      object[key] = newVal
    }
  }
}

2. reactive

再来看下vue3的响应式reactive源码:

先认识下以下4个全局存储,使用weakmap存储起普通对象和生成的响应式对象,因为很多地方都需要用到判断以及取值。其中rawToReactive和reactiveToRaw是一组,只不过key和value互相对调。

// 防止重复设置响应式对象,建立store存起来
// WeakMaps that store {raw <-> observed} pairs.
const rawToReactive = new WeakMap<any, any>() // 原始object对象:封装的响应式对象
const reactiveToRaw = new WeakMap<any, any>() // 封装的响应式对象:原始object对象
// 只读响应式
const rawToReadonly = new WeakMap<any, any>()
const readonlyToRaw = new WeakMap<any, any>()

下面是reactive入口,如果传入参数是只读响应式,或者是用户设置的只读类型,返回处理。大部分都会走createReactiveObject方法:

export function reactive(target: object) {
  // if trying to observe a readonly proxy, return the readonly version.
  if (readonlyToRaw.has(target)) {
    return target
  }
  // target is explicitly marked as readonly by user
  if (readonlyValues.has(target)) {
    return readonly(target)
  }
  // 给普通对象创建响应式对象
  return createReactiveObject(
    target,
    rawToReactive,
    reactiveToRaw,
    mutableHandlers,
    mutableCollectionHandlers
  )
}

如下面注释解释,大部分代码都是为了做边界和重复处理。最重要的还是创建proxy对象:
observed = new Proxy(target, mutableHandlers)

function createReactiveObject(
  target: unknown, // 原始对象
  toProxy: WeakMap<any, any>, // 全局rawToReactive
  toRaw: WeakMap<any, any>, // 全局reactiveToRaw
  baseHandlers: ProxyHandler<any>,
  collectionHandlers: ProxyHandler<any>
) {
  // 必须是对象
  if (!isObject(target)) {
    if (__DEV__) {
      console.warn(`value cannot be made reactive: ${String(target)}`)
    }
    return target
  }

  // 重复的对象引用,最终都返回初始的监听对象,这就是创建全局store的原因之一
  // target already has corresponding Proxy
  let observed = toProxy.get(target)
  if (observed !== void 0) {
    return observed
  }
  // target is already a Proxy
  if (toRaw.has(target)) {
    return target
  }

  // vue对象、vnode对象等不能被创建为响应式
  // only a whitelist of value types can be observed.
  if (!canObserve(target)) {
    return target
  }

  // 真正创建代理Proxy对象并返回
  const handlers = collectionTypes.has(target.constructor)
    ? collectionHandlers // [Set, Map, WeakMap, WeakSet]对象走这个handles
    : baseHandlers // 大部分走baseHandle
  observed = new Proxy(target, handlers)
  // 创建完马上全局缓存
  toProxy.set(target, observed)
  toRaw.set(observed, target)
  if (!targetMap.has(target)) {
    targetMap.set(target, new Map())
  }
  return observed
}

所以还是看代理对象mutableHandlers中的处理:

export const mutableHandlers: ProxyHandler<object> = {
  get: createGetter(false),
  set,
  deleteProperty,
  has,
  ownKeys
}

get、has、deleteProperty、ownKeys代理方法中,都调用了track函数,用来收集依赖,这个下文讲;而set调用了trigger函数,当响应式数据变化时,收集的依赖被执行回调。从原理看,这跟vue2.x是一致的。

看下最常用的get、set。get中除常规边界处理外,最重要是根据代理值的类型,对object类型进行递归调用reactive

function createGetter(isReadonly: boolean) {
  return function get(target: object, key: string | symbol, receiver: object) {
     // 获取到代理的值
    const res = Reflect.get(target, key, receiver)
    if (isSymbol(key) && builtInSymbols.has(key)) {
      return res
    }

    // 如果是ref包裹的对象,直接返回解包后的值
    if (isRef(res)) {
      return res.value
    }
    // track是逻辑和视图变化重要的一块
    track(target, OperationTypes.GET, key)
    // 值类型,直接返回值;对象类型,递归响应式reactive(res)
    return isObject(res)
      ? isReadonly
        ? // need to lazy access readonly and reactive here to avoid
          // circular dependency
          readonly(res)
        : reactive(res)
      : res
  }
}

set函数里除了代理set方法外,最重要的莫过于当值改变时,触发trigger方法,下文详细讲述该函数。

function set(
  target: object,
  key: string | symbol,
  value: unknown,
  receiver: object
): boolean {
  value = toRaw(value)
  const oldValue = (target as any)[key]
  if (isRef(oldValue) && !isRef(value)) {
    oldValue.value = value
    return true
  }
  // prxoy
  const hadKey = hasOwn(target, key) // 是否target本来旧有key属性,等价于:key in target
  const result = Reflect.set(target, key, value, receiver)
  // don't trigger if target is something up in the prototype chain of original
  if (target === toRaw(receiver)) {
      if (!hadKey) {
        trigger(target, OperationTypes.ADD, key) // 触发新增
      } else if (hasChanged(value, oldValue)) {
        trigger(target, OperationTypes.SET, key) // 触发修改
      }
  }
  return result
}

3. track/trigger

这里是vue3响应式源码的难点。但原理跟vue2.x基本一致,只不过实现方式上有些不同。
track用于收集依赖deps(依赖一般收集effect/computed/watch的回调函数),trigger 用于通知deps,通知依赖这一状态的对象更新。

3.1 举个例子解释

如下代码,使用effect或computed api时,里面使用了count.num,意味着这个effect依赖于count.num。当count.num set改变值时,需要通知该effect去执行。那什么时候count.num收集到effect这个依赖呢?
答案是创建effect时的回调函数。如果回调函数中用到响应式数据(意味着会去执行get函数),则同步这个effect到响应式数据(这里是count.num)的依赖集中。

其流程是(全文重点):1. effect/computed函数执行 -> 2. 代码有书写响应式数据,调用到get,依赖收集 -> 3. 当有set时,依赖集更新。

const count = reactive({ num: 0 })
// effect默认没带lazy参数,先会执行effect
effect(() => {
  // effect用到对应响应式数据时,count.num get就已经收集好了该effect依赖
  // 同理,使用computed api时,
  console.log(count.num)
})
// computed依赖于count.num,也意味着该computed是count.num的依赖项
const computedNum = computed(() => 2 * count.num))
count.num = 7

3.2 对应源码解释

理解了上面这个案例,源码阅读就能顺畅的多。

先挑effect实现过程,再来看依赖收集track函数和执行依赖函数trigger。effect api主要用effect包装了回调函数fn,并默认执行fn回调函数,最终执行run(effect, fn, args)。

export function effect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
  if (isEffect(fn)) {
    fn = fn.raw
  }
  // 回调fn函数,包装成effect
  const effect = createReactiveEffect(fn, options)
  // 默认不是懒加载,lazy=fasle,执行effect函数。
  if (!options.lazy) {
    effect()
  }
  return effect
}

function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(...args: unknown[]): unknown {
    return run(effect, fn, args) // 创建effect时,执行run
  } as ReactiveEffect
  effect._isEffect = true // 判断是effect
  effect.active = true // effect支持手动stop,此时active会被设置为false
  effect.raw = fn
  effect.scheduler = options.scheduler
  effect.onTrack = options.onTrack
  effect.onTrigger = options.onTrigger
  effect.onStop = options.onStop
  effect.computed = options.computed
  effect.deps = []
  return effect
}

再看run函数内容。其实就是执行回调函数时,先对effect入栈,使得当前effectStack有值。这个就非常巧妙,当执行fn回调时,回调函数的代码中又会去访问响应式数据(reactive),这样又会执行响应数据的get方法,get方法又会去执行后文讲的trick方法,trick进行依赖收集。

依赖收集哪些东西呢?就是收集当前的effect回调函数。这个回调函数(被effect包装)不就是刚被存储在effectStack么,所以在后续trick函数中可以看到使用effectStack栈。当执行完回调函数,再进行出栈。

通过使用栈数据结构,以及对代码执行的时机,非常巧妙的就把当前effect传递过去,最终被响应式数据收集到依赖集中。

function run(effect: ReactiveEffect, fn: Function, args: unknown[]): unknown {
  if (!effect.active) {
    return fn(...args)
  }

  // 通常都是走这里,执行回调,同时不同时机effect入栈/出栈
  if (!effectStack.includes(effect)) {
    cleanup(effect)
    // 这里的try finally很巧妙
    // 入栈 -> 回调函数执行(使用栈,相当于把effect传递过去了) -> 出栈
    try {
      effectStack.push(effect)
      return fn(...args)
    } finally {
      effectStack.pop()
    }
  }
}

再来看看依赖收集trick/trigger具体实现细节。

先来看下几个存储变量,主要是依赖收集时用到的:

// The main WeakMap that stores {target -> key -> dep} connections.
// Conceptually, it's easier to think of a dependency as a Dep class
// which maintains a Set of subscribers, but we simply store them as
// raw Sets to reduce memory overhead.
export type Dep = Set<ReactiveEffect>
export type KeyToDepMap = Map<any, Dep>
// 原始对象: new Map({key1: new Set([effect1, effect2,...])}, {key2: Set2}, ...)
// key是原始对象里的属性, 值为该key改变后会触发的一系列的函数, 比如渲染、computed
export const targetMap = new WeakMap<any, KeyToDepMap>()

track函数进行数据依赖采集, 以便于后面数据更改能够触发对应的函数。

// 收集target key的依赖
// get: track(target, OperationTypes.GET, key)
export function track(target: object, type: OperationTypes, key?: unknown) {
  // 定义的computed、effect api都会推入effectStack栈中
  if (!shouldTrack || effectStack.length === 0) {
    return
  }

  // 调用effect/computed api时,能拿到effect对象(即依赖的回调函数)
  const effect = effectStack[effectStack.length - 1]

  if (type === OperationTypes.ITERATE) {
    key = ITERATE_KEY
  }

  // targetMap = {target1: deps = {key1: [], key2: [], ...}},两层嵌套
  // 初始化target
  let depsMap = targetMap.get(target)
  if (depsMap === void 0) {
    targetMap.set(target, (depsMap = new Map()))
  }
  // 初始化target.key。键是target.key,值是依赖的effect数组,是个集合。
  let dep = depsMap.get(key!)
  if (dep === void 0) {
    depsMap.set(key!, (dep = new Set()))
  }
  // 依赖收集
  if (!dep.has(effect)) {
    dep.add(effect)
    effect.deps.push(dep)
  }
}

trigger,将track收集到的effect函数集合,添加到runners中(二选一放进effects或computedRunners中),并通过scheduleRun执行effect:

// set: trigger(target, OperationTypes.SET, key)
export function trigger(
  target: object,
  type: OperationTypes,
  key?: unknown,
  extraInfo?: DebuggerEventExtraInfo
) {
  const depsMap = targetMap.get(target)
  if (depsMap === void 0) {
    // never been tracked
    return
  }
  // 把拿到的depsMap.get(key),二选一放进effects或computedRunners中。
  const effects = new Set<ReactiveEffect>()
  const computedRunners = new Set<ReactiveEffect>()
  // 根据不同的OperationTypes,把effect=depsMap.get(key)放进runners中
  if (type === OperationTypes.CLEAR) {
    // collection being cleared, trigger all effects for target
    depsMap.forEach(dep => {
      addRunners(effects, computedRunners, dep)
    })
  } else {
    // schedule runs for SET | ADD | DELETE
    if (key !== void 0) {
      addRunners(effects, computedRunners, depsMap.get(key))
    }
    // also run for iteration key on ADD | DELETE
    if (type === OperationTypes.ADD || type === OperationTypes.DELETE) {
      const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY
      addRunners(effects, computedRunners, depsMap.get(iterationKey))
    }
  }

  // 执行runners,即执行effects
  const run = (effect: ReactiveEffect) => {
    scheduleRun(effect, target, type, key, extraInfo)
  }
  // Important: computed effects must be run first so that computed getters
  // can be invalidated before any normal effects that depend on them are run.
  computedRunners.forEach(run)
  effects.forEach(run)
}

// 添加runner时,二选一
function addRunners(
  effects: Set<ReactiveEffect>,
  computedRunners: Set<ReactiveEffect>,
  effectsToAdd: Set<ReactiveEffect> | undefined
) {
  if (effectsToAdd !== void 0) {
    effectsToAdd.forEach(effect => {
      if (effect.computed) {
        computedRunners.add(effect)
      } else {
        effects.add(effect)
      }
    })
  }
}

总结

响应式数据,就是当数据对象改变时(set),有用到数据对象的地方,都会自动执行响应的逻辑。比如effect/computed/watch等js api用到数据对象,则执行对应的回调函数。而视图view用到数据对象时,则重新vnode diff,最后自动进行dom更新(即视图更新)。

而Vue3响应式源码跟Vue2.x源码流程基本一致,依然是利用在使用响应式数据时,执行数据的get方法,收集相关的依赖(依赖可以是回调函数,如effect/computed,也可以是视图自动更新);在数据进行变化的时候,执行数据的set方法,把收集的依赖都依次执行。

记录markdown上传github后乱码解决过程

换成issue写博客,遇到markdown乱码问题,记录下解决过程。

问题

把技术博客markdown文件上传到github上时,总是会出现莫名的黑体?。如下:

如图

分析问题

前期一直以为是编码问题,但Mac默认是utf-8编码,github是可以识别的。再想着是不是IDE错误,但很明显vscode也是utf-8。看来不太像编码出问题了,所以改了下方案,把文章拷贝到Sublime IDE中看。果然,通过横向对比,发现文章莫名多出很多bs符号,而且bs符号就是最终黑体?部分。ok,定位问题。后来查找issue,终于发现这是vscode的bug,传送门。原来是我习惯在vscode中写markdown,而且使用了一款实时预览markdown的插件(Auto-Open Markdown Preview,十分好用,推荐),再写的过程删除会产生这bs符号。

  1. 系统编码乱码
  2. IDE编码设置错误
  3. 横向多IDE对比(找到问题)
  4. 确定问题点,解决方案

image

解决方案

只需要把bs删除即可。这里需要先让bs显示在vscode中,毕竟现在vscode已是最爱,离不开。方法是设置vscode的editor.renderControlCharacters为true,即可显示隐藏的特殊字符。接下来删除bs字符再重新上传github即可。

image

stylelint样式规范工具

笔者近期做代码优化,其中就重要的一块就是代码规范。之前的文章写过ESLint,用来规范js写法,现在使用StyleLint来规范css写法。

  • 安装和使用
  • 添加例外
  • 自动修复

安装和使用

安装

npm install --save-dev stylelint

使用

  1. 新增.stylelintrc文件

  2. 在文件中设置规则,以下是笔者部门使用的css规范

{
    "rules": {
        # 缩进 4 个空格
        "indentation": 4,

        # 去掉小数点前面的 0
        "number-leading-zero": "never",

        # 使用双引号
        "string-quotes": "double",

        # 每个属性声明末尾都要加分号
        "declaration-block-trailing-semicolon": "always",

        # 属性值 0 后面不加单位
        "length-zero-no-unit": true,

        # 对空行的处理
        # 第一条属性声明前不允许有空行
        "declaration-empty-line-before": [
            "never",
            { ignore: [ "after-declaration" ] }
        ],
        # 每个样式规则前后都有空行,除了第一条规则与规则前有注释
        "rule-empty-line-before": [
            "always",
            { except: [ "after-single-line-comment", "first-nested" ] }
        ],
        # 在结尾 "}" 之前不允许有空行
        "block-closing-brace-empty-line-before": [ "never" ],
        # "@" 语句之前都有空行,但是忽略 "@" 语句在代码块中间与同个非代码块 "@" 语句之间的空行这两种情况
        "at-rule-empty-line-before": [
            "always",
            { ignore: [ "inside-block", "blockless-after-same-name-blockless" ] }
        ],
        # 不允许超过一行的空行
        "max-empty-lines": 1,
        # 每行句末不允许有多余空格
        "no-eol-whitespace": true,
        # 文件末尾需要有一个空行
        "no-missing-end-of-source-newline": true,

        # 大小写处理
        "unit-case": "lower",
        "color-hex-case": "upper",
        "value-keyword-case": "lower",
        "function-name-case": "lower",
        "property-case": "lower",
        "at-rule-name-case": "lower",
        "selector-pseudo-class-case": "lower",
        "selector-pseudo-element-case": "lower",
        "selector-type-case": "lower",
        "media-feature-name-case": "lower",

        # 对空格的处理
        # "{" 前必须有空格
        "block-opening-brace-space-before": "always",
        # 注释 "/*" 后和 "*/" 前必须有空格
        "comment-whitespace-inside": "always",
        # 属性名 ":" 后必须有空格
        "declaration-colon-space-after": "always",
        # 属性名 ":" 前不能有空格
        "declaration-colon-space-before": "never",
        # 声明属性末尾 ";" 前不能有空格
        "declaration-block-semicolon-space-before": "never",
        # 属性值中的 "," 后必须有空格
        "function-comma-space-after": "always",
        # 选择器例如 ">、+、~" 前后必须要有空格
        "selector-combinator-space-before": "always",
        "selector-combinator-space-after": "always",
        # 分隔多个选择器之间的 "," 后必须有空格
        "selector-list-comma-space-after": "always",
        # 分隔多个选择器之间的 "," 前不能有空格
        "selector-list-comma-space-before": "never",
        # 子代选择器之间没有额外空格
        "selector-descendant-combinator-no-non-space": true,
        # 多个属性值之间的 "," 后必须有空格
        "value-list-comma-space-after": "always",
        # 多个属性值之间的 "," 前不能有空格
        "value-list-comma-space-before": "never",
        # 媒体查询中设置断点宽度里的 ":" 后必须有空格
        "media-feature-colon-space-after": "always",
        # 媒体查询中设置断点宽度里的 ":" 前不能有空格
        "media-feature-colon-space-before": "never"
    }
}

规则检查

stylelint 'src/**/*.vue' --fix

stylelint命令有时候无法解析到,因为使用了全局的sylelint,这时可以指定相对路径./node_modules/.bin/stylelint

提交git时检查

需要用到插件husky,该插件会在git提交时,执行npm run precommit命令,所以需要在package.json中添加如下代码检查:

"lint": "eslint --quiet --ext .js,.vue src",
"style": "stylelint 'src/**/*.vue' --fix",
"precommit": "npm run lint && npm run style",

添加例外

在stylelint使用过程中,有时候会对某条css添加例外,不要适用规则或部分规则

关闭全部规则:

/* stylelint-disable */
a {}
/* stylelint-enable */

关闭部分规则:

/* stylelint-disable selector-no-id, declaration-no-important   */
#id {
    color: pink !important;
}
/* stylelint-enable */

自动修复

有些项目是开发到一半时,添加了StyleLint进行css约束,这时候需要自动化对不满足条件的Rule进行修复,如下是使用到的几种:

1.--fix命令

该命令能fix大部分格式问题,具体哪些规则可以自动fix可以看这里

2.Vetur插件--格式化文件

优点是可以统一格式化文件,缺点是只能单个文件操作

3.vscode-stylefmt插件

类似Vetur插件,但该插件可定制化更多,详情请移至github

4.stylefmt

该工具也是官方推荐,可以批量修改,使用如下命令修改,详情见 github

stylefmt --stdin-filename input.css

Electron工程踩坑记录

最近公司有个新产品线,需要将应用打包成客户端,提供私有化部署。考虑到Web线上已经实现大部分需求,技术选型时使用Electron。本文不是帮助读者的扫盲文,只是记录下项目工程中遇到的坑,所以阅读本文需要web和electron知识。

应产品要求,私有化部署主要考虑windows端,mac端其次。框架选型使用electron-vue脚手架(这里也强烈推荐),该脚手架包含Vue技术栈单页应用 + electron + 打包完整流程。内置Vuex,Vue-Router,Webpack,electron-builder等。下面的大部分实践源码放在这

1. 自定义标题栏

这应该是每一个使用electron实现web客户端都会遇到的问题,使用原生的外边框,第一太丑,第二也不统一。

解决方案:frame + css drag

frame: false: 主进程中设置窗体参数。去掉默认的标题栏

-webkit-app-region: drag: 渲染进程中设置css。对应的组件可以进行拖动了

mainWindow = new BrowserWindow({
    height: 350,
    width: 550,
    useContentSize: true,
    resizable: isDev, // 是否可调整大小
    alwaysOnTop: !isDev, // 应用是否始终在所有顶层之上
    transparent: true, // 透明边框
    frame: false, // 不使用默认边框
    center: true
})
.u-header {
    position: relative;
    width: 100%;
    height: 50px;
    line-height: 50px;
    -webkit-app-region: drag; /* as window header */
}

2. 标题栏按钮无效 -- only windows

该bug只在windows平台上显示,mac上正常。在header组件中设置为drag,导致组件里的元素都无法点击。

解决方案:在需要点击的元素上添加no-drag。-webkit-app-region: no-drag;详细看此issue

3. 自定义标题栏无法实现css hover -- only windows

当设置了为drag时,在windows上会屏蔽所有的鼠标事件,所以hover不起作用。这是一个由操作系统导致的问题,故无法修复,相关issue

解决方案:去掉-webkit-app-region: drag;即可。

如果要同时保留可拖动并且hover上有变化,在windows暂时无法实现,需要对此进行取舍或改变交互设计。

4. 打包后程序调试

electron-vue在开发环境默认启用electron-debug插件开启调试。但打包完成,交付到测试同学手里,需要在错误的时候打开开发者工具定位问题。

解决方案:通过注册快捷键,调开web的开发者模式。

globalShortcut.register('CommandOrControl+Shift+L', () => {
    let focusWin = BrowserWindow.getFocusedWindow()
    focusWin && focusWin.toggleDevTools()
})

5. 文本不可选择

既然作为客户端,就应该像个客户端程序,不能对展示型的文本进行用户选择。

解决方案:使用css -webkit-user-select: none;

html {
    -webkit-tap-highlight-color: transparent;
    -webkit-text-size-adjust: 100%;
    height: 100%;
    -webkit-user-select: none; /* disable user select text */
}

6. 打包参数设置

electron应用需要进行打包,变成exe可执行文件给用户。推荐使用最新的electron-builder进行打包(electron-vue脚手架中有提供该选项)。这里对常用的设置进行说明

scripts: {
    /** 打包成windows系统 **/
    "build": "node .electron-vue/build.js && electron-builder --win",
    /** 打包成macos系统 **/
    "build:mac": "node .electron-vue/build.js && electron-builder --mac",
},
"build": {
    /** 最终可执行文件名称:${productName}-${version}.${ext} **/
    "productName": "sight-electron-app",
    "appId": "netease.sight.controller",
    /** 压缩形式,默认normal;store打包最快,适合测试;maximum打包体积最小,适合生产模式 **/
    "compression": "maximum",
    /** 是否将多个文件合并为tar风格的归档模式 **/
    "asar": true,
    "directories": {
      "output": "build"  /** 打包结果目标地址 **/
    },
    "files": [
      "dist/electron/**/*" /** 需要打包的文件地址 **/
    ],
    /** 不同平台设置 **/
    "mac": {
      "icon": "build/icons/icon.icns"
    },
    "win": {
      "icon": "build/icons/icon.ico"
    },
    "linux": {
      "icon": "build/icons"
    }
}

7. 触摸板放大缩小 -- only mac

在macOS系统中,触摸板的放大缩小手指指令,会导致electron程序内的webFrame内容也跟着放大缩小。

解决方案:在renderer进程中设置其缩放范围require('electron').webFrame.setZoomLevelLimits(1, 1)

8. web端唤起本地客户端

electron提供该API能力:app.setAsDefaultProtocolClient(protocol[, path, args])

9. 禁止多开窗口

多次双击window 的exe文件,会开启多个窗口;mac下默认开1个,但通过命令还是可以多开。

解决方案:判断单实例:app.makeSingleInstance(callback)

/**
 * 防止应用多开。bugfix:sholudQuit总是返回true,故暂时注释以下代码
 * 当进程是第一个实例时,返回false。
 * 如果是第二个实例时,返回true,并且执行第一个实例的回调函数
 */
const shouldQuit = app.makeSingleInstance((commandLine, workingDir) => {
    if (mainWindow) {
        mainWindow.isMinimized() && mainWindow.restore()
        mainWindow.focus()
    }
})
if (shouldQuit) {
    app.quit()
}

10. 网络状态检测

客户端经常遇见断网情况处理,当网络断开时需要给用户提示,当网络连接时继续服务。通常web情况下是采取轮询服务器方式,但这种方式比较消耗服务器性能。这里可以利用electron的node工具包public-ip进行判断。public-ip查询dns获取公网ip地址,如果能拿到值表示联网正常。本来到此可以很好的解决,但产品要求的客户端,既要提供公共部署,也需要进行无外网情况下的私有化部署

解决方案:public-ip + 轮询方式。优先进行公网IP查询,如果成立则返回网络状态良好,如果查询不到再进行服务器心跳检查。实现方式参考is-online

11. 日志监听

每个系统的异常监控都必不可少,特别是私有化部署客户端这种模式,日志记录显得必不可少。由于electron拥有node的环境,结合window.onerror收集错误信息,前端把日志记录在本地文件。当出现问题时,用户可以直接把日志文件发给开发者,从而定位原因。如果是网络版模式,可以通过Ajax收集错误信息。如果是程序异常崩溃,window.onerror可能没法监测的到,好在electron提供了CrashReporter收集

解决方案:推荐electron-log + CrashReporter

const log = require('electron-log')

log.transports.file.level = 'info'
log.transports.file.format = '{h}:{i}:{s}:{ms} {text}'
log.transports.file.maxSize = 5 * 1024 * 1024
log.transports.console.level = false

12. 自动更新

该需求停留在调研,这篇文章讲的非常详细,待实践好再来续更

最后,附上@changkun的electron深度总结思维导图,总结的非常棒,许多细节使笔者受益良多。出处

Node-常用包总结

很多工具包都是基于node环境,node的成功造就了大前端的繁荣。以下总结实用性较强的一些工具包。

Common

  • Tool
    • webpack 打包工具
    • eslint js代码检查
    • stylelint css代码检查
    • prettier 格式化代码工具
    • husky git钩子工具
  • Lib
    • lodash/underscore js基础工具库
    • moment - 重量级时间处理库,支持时间解析、格式化、计算等,功能强大,支持浏览器和 Node.js
    • date-fns - 较 moment 更轻量级的事件处理库,体积更小
    • fastclick 处理移动端点击事件
    • pinyin 前端/nodejs汉字转拼音
  • CSS
    • Sass
      • sass-loader node-sass sassLib,解析sass语法
      • sass-resource 全局导入sass文件,使得不用每个组件引入。vue-cli3默认自带
    • PostCSS
  • Yeoman 快速构建项目,有较多模板可选择
  • browser-sync 取代LiveReload为新型浏览器自动刷新插件,提高多浏览器开发效率。对于没有使用Hot reload的项目非常有用
  • onchange npm script文件变动检测

Vue

  • vue-router 官方路由
  • vuex 官方状态库
  • Nuxt.js - Vue 同构框架
  • axios - vue官方推荐的client库,功能丰富,支持Node和浏览器
  • vuelidate 轻量级表单验证
  • vue-svgicon 轻量级svg图标
  • vue-datepicker-local 轻量级日期时间组件

React

  • 状态库
  • RxJS
  • Next.js - React 同构框架
  • ReactNative
    • react-native-scrollable-tab-view 可滚动tab标签
    • react-navigation 导航库
    • react-native-fit-image 自适应图片库,包括网络图片
    • react-native-vector-icons 图标库
    • react-native-device-info 获取设备信息,系统名/版本/型号等等
    • react-native-simple-store 轻量级store。
    • react-native-storage 持久化存储
    • react-native-splash-screen 首屏splash
    • 更多...

Node

  • 框架
    • express 轻量级web框架,使用最广泛的 Node.js web 框架
    • Koa - express 原班人马打造,轻量精美的框架
    • egg - 基于 Koa,强大的 loader / plugin 等机制,项目架构更清晰可控,阿里巴巴企业级应用框架
    • keystone - 基于 Mongodb 的 CMS
  • 部署工具
    • nodemon - 支持热加载和自动重启
    • pm2 - 支持热启动、负载、集群、监控、重启等功能
    • http-server 静态文件服务器命令行工具,无需配置,一条命令开启 http 服务
  • 模块
    • 客户端代理
      • http.request()/http.get() node原生
      • request 老牌客户端请求代理模块,包装原生Node的http.request,使得调用更加简单。
      • superagent 客户端请求代理模块。轻量的,渐进式的ajax api,特色是链式调用,只支持Node端。
      • got 轻量级,但也支持promise。
      • axios 客户端请求代理模块。功能丰富,支持Promise,支持Node和浏览器
    • cheerio nodejs dom解析库,常用来做爬虫
    • http-proxy - 功能全面的http代理库
    • mongoose - 全能的 MongoDB ORM 库

持续更新中...

AI前端Vue规范

文件命名

统一小写,多个单词作为文件名使用分隔符-

// bad
EntityList.vue
entityList.vue

// good
entity-list.vue

紧密耦合的组件命名

和父组件紧密耦合的子组件应该以父组件名作为前缀命名

// bad
components/
|- todo-list.vue
|- todo-item.vue
└─ todo-button.vue

// good
components/
|- todo-list.vue
|- todo-list-item.vue
└─ todo-list-item-button.vue

自闭合组件

在单文件组件中没有内容的组件应该是自闭合的

<!-- bad -->
<u-input></u-input>

<!-- good -->
<u-input />

指令缩写

: 表示 v-bind: ,用@表示v-on

<!-- bad -->
<input v-bind:value="value" v-on:input="onInput">

<!-- good -->
<input :value="value" @input="onInput">

组件数据

组件的 data 必须是一个函数,并且建议在此不使用箭头函数

// bad
export default {
  data: () => ({
    foo: 'bar'
  })
}

// good
export default {
  data () {
    return {
      foo: 'bar'
    }
  }
}

props命名

小驼峰命名。内容尽量详细,至少有默认值

// bad
greeting-text: String

// good
greetingText: { type: String, default: ''}

组件属性顺序和分行规则

顺序原则:重要属性放前面

顺序依据:依次指令->props属性-> 事件->dom属性(class有标记作用,除外)

分行规则:放在一行,重要内容较多时,可放置2~3行

<!-- bad -->
<u-select
    class="select"
    size="s"
    @select="searchEntity($event, row)"
    @blur="searchEntity($event, row)"
    v-model="row.variableId"
    :list="variableList" />

<!-- good -->
<u-select v-model="row.variableId" :list="variableList" size="s"
    @select="searchEntity($event, row)" @blur="searchEntity($event, row)" class="select" />

Vue API顺序

export default {
    name: '',
    /*1. Vue扩展 */
    extends: '', // extends和mixins都扩展逻辑,需要重点放前面
    mixins: [],   
    components: {},
    /* 2. Vue数据 */
    props: {},
    model: { prop: '', event: '' }, // model 会使用到 props
    data () {
        return {}
    },
    computed: {},
    watch:{}, // watch 监控的是 props 和 data,有必要时监控computed
    /* 3. Vue资源 */
    filters: {},
    directives: {},
    /* 4. Vue生命周期 */
    created () {},
    mounted () {},
    destroy () {},
    /* 5. Vue方法 */
    methods: {}, // all the methods should be put here in the last
}

Vue组件顶级标签顺序

顺序保持一致,且标签之间留有空行。template第一层级下四个空格,script和style第一层级都不加空格

<template>
    <div></div>
</template>

<script>
export default {}
</script>

<style>
.app {}
</style>

import引入顺序 V1.1

原则:同等类型的放一起,优先放mixins和components等UI资源。忌随意放置

// bad
import { getAllEntityList, getVariableGroup } from '@/server/api'
import { helpers } from 'vuelidate/lib/validators'
import { getRepeatLine } from '@/utils/common'
import { CloseModalMixin, InvalidCheckMixin } from '@/components/common/mixins'
import VSearchSelect from '@/components/variable/v-search-select'
import EModifyModal from '@/components/entity/e-modify-modal'
import { MODIFY_MODAL_TYPE } from '@/utils/config'
import { botIdLoc, custIdLoc } from '@/utils/locs'

// good
import { CloseModalMixin, InvalidCheckMixin } from '@/components/common/mixins'
import VSearchSelect from '@/components/variable/v-search-select'
import EModifyModal from '@/components/entity/e-modify-modal'
import { getAllEntityList, getVariableGroup } from '@/server/api'
import { helpers } from 'vuelidate/lib/validators'
import { MODIFY_MODAL_TYPE } from '@/utils/config'
import { getRepeatLine } from '@/utils/common'
import { botIdLoc, custIdLoc } from '@/utils/locs'

Vue 复杂data加注释/分组 V1.1

data数据是连接View和Modal的基础,当ViewModal复杂时,建议进行注释并分组。另外,当data过于复杂时应考虑优化重构。

// bad
data() {
    return {
        isOpenModal: false,
        list: [],
        groupList: [],
        searchParams: { groupId: '', searchParam: '', searchType: '' },
        pageParam: { currentPage: 1, pageSize: 50 },
        totalCount: 0,
        groupId: '',
        typeList: [],
        defaultType: 'paramName'
    }
}

// good
data() {
    return {
        variableList: [],
        groupList: [],
        typeList: [],

        /*
        * 查询参数
        * 组与组之间通过空行区分
        */
        searchParams: { groupId: '', searchParam: '', searchType: '', currentPage: 1, pageSize: 50 },
        totalCount: 0,
        defaultType: '',

        isOpenModal: false
    }
}

参考连接

Vue官方风格指南

有赞风格指南

个人理解Vue和React区别

个人理解Vue和React区别

Vue和React相同点非常多:

  1. 都使用Virtural DOM
  2. 都使用组件化**,流程基本一致
  3. 都是响应式,推崇单向数据流
  4. 都有成熟的社区,都支持服务端渲染

Vue和React实现原理和流程基本一致,都是使用Virtual DOM + Diff算法。不管是Vue的template模板 + options api写法,还是React的Class或者Function(js 的class写法也是function函数的一种)写法,底层最终都是为了生成render函数,render函数执行返回VNode(虚拟DOM的数据结构,本质上是棵树)。当每一次UI更新时,总会根据render重新生成最新的VNode,然后跟以前缓存起来老的VNode进行比对,再使用Diff算法(框架核心)去真正更新真实DOM(虚拟DOM是JS对象结构,同样在JS引擎中,而真实DOM在浏览器渲染引擎中,所以操作虚拟DOM比操作真实DOM开销要小的多)。

Vue和React通用流程:vue template/react jsx -> render函数 -> 生成VNode -> 当有变化时,新老VNode diff -> diff算法对比,并真正去更新真实DOM。

核心还是Virtual DOM,为什么Vue和React都选择Virtual DOM(React首创VDOM,Vue2.0开始引入VDOM)?,个人认为主要有以下几点:

  1. 减少直接操作DOM。框架给我们提供了屏蔽底层dom书写的方式,减少频繁的整更新dom,同时也使得数据驱动视图
  2. 为函数式UI编程提供可能(React核心**)
  3. 可以跨平台,渲染到DOM(web)之外的平台。比如ReactNative,Weex

以下重点说下两者不同的点。

1. 核心**不同

Vue早期定位是尽可能的降低前端开发的门槛(这跟Vue作者是独立开发者也有关系)。所以Vue推崇灵活易用(渐进式开发体验),数据可变,双向数据绑定(依赖收集)

React早期口号是Rethinking Best Practices。背靠大公司Facebook的React,从开始起就不缺关注和用户,而且React想要做的是用更好的方式去颠覆前端开发方式(事实上跟早期jquery称霸前端,的确是颠覆了)。所以React推崇函数式编程(纯组件),数据不可变以及单向数据流。函数式编程最大的好处是其稳定性(无副作用)和可测试性(输入相同,输出一定相同),所以通常大家说的React适合大型应用,根本原因还是在于其函数式编程。

由于两者核心**的不同,所以导致Vue和React许多外在表现不同(从开发层面看)

1.1 核心**不同导致写法差异

Vue推崇template(简单易懂,从传统前端转过来易于理解)、单文件vue。而且虽然Vue2.0以后使用了Virtual DOM,使得Vue也可以使用JSX(bebel工具转换支持),但Vue官方依然首先推荐template,这跟Vue的核心**和定位有一定关系。

React推崇JSX、HOC、all in js

1.2 核心**不同导致api差异

Vue定位简单易上手,基于template模板 + options API,所以不可避免的有较多的概念和api。比如template模板中需要理解slot、filter、指令等概念和api,options API中需要理解watch、computed(依赖收集)等概念和api。

React本质上核心只有一个Virtual DOM + Diff算法,所以API非常少,知道setState就能开始开发了。

1.3 核心**不同导致社区差异

由于Vue定义简单易上手,能快速解决问题,所以很多常见的解决方案,是Vue官方主导开发和维护。比如状态管理库Vuex、路由库Vue-Router、脚手架Vue-CLI、Vutur工具等。属于那种大包大揽,遇到某类通用问题,只需要使用官方给出的解决方案即可。

React只关注底层,上层应用解决方案基本不插手,连最基础的状态管理早期也只是给出flow单向数据流**,大部分都丢给社区去解决。比如状态管理库方面,有redux、mobx、redux-sage、dva等一大堆(选择困难症犯了),所以这也造就了React社区非常繁荣。同时由于有社区做上层应用解决方案,所以React团队有更多时间专注于底层升级,比如花了近2年时间把底层架构改为Fiber架构,以及创造出React Hooks来替换HOC,Suspense等。 更多框架设计**可看 尤雨溪 - 在框架设计中寻求平衡

1.4 核心**不同导致未来升级方向不同

核心**不同,决定了Vue和React未来不管怎么升级变化,Vue和React考虑的基本盘不变。

Vue依然会定位简单易上手(渐进式开发),依然是考虑通过依赖收集来实现数据可变。这点从Vue3核心更新内容可以看到:template语法基本不变、options api只增加了setup选项(composition api)、基于依赖收集(Proxy)的数据可变。更多Vue3具体更新内容可看笔者总结 Vue3设计** 或者 尤雨溪 - 聊聊 Vue.js 3.0 Beta 官方直播

React的函数式编程这个基本盘不会变。React核心**,是把UI作为Basic Type,比如String、Array类型,然后经过render处理,转换为另外一个value(纯函数)。从React Hooks可以看出,React团队致力于组件函数式编程,(纯组件,无class组件),尽量减少副作用(减少this,this会引起副作用)。

2. 组件实现不同

Vue源码实现是把options挂载到Vue核心类上,然后再new Vue({options})拿到实例(vue组件的script导出的是一个挂满options的纯对象而已)。所以options api中的this指向内部Vue实例,对用户是不透明的,所以需要文档去说明this.$slot、this.$xxx这些api。另外Vue插件都是基于Vue原型类基础之上建立的,这也是Vue插件使用Vue.install的原因,因为要确保第三方库的Vue和当前应用的Vue对象是同一个。

React内部实现比较简单,直接定义render函数以生成VNode,而React内部使用了四大组件类包装VNode,不同类型的VNode使用相应的组件类处理,职责划分清晰明了(后面的Diff算法也非常清晰)。React类组件都是继承自React.Component类,其this指向用户自定义的类,对用户来说是透明的。

image

3. 响应式原理不同

这个问题网上已经有许多优秀文章都详细讲解过,这里就不具体展开讲,对Vue3响应式原理有兴趣可以看笔者 Vue3响应式原理(Vue2和Vue3响应式原理基本一致,都是基于依赖收集,不同的是Vue3使用Proxy)。

Vue

  • Vue依赖收集,自动优化,数据可变。
  • Vue递归监听data的所有属性,直接修改。
  • 当数据改变时,自动找到引用组件重新渲染。

React

  • React基于状态机,手动优化,数据不可变,需要setState驱动新的State替换老的State。
  • 当数据改变时,以组件为根目录,默认全部重新渲染

4. diff算法不同

两者流程思维上是类似的,都是基于两个假设(使得算法复杂度降为O(n)):

  1. 不同的组件产生不同的 DOM 结构。当type不相同时,对应DOM操作就是直接销毁老的DOM,创建新的DOM。
  2. 同一层次的一组子节点,可以通过唯一的 key 区分。

但两者源码实现上有区别:

Vue基于snabbdom库,它有较好的速度以及模块机制。Vue Diff使用双向链表,边对比,边更新DOM。

React主要使用diff队列保存需要更新哪些DOM,得到patch树,再统一操作批量更新DOM

image

5. 事件机制不同

Vue

  • Vue原生事件使用标准Web事件
  • Vue组件自定义事件机制,是父子组件通信基础
  • Vue合理利用了snabbdom库的模块插件

React

  • React原生事件被包装,所有事件都冒泡到顶层document监听,然后在这里合成事件下发。基于这套,可以跨端使用事件机制,而不是和Web DOM强绑定。
  • React组件上无事件,父子组件通信使用props

Vue 和 React源码流程图

Vue整体流程图

image

React整体流程图

image

ES6-Object

Object是js最重要的数据结构,es6对其进行了重大升级。除了解构外,Object还提供了大量的基础方法。另外Object对象属性及其方法太常用,有些相似的方法容易使用错误,故根据MDN归类整理。注意文中重点标注的文字以及角标标注的ES发布版本。

Key/Value

Object.keys(obj)ES5

该方法返回一个给定对象的自身可枚举属性组成的数组。

只列出自身的枚举属性

Object.values(obj)ES8

该方法返回一个给定对象自身的所有可枚举属性值的数组

只列出自身的枚举属性值

Object.entries(obj)ES8

该方法返回一个给定对象自身可枚举属性的键值对数组。

只列出自身枚举的键值对数组。

Object.fromEntries()Stage 3是其逆方法,把键值对列表转换为一个对象.

Object.getOwnPropertyNames(obj)

该方法返回一个数组,该数组对元素是 obj自身拥有的枚举或不可枚举属性名称字符串

自身的枚举和不枚举属性都会列出

for ... in

for...in语句以任意顺序遍历一个对象的可枚举属性(包括原型链上的可枚举属性)。包括原型链上的可枚举属性。

自身和原型链上的属性

Descriptor

Object.defineProperty(obj, prop, descriptor)

该方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

Object.getOwnPropertyDescriptor()返回对象对应的属性描述符。

Object.defineProperties(obj, props)

该方法直接在一个对象上定义一个或多个新的属性或修改现有属性,并返回该对象。

Object.getOwnPropertyDescriptors()ES8返回对象所有属性描述符。该方法引入目的是为了解决Object.assign()无法正确拷贝get属性和set属性的问题,详见此

Other

Object.assign(target, ...sources)ES6

该方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象。

翻看源码能知道,它是一层浅拷贝

Object. is(value1, value2)ES6

该方法判断两个值是否是相同的值。

解决ES5中只有===和==判断的不足Equality comparisons and sameness

Object.freeze(obj)Stage 1

该方法可以冻结一个对象,冻结指的是不能向这个对象添加新的属性,不能修改其已有属性的值,不能删除已有属性,以及不能修改该对象已有属性的可枚举性、可配置性、可写性。该方法返回被冻结的对象。目前该方法在tc39 Stage1阶段,兼容性需要看后续发展。

Object.isFrozen(obj)判断一个对象是否被冻结

参考文档:

Eslint代码检查规范 - React/ReactNative

在前端编码时,为了规范每个成员的代码风格以及避免低级的错误,我们可以使用Eslint来制定规则.本文旨在帮助团队成员形成良好的React代码规范。推荐使用Airbnb Eslint规范+自定义Rules

Airbnb Eslint规范

目前使用eslint不再需要自己手动装太多npm包,社区已经在最新eslint初始化命令中自动安装。

安装Eslint

有全局安装和本地安装两种方式,推荐本地安装

npm install --save-dev eslint

初始化Eslint

初始化会供用户很多可选的选择,这里推荐使用流行的Airbnb Eslint。安装完后,在package.json中会自动安装需要的依赖,分别为eslint-config-airbnb、eslint-plugin-import、eslint-plugin-jsx-a11y、eslint-plugin-react。同时也会创建.eslintrc配置文件

eslint --init

Eslint检查

eslint的Command Line Interface有命令行调用接口,如何搭配命令行取决于项目风格。格式:

eslint [options] [file|dir|glob]*

eg:当前app目录下监测js并打印报错

eslint --quiet --ext .js app
--ext [String]                 Specify JavaScript file extensions - default: .js
--quiet                        Report errors only - default: false

tips: 如果测试执行报错,可能你同时安装了本地和全局eslint,这里可以把eslint命令指定为本地路径:./node_modules/.bin/eslint,参考见该issue

自定义Rules

自定义Rules综合考虑了笔者部门小伙伴习惯的Vue风格,如不使用分号结尾,以及React特殊的JSX语法,形成以下推荐配置:

module.exports = {
    "extends": ["airbnb"], // 使用airbnb规则
    "parser": "babel-eslint",// React使用了大量ES6语法,使用babel-eslint解析器代替默认的Espree
    "globals": { // 全局变量设置
        "__DEV__": false // false 表示这个全局变量不允许被重新赋值
    },
    "rules": {
        // 4个空格
        "indent": [2, 4],
        "react/jsx-indent": [2, 4],
        "react/jsx-indent-props": [2, 4],

        "semi": [2, "never"], // 是否使用分号结尾
        "no-console": 'off', // 允许console
        "max-len": "off", // 单行没有字数限制
        "object-curly-newline": "off", // 关闭大括号内换行符的一致性
        "comma-dangle": "off", // 关闭是否使用拖尾逗号
        "arrow-parens": "off", // 关闭箭头函数是否需要大括号

        "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], // 允许使用js/jsx文件扩展
        "react/sort-comp": "off", // 关闭sort
        "react/no-array-index-key": "off",// 允许使用index作为List的key
        "no-unused-expressions": "off",// 允许三元表达式
        "import/no-unresolved": "off",// 允许require image
        "react/no-multi-comp": "off", // 允许一个文件定义多个组件
        "react/display-name": "off", // 不需要给组件定义displayName
    }

};

注意:需要额外安装babel-eslint以解析ES6语法:npm install --save-dev babel-eslint

Vue项目目录结构推荐

目录结构保持一致,使得多人合作容易理解与管理,提高工作效率。Vue标准项目

简要说明

  • main.js主入口,router.js路由划分
  • plugins 自己或第三方插件,包括但不限于components、directives、filters、third lib
  • pages 所有路由页面。原则:轻page,重component
  • components 所有组件。包括原子组件、业务公用组件、页面独有组件
  • server api引入入口
  • assets sass、图片资源入口,不常修改数据
  • utils 工具文件夹
  • store 标准vuex格式,非必须

详细说明

project
└───src
│   │   app.vue    // 主页面
│   │   main.js    // 主入口
|   |   router.js  // 所有路由
│   │
│   |____assets    // css、image、svg等资源
│   |   |____css   // 所有sass资源
|   |   |    |  reset.scss       // 兼容各浏览器
|   |   |    |  global.scss      // 全局css
|   |   |    |  variable.scss    // sass变量和function等
│   |   |____img   // image图标库
|   |   |____svg   // svg图标库
|   |
|   |____components    // 组件
│   |   |____common    // common自注册组件
│   |        |____base // 原子组件(如果是引入第三方,该文件夹可省略)
│   |        |   ...   // 业务公用组件
│   |   |____entity    // entity页面组件
│   |   |____about     // about页面组件
|   |
|   |____pages     // UI层(原则:轻page,重component)
|   |   |____entity
|   |   |    |  list.vue      // 列表页
|   |   |    |  create.vue    // 新增页
|   |   |    |  edit.vue      // 修改页
|   |   | main.vue
|   |
|   |____plugins   // 自己或第三方插件
|   |   | index.js       // 插件入口文件
|   |   | directives.js  // 所有Vue指令
|   |   | filters.js  // 所有Vue过滤
|   |
|   |____server    // 接口层
|   |   | index.js   // 所有接口
|   |   | http.js  // axios二次封装
|   |
|   |____store     // vuex数据
|   |   | index.js
|   |
|   |____utils     // 工具层
|   |   | config.js// 配置文件,包括常量配置
|
└───public         // 公用文件,不经过webpack处理
│   │   favicon.ico
│   │   index.html
│   vue.config.js  // vue-cli3主配置
│   babel.config.js// babel配置
│   .eslintrc.js   // eslint配置
│   .prettierrc.js // perttier配置
│   package.json   // npm配置
│   README.md      // 项目说明

Node-常用API

总结NodeJS常用的API,重点地方有笔者的释义以及详细说明。关联度高的模块放一起叙述,并有对比说明,比如buffer与fs,stream与http,process与child_process。本文尽量做到兼具实用与API广度,更多详细内容请看Node.JS官网文档

path

  • __filename。全局值,当前文件绝对路径
  • __dirname。全局值,当前文件夹绝对路径。等效于path.resolve(__filename, '..')
  • path.join([...paths])。相当于把所传入的任意多的参数 按照顺序 进行命令行般的推进
    path.join('a','b','../c/lolo') // a/c/lolo
  • path.resolve([...paths])。以当前文件的路径为起点,返回绝对路径。可以理解为每次都是新建cd命令
    path.resolve('/a', '/b') // '/b'
    path.resolve('./a', './b') // '/User/../a/b'
  • path.dirname(path(string))。返回指定的路径 所在文件夹的绝对路径
  • path.basename(path(string))。返回指定Path路径所在文件的名字
  • path.extname(path | string)。获取指定字符串或者文件路径名字的后缀名,带.比如.txt

url

url 模块提供了两套 API 来处理 URL 字符串:一个是Node.js特有的API,是旧版本的遗留;另一个则是实现了WHATWG URL Standard的 API (const {URL} = require('url')方式),该标准也在各种浏览器中被使用。

旧版url api,新版URL Standard见这

  • url.parse(urlString[, parseQueryString[, slashesDenoteHost]])。把url字符串解析为url对象
  • url.format(urlObject)。把url对象解析为字符串
  • url.resolve(from, to)。以一种 Web 浏览器解析超链接的方式, 基于一个基础 URL,对目标 URL进行解析。查看其源码实现:
    Url.prototype.resolve = function(relative) {
    return this.resolveObject(urlParse(relative, false, true)).format();
    };

querystring

  • querystring.parse。一个URL查询字符串 str 解析成一个键值对的集合。
  • querystring.stringify。遍历给定的 obj 对象的自身属性,生成 URL 查询字符串。

Stream

Node中很多数据结构都继承自Stream。Stream在文件系统和http请求中,有非常大的作用。

  • 对于文件处理,小文件对于buffer数据结构能很好的解决,直接将文件所有数据加载到内存中;但对于大文件,buffer这种方式不可取,容易打爆内存。最好的方式是像流水一样,将读取的数据实时流入到写入的文件,直到数据全部读完并写入好对应目标文件。举个例子,就像两个水池,待读取的文件是一个装满水的池子,写入的空文件是一个等待蓄满水的空池子。通过Stream API的方式,将两个池子嫁接起来,使得满水的池子可以持续注水到空池子中。而且Stream继承自EmitEvents,过程中会有许多回调事件发出,供开发人员自定义内容。
  • 对于http请求处理,最常见的就是通过流去写入内容。

以下对象继承了可读流:

  • HTTP responses, on the client
  • HTTP requests, on the server
  • fs read streams
  • child process stdout and stderr
  • process.stdin
  • zlib streams
  • crypto streams
  • TCP sockets

Stream类

  • stream.Writable
    • write() 写入数据到流
    • end() 表明已没有数据要被写入
    • Events
      • drain。 每个数据块写入成功后回调事件,可能多次触发。
      • pipe。当readableStream.pipe()调用时回调事件
      • finish。 当writeableStream.end()调用并数据读完后回调事件。
      • close
      • error
  • stream.Readable
    • pipe(destination:stream.Writable, options) 把可读数据流入可写流中
    • pause() 流暂停。使读取流暂停emit 'data'事件
    • resume() 流重启。重新开始emit 'data'事件
    • unpipe()
    • Events
      • data。每次读取完一段数据块回调事件,可能多次触发。
      • readable。数据读取可用时回调事件。
      • end。 数据全部读取完成回调事件。
      • close
      • error
  • stream.Duplex
  • stream.Transform

以下举一个完整例子:

const fs = require('fs')

let readStream = fs.createReadStream('./node/snapshot/test1.png')
let writeStream = fs.createWriteStream('./node/snapshot/test1-copy.png')

readStream
    .on('data', (chunk) => {
        // 当读取较大文件时,写入流的速度可能没有读入的速度快
        // 所以这里做了一个处理,没写完暂停读入流
        if (writeStream.write(chunk) === false)
            readStream.pause()
    })
    .on('end', () => writeStream.end())

// 这里配合上面pause,写完了继续读入流
writeStream.on('drain', () => readStream.resume())

// 以上换成管道的方式,只需一行代码:readStream.pipe(writeStream)

http

  • http.Server。http.createServer(function(req, res){})返回该类。
    • listen()
  • http.ClientRequestNode作为客户端。http.get()/http.request()返回该类。
    • 可写流。详细参见上章节stream.Writable
    • write(chunk[, encoding][, callback])。stream继承,请求写入数据,一般是POST请求需要。
    • end([data][, encoding][, callback])。stream继承,请求发出。
    • 回调函数res是可读流
  • http.ServerResponseNode作为服务端。服务端res即是该类的实例。
    • 可写流
    • write(chunk[, encoding][, callback])
    • end([data][, encoding][, callback])
    • setHeader(name, value)
    • writeHead(statusCode[, statusMessage][, headers])
// Node作为客户端发送请求
const postData = querystring.stringify({
  'msg': 'Hello World!'
});
const options = {
  hostname: 'www.google.com',
  port: 80,
  path: '/upload',
  method: 'POST',
  headers: {
    'Content-Type': 'application/x-www-form-urlencoded',
    'Content-Length': Buffer.byteLength(postData)
  }
};

const req = http.request(options, (res) => {
  // 响应值res作为从目标接口中获取到的值,是个readable.Stream
  res.setEncoding('utf8');
  res.on('data', (chunk) => console.log(`BODY: ${chunk}`));
  res.on('end', () => console.log('No more data in response.'));
});
req.on('error', (e) => {
  console.error(`problem with request: ${e.message}`);
});

// req作为客户端请求,是个writeable.Stream。添加上参数并请求出去
req.write(postData);
req.end();
// Node作为服务端,res(response)参数继承的是writeable.Stream
const http = require('http')

http
    .createServer((req, res) => {
        res.write('hello world')
        res.end()
    })
    .listen(3001)

http.get()与 http.request() 唯一的区别是它设置请求方法为 GET 且自动调用 req.end()

在node是客户端的时候,req是从node这边发起的,是可写流,res是从外边响应的,是可读流。

在node是服务端的时候,req是从客户端发起的,是可读流,res是从node响应的,是可写流。

Bufferglobal

在 TCP 流或文件系统操作等场景中处理字节流。Buffer 类是一个全局变量。Buffer 类的实例类似于整数数组,但 Buffer 的大小是固定的、且在 V8 堆外分配物理内存。 Buffer 的大小在创建时确定,且无法改变。

Node.js v6.0.0 之前,Buffer 实例是通过 Buffer 构造函数创建的,它根据参数返回不同的 Buffer。为了使 Buffer 实例的创建更可靠,new Buffer() 构造函数已被废弃,建议使用 Buffer.from()、Buffer.alloc()和 Buffer.allocUnsafe()

  • 创建Buffer
    • new Buffer(number/string/array)Deprecated。推荐使用 Buffer.from(array/string) 和Buffer.alloc(size)代替。
    • Buffer.from(array/string)
    • Buffer.alloc(size)
  • Buffer静态方法
    • Buffer.isBuffer(obj)
    • Buffer.byteLength(string)
    • Buffer.concat()
    • Buffer.compare()
  • Buffer实例
    • length
    • toString() buffer转为string
    • write(string, offset=0, length, encoding='utf8')。对buffer对象写入string
    • copy()
    • slice()
    • compare()
    • equals()
    • fill()
let buf = new Buffer('hello world') // 初始化之后,实例buf长度无法改变
console.log(buf.length, buf.toString()) // 11, hello world

buf.write('temp')
console.log(buf.length, buf.toString()) // 11, tempo world

buf.write('01234567891234567890')
console.log(buf.length, buf.toString()) // 11, 01234567891

File System

  • file文件操作
    • readFile(path[, options], callback)

      • 没有指定 encoding,则返回原始的 buffer
    • writeFile(file, data[, options], callback)

      • 如果文件已存在,则覆盖文件。
      • data支持 string/Buffer/TypedArray/DataView
      • data 是一个 buffer,则忽略 encoding
    • copyFile(src, dest[, flags], callback)

    • rename(oldPath, newPath, callback)。文件重命名

    • write(fd, string[, positinon[, encoding]], callback)。将 string 写入到 fd 指定的文件写文件

    • exists(url, callback(boolean))Deprecated。查询是否存在,一般用在单纯检查文件而不去操作(open/readFile/writeFile等操作文件不成立时会回调err)文件时。推荐使用fs.stat() or fs.access() 代替该方法。

    • stat(path[, options],callback(err, stat))。查询文件/目录信息

      • stat.isFile 是否一个普通文件
      • stat.isDirectory 是否一个目录
      • stat.ctime 最后一次修改文件的时间
    • createReadStream(path[, options])。 指定路径读取,获得Readable Stream。

    • createWriteStream(path[, options])。创建空的写入流到目标路径,获得Writeable Stream。

  • dir目录操作
    • readdir(path[, options], callback)。读目录,获取目录下的所有文件和文件夹名称。
    • rmdir(path, callback)。移除目录

文件操作的path参数,绝对路径和相对路径都支持(相对路径基于process.cwd())。

const fs = require('fs')
const path = require('path')
let dir = './node/snapshot'
fs.readFile(path.join(dir, 'test1.png'), (err, data) => {
    console.log(Buffer.isBuffer(data)) // true

    fs.writeFile(path.join(dir, 'test1_copy.png'), data, (error) => console.log(error)
})

Processglobal

process对象是一个提供当前node进程信息的全局对象,所以该对象不需要require()引入。process同时也是EventEmitter(典型的发布订阅模式案例)的一个实例,所以有.on()等方法。

  • process.argv。一个包含命令行参数的数组。第一个元素是’node’,第二个元素是JavaScript文件的文件名。接下来的元素则是附加的命令行参数。

    // process.js
    process.argv.forEach(function(val, index, array) {
    console.log(index + ': ' + val);
    });
    
    // output
    $ node process.js one two=three four
    0: node
    1: /Users/node/process.js
    2: one
    3: two=three
    4: four
  • process.env。返回用户设置的环境变量。

    // index.js
    console.log(process.env.NODE_ENV) // production
    
    // output
    $ cross-env NODE_ENV=production node index
    production
  • process.cwd()。返回当前进程的工作目录

    和 __dirname 不同, __dirname 返回的是当前文件的所在目录

  • process.exit()。退出当前程序。

    当执行exit()方法时,可以使用process.on('exit', callback)作为退出程序前的清理工作。

  • process signal Events。当标准POSIX信号被触发(通常是process.kill(signal)或Ctrl+C等操作),nodejs进程可以通过监听对应信号来进行回调。

    • SIGINT:interrupt,程序终止信号,通常在用户按下CTRL+C时发出,用来通知前台进程终止进程。
    • SIGTERM:terminate,程序结束信号,通常用来要求程序自己正常退出。process.kill()缺省产生这个信号。

child_process

子程序,在node中,child_process这个模块非常重要。熟悉shell脚本的同学,可以用它的异步特性完成很多事情。

异步创建子程序有四种方式,后三种底层都是spawn实现的:

  • child_process.spawn(command[, args][, options])
  • child_process.exec(command[, options][, callback])
  • child_process.execFile(file[, args][, options][, callback])
  • child_process.fork(modulePath[, args][, options])

create child_process

  • child_process.spawn。Node.js 的父进程与衍生的子进程之间会建立 stdin、stdout 和 stderr 的管道。
    • options.stdio: stdio(标准输入输出) 用来配置子进程和父进程之间的 IO 通道,可以传递一个数组或者字符串。如,['pipe', 'pipe', 'pipe'],分别配置:标准输入、标准输出、标准错误。如果传递字符串,则三者将被配置成一样的值。简要介绍其中三个可以取的值:
      • pipe(默认):父子进程间建立 pipe 通道,可以通过 stream 的方式来操作 IO
      • inherit:子进程直接使用父进程的 IO(该种情况使用较多,子进程命令中,执行的node文件里使用process对象与主文件中一致)
      • ignore:不建立 pipe 通道,不能 pipe、不能监听 data 事件、IO 全被忽略
const { spawn } = require('child_process');
var ls = spawn('ls', ['-al'],{
    stdio: 'inherit'
});

ls.stdout.on('data', function(data){
    console.log('data from child: ' + data);
});


ls.stderr.on('data', function(data){
    console.log('error from child: ' + data);
});

ls.on('close', function(code){
    console.log('child exists with code: ' + code);
});
  • child_process.exec。创建一个shell,然后在shell里执行命令。执行完成后,将stdout、stderr作为参数传入回调方法。exec 比较适合用来执行 shell 命令,然后获取输出。
const { exec } = require('child_process');

exec('ls -al', function(error, stdout, stderr){
    if(error) {
        console.error('error: ' + error);
        return;
    }
    console.log('stdout: ' + stdout);
    console.log('stderr: ' + typeof stderr);
});
  • events。child_process支持以下事件:
    • exit。子进程退出。注意其和close的区别,当exit触发时,其stdio流有可能还打开着,可以在此时做一些清理工作。通常情况下,child_process.kill()会触发该事件。
    • close。当子进程关闭时。通常情况下,child_process.kill()也会触发该事件。
    • error。当子进程不能关闭时,关闭它会报error事件。调用kill()可能会触发该事件。
    • message。跟child_process.send方法有关,父子进程间通信。
    • disconnect。跟child_process.disconnect方法有关。
var child_process = require('child_process')

var proc = child_process.spawn('pm2-runtime', ['proxy-server', '--', './dist'], { stdio: 'inherit' })

process.on('SIGTERM', () => proc.kill('SIGTERM'))
process.on('SIGINT', () => proc.kill('SIGINT'))
process.on('SIGBREAK', () => proc.kill('SIGBREAK'))
process.on('SIGHUP', () => proc.kill('SIGHUP'))

proc.on('exit', process.exit)

参考文章

Node-Debug for VSCode

调试对于任何一门语言都是及其重要的。好的调试工具能让人更有效率的开发以及查错。Node没有chrome developer tool这样的Web可视化集成调试工具,但VSCode默认集成了TS、Git、Debug等实用工具,而且使用非常方便。VSCode的插件生态,也让VSCode变成前端开发必备的利器。以下介绍VSCode下的Node调试。

基本用法

  1. 进入VScode界面,点击界面左边的第四个类似虫子的按钮,进入调试界面:
  2. 点击页面上方“没有配置”下拉菜单,选择“添加配置”。
  3. 选择Node.js环境。
  4. 选择完成之后,在项目的根目录中会生成一个.vscode的目录,这个目录中存放了各种各样的VScode编辑器的配置。VSCode根据你选择的环境,生成了对应的配置文件lanuch.json。Node内容如下:
{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "Launch Program",
            "program": "${workspaceFolder}/node/http.js" // 调试路径入口,需要根据自己项目进行配置
        }
    ]
}
  1. 设置断点,点击开始调试按钮(绿色三角形),就可以开始调试。

调试参数配置

lanuch.json配置项较多,可查看官方文档详细了解。VSCode也集成了一些常用的调试配置片段,有Node、Chrome、Electron、Gulp等。以下说明几个重要参数:

  • name: 给该配置项取个名字
  • type: 通常有node、chrome等参数
  • request: launch/attach
    • launch模式,由 vscode 来启动一个独立的具有 debug 模式的程序
    • attach模式,是连接已经启动的服务。比如已经在外面将项目启动,突然需要调试,不需要关掉已经启动的项目再去vscode中重新启动,只要以attach的模式启动,vscode可以连接到已经启动的服务。
  • program: debug node入口文件的绝对路径。只在launch模式有效
  • runtimeExecutable: 执行器的绝对路径,默认是node。只在launch模式有效
  • runtimeArgs: 执行器参数。只在launch模式有效

debug使用npm启动

以上Node调试方式有个问题,每次文件入口修改都需要改动lanuch.json配置文件。我们的方法是可以使用让npm script充当入口,让改动变成在package.json中。

以上需要改造两步:

  1. 修改lanuch.json配置成npm命令方式:
{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "Launch via NPM",
            "runtimeExecutable": "npm", // npm 执行器。使用npm script方式作为入口
            "runtimeArgs": [
                "run-script",
                "start:debug"
            ],
            "port": 5858 // 调试的端口指定,attach时用到
        }
    ]
}
  1. 修改package.json的scripts配置
{
    // 注意:需要配置上--inspect-brk=5858以attach到debugger
    "start:debug": "nodemon --inspect-brk=5858 node/http.js"
}

参考文章

前端工程工具链

为了提高整体开发效率,需要定制一些vue/electron/官网等脚手架工具。首先要将一些代码规范考虑在内,需要保持git仓库的代码就像是一个人写出来的。根据团队习惯,考虑后使用组合工具:eslint + stylelint + prettier + husky

  1. eslint: 对js做规则约束。强制校验
  2. stylelint: 对css做规则约束
  3. prettier: 代码格式化。强制格式化
  4. husky:本地的git钩子工具

另外敏捷开发过程中,代码复查是至关重要的一环,团队需要使用工具辅助代码分析。经比较和实践后,使用工具:jsinspect + jscpd

  1. jsinspect: 对js或jsx代码做重复检测。强制校验
  2. jscpd: 对代码重复率进行报告总结,辅助代码复查

eslint

1. 安装

npm install --save-dev eslint eslint-plugin-vue babel-eslint

2. .eslintrc.js配置

module.exports = {
    root: true,
    // 指定代码的运行环境。不同的运行环境,全局变量不一样
    env: {
      browser: true,
      node: true
    },
    parserOptions: {
    // ESLint 默认使用Espree作为其解析器,安装了 babel-eslint 用来代替默认的解析器
      parser: 'babel-eslint'
    },
    // 使得不需要自行定义大量的规则
    extends: [
      // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules.
      'plugin:vue/essential'
    ],
    // 插件
    plugins: [
      'vue'
    ],
    // add your custom rules here
    rules: {
      // allow async-await
      'generator-star-spacing': 'off',
      // allow debugger during development
      'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
      'indent': [2, 4, { 'SwitchCase': 1 }],
      ...
    }
  }

3. 提交前强制校验

将约束命令放置在提交代码前检查,这就要使用husky这个工具,该工具能在提交代码precommit时调用钩子命令。

"scripts": {
    "lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
    "precommit": "npm run lint"
}

prettier

  1. 安装
npm install --save-dev prettier
  1. .prettierrc.js配置

以下感谢团队伙伴@birttany的配置说明

module.exports = {
    printWidth: 100, // 设置prettier单行输出(不折行)的(最大)长度

    tabWidth: 4, // 设置工具每一个水平缩进的空格数

    useTabs: false, // 使用tab(制表位)缩进而非空格

    semi: false, // 在语句末尾添加分号

    singleQuote: true, // 使用单引号而非双引号

    trailingComma: 'none', // 在任何可能的多行中输入尾逗号

    bracketSpacing: true, // 在对象字面量声明所使用的的花括号后({)和前(})输出空格

    arrowParens: 'avoid', // 为单行箭头函数的参数添加圆括号,参数个数为1时可以省略圆括号

    parser: 'babylon', // 指定使用哪一种解析器

    jsxBracketSameLine: true, // 在多行JSX元素最后一行的末尾添加 > 而使 > 单独一行(不适用于自闭和元素)

    rangeStart: 0, // 只格式化某个文件的一部分

    rangeEnd: Infinity, // 只格式化某个文件的一部分

    filepath: 'none', // 指定文件的输入路径,这将被用于解析器参照

    requirePragma: false, // (v1.7.0+) Prettier可以严格按照按照文件顶部的一些特殊的注释格式化代码,这些注释称为“require pragma”(必须杂注)

    insertPragma: false, //  (v1.8.0+) Prettier可以在文件的顶部插入一个 @format的特殊注释,以表明改文件已经被Prettier格式化过了。

    proseWrap: 'preserve' // (v1.8.2+)
}

3. 提交前强制格式化

在提交git时需要对整个项目执行format格式化,使得代码强制统一。格式化之后再用eslint检查语法错误,无误后把格式化后的代码用git add .添加进入。如果有错误直接中断提交。

"scripts": {
    "format": "prettier --write './**/*.{js,ts,vue,json}'",
    "lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
    "precommit": "npm run format && npm run lint && git add ."
}

stylelint

参见另一篇博客

jsinspect

1. 安装

npm install jsinspect --save-dev

2. 提交前强制校验

"scripts": {
    "format": "prettier --write './**/*.{js,ts,vue,json}'",
    "lint": "eslint --ext .js,.vue --ignore-path .gitignore .",
    "inspect": "jsinspect -t 50 ./src",
    "precommit": "npm run format && npm run lint && npm run inspect && git add ."
}

jscpd

1. 安装

npm install jscpd --save-dev

2. 代码复查辅助命令

"scripts": {
    "codereview": "jscpd ./src"
}

Webpack 模块打包原理

Webpack 模块打包原理

在使用webpack的过程中,你是否好奇webpack打包的代码为什么可以直接在浏览器中跑?为什么webpack可以支持各种ES6最新语法?为什么在webpack中可以书写import ES6模块,也支持require CommonJS模块?

模块规范

关于模块,我们先来认识下目前主流的模块规范(自从有了ES6 Module及Webpack等工具,AMD/CMD规范生存空间已经很小了):

  • CommonJS
  • UMD
  • ES6 Module

CommonJS

ES6前,js没有属于自己的模块规范,所以社区制定了 CommonJS规范。而NodeJS所使用的模块系统就是基于CommonJS规范实现的。

// CommonJS 导出
module.exports = { age: 1, a: 'hello', foo:function(){} }

// CommonJS 导入
const foo = require('./foo.js')

UMD

根据当前运行环境的判断,如果是 Node 环境 就是使用 CommonJS 规范, 如果不是就判断是否为 AMD 环境, 最后导出全局变量。这样代码可以同时运行在Node和浏览器环境中。目前大部分库都是打包成UMD规范,Webpack也支持UMD打包,配置API是output.libraryTarget。详细案例可以看笔者封装的npm工具包:cache-manage-js

(function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
    typeof define === 'function' && define.amd ? define(factory) :
    (global.libName = factory());
}(this, (function () { 'use strict';})));

ES6 Module

ES6 模块的设计**是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。具体**和语法可以看笔者的另外一篇文章:ES6-模块详解

// es6模块 导出
export default { age: 1, a: 'hello', foo:function(){} }

// es6模块 导入
import foo from './foo'

Webpack模块打包

既然模块规范有这么多,那webpack是如何去解析不同的模块呢?

webpack根据webpack.config.js中的入口文件,在入口文件里识别模块依赖,不管这里的模块依赖是用CommonJS写的,还是ES6 Module规范写的,webpack会自动进行分析,并通过转换、编译代码,打包成最终的文件。最终文件中的模块实现是基于webpack自己实现的webpack_require(es5代码),所以打包后的文件可以跑在浏览器上。

同时以上意味着在webapck环境下,你可以只使用ES6 模块语法书写代码(通常我们都是这么做的),也可以使用CommonJS模块语法,甚至可以两者混合使用。因为从webpack2开始,内置了对ES6、CommonJS、AMD 模块化语句的支持,webpack会对各种模块进行语法分析,并做转换编译

我们举个例子来分析下打包后的源码文件,例子源代码在 webpack-module-example

// webpack.config.js
const path = require('path');

module.exports = {
    mode: 'development',
  // JavaScript 执行入口文件
  entry: './src/main.js',
  output: {
    // 把所有依赖的模块合并输出到一个 bundle.js 文件
    filename: 'bundle.js',
    // 输出文件都放到 dist 目录下
    path: path.resolve(__dirname, './dist'),
  }
};
// src/add
export default function(a, b) {
    let { name } = { name: 'hello world,'} // 这里特意使用了ES6语法
    return name + a + b
}

// src/main.js
import Add from './add'
console.log(Add, Add(1, 2))

打包后精简的bundle.js文件如下:

// modules是存放所有模块的数组,数组中每个元素存储{ 模块路径: 模块导出代码函数 }
(function(modules) {
// 模块缓存作用,已加载的模块可以不用再重新读取,提升性能
var installedModules = {};

// 关键函数,加载模块代码
// 形式有点像Node的CommonJS模块,但这里是可跑在浏览器上的es5代码
function __webpack_require__(moduleId) {
  // 缓存检查,有则直接从缓存中取得
  if(installedModules[moduleId]) {
    return installedModules[moduleId].exports;
  }
  // 先创建一个空模块,塞入缓存中
  var module = installedModules[moduleId] = {
    i: moduleId,
    l: false, // 标记是否已经加载
    exports: {} // 初始模块为空
  };

  // 把要加载的模块内容,挂载到module.exports上
  modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
  module.l = true; // 标记为已加载

  // 返回加载的模块,调用方直接调用即可
  return module.exports;
}

// __webpack_require__对象下的r函数
// 在module.exports上定义__esModule为true,表明是一个模块对象
__webpack_require__.r = function(exports) {
  Object.defineProperty(exports, '__esModule', { value: true });
};

// 启动入口模块main.js
return __webpack_require__(__webpack_require__.s = "./src/main.js");
})
({
  // add模块
  "./src/add.js": (function(module, __webpack_exports__, __webpack_require__) {
    // 在module.exports上定义__esModule为true
    __webpack_require__.r(__webpack_exports__);
    // 直接把add模块内容,赋给module.exports.default对象上
    __webpack_exports__["default"] = (function(a, b) {
      let { name } = { name: 'hello world,'}
      return name + a + b
    });
  }),

  // 入口模块
  "./src/main.js": (function(module, __webpack_exports__, __webpack_require__) {
    __webpack_require__.r(__webpack_exports__)
    // 拿到add模块的定义
    // _add__WEBPACK_IMPORTED_MODULE_0__ = module.exports,有点类似require
    var _add__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/add.js");
    // add模块内容: _add__WEBPACK_IMPORTED_MODULE_0__["default"]
    console.log(_add__WEBPACK_IMPORTED_MODULE_0__["default"], Object(_add__WEBPACK_IMPORTED_MODULE_0__["default"])(1, 2))
  })
});

以上核心代码中,能让打包后的代码直接跑在浏览器中,是因为webpack通过__webpack_require__ 函数模拟了模块的加载(类似于node中的require语法),把定义的模块内容挂载到module.exports上。同时__webpack_require__函数中也对模块缓存做了优化,防止模块二次重新加载,优化性能。

再让我们看下webpack的源码:

// webpack/lib/MainTemplate.js

// 主文件模板
// webpack生成的最终文件叫chunk,chunk包含若干的逻辑模块,即为module
this.hooks.render.tap( "MainTemplate",
(bootstrapSource, chunk, hash, moduleTemplate, dependencyTemplates) => {
  const source = new ConcatSource();
  source.add("/******/ (function(modules) { // webpackBootstrap\n");
  // 入口内容,__webpack_require__就在bootstrapSource中
  source.add(new PrefixSource("/******/", bootstrapSource));
  source.add("/******/ })\n");
  source.add(
    "/************************************************************************/\n"
  );
  source.add("/******/ (");
  source.add(
    // 依赖的module都会写入对应数组
    this.hooks.modules.call(
      new RawSource(""),
      chunk,
      hash,
      moduleTemplate,
      dependencyTemplates
    )
  );
  source.add(")");
  return source;
}

Webpack ES6语法支持

可能细心的读者看到,以上打包后的add模块代码中依然还是ES6语法,在低端的浏览器中不支持。这是因为没有对应的loader去解析js代码,webpack把所有的资源都视作模块,不同的资源使用不同的loader进行转换。

这里需要使用babel-loader及其插件@babel/preset-env进行处理,把ES6代码转换成可在浏览器中跑的es5代码。

// webpack.config.js
module.exports = {
  ...,
  module: {
    rules: [
      {
        // 对以js后缀的文件资源,用babel进行处理
        test: /\.m?js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ['@babel/preset-env']
          }
        }
      }
    ]
  }
};
// 经过babel处理es6语法后的代码
__webpack_exports__["default"] = (function (a, b) {
  var _name = {    name: 'hello world,'  }, name = _name.name;
  return name + a + b;
});

Webpack 模块异步加载

以上webpack把所有模块打包到主文件中,所以模块加载方式都是同步方式。但在开发应用过程中,按需加载(也叫懒加载)也是经常使用的优化技巧之一。按需加载,通俗讲就是代码执行到异步模块(模块内容在另外一个js文件中),通过网络请求即时加载对应的异步模块代码,再继续接下去的流程。那webpack是如何执行代码时,判断哪些代码是异步模块呢?webpack又是如何加载异步模块呢?

webpack有个require.ensure api语法来标记为异步加载模块,最新的webpack4推荐使用新的import() api(需要配合@babel/plugin-syntax-dynamic-import插件)。因为require.ensure是通过回调函数执行接下来的流程,而import()返回promise,这意味着可以使用最新的ES8 async/await语法,使得可以像书写同步代码一样,执行异步流程。

现在我们从webpack打包后的源码来看下,webpack是如何实现异步模块加载的。修改入口文件main.js,引入异步模块async.js:

// main.js
import Add from './add'
console.log(Add, Add(1, 2), 123)

// 按需加载
// 方式1: require.ensure
// require.ensure([], function(require){
//     var asyncModule = require('./async')
//     console.log(asyncModule.default, 234)
// })

// 方式2: webpack4新的import语法
// 需要加@babel/plugin-syntax-dynamic-import插件
let asyncModuleWarp = async () => await import('./async')
console.log(asyncModuleWarp().default, 234)
// async.js
export default function() {
    return 'hello, aysnc module'
}

以上代码打包会生成两个chunk文件,分别是主文件main.bundle.js以及异步模块文件0.bundle.js。同样,为方便读者快速理解,精简保留主流程代码。

// 0.bundle.js

// 异步模块
// window["webpackJsonp"]是连接多个chunk文件的桥梁
// window["webpackJsonp"].push = 主chunk文件.webpackJsonpCallback
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([
  [0], // 异步模块标识chunkId,可判断异步代码是否加载成功
  // 跟同步模块一样,存放了{模块路径:模块内容}
  {
  "./src/async.js": (function(module, __webpack_exports__, __webpack_require__) {
      __webpack_require__.r(__webpack_exports__);
      __webpack_exports__["default"] = (function () {
        return 'hello, aysnc module';
      });
    })
  }
]);

以上知道,异步模块打包后的文件中保存着异步模块源代码,同时为了区分不同的异步模块,还保存着该异步模块对应的标识:chunkId。以上代码主动调用window["webpackJsonp"].push函数,该函数是连接异步模块与主模块的关键函数,该函数定义在主文件中,实际上window["webpackJsonp"].push = webpackJsonpCallback,详细源码咱们看看主文件打包后的代码:

// main.bundle.js

(function(modules) {
// 获取到异步chunk代码后的回调函数
// 连接两个模块文件的关键函数
function webpackJsonpCallback(data) {
  var chunkIds = data[0]; //data[0]存放了异步模块对应的chunkId
  var moreModules = data[1]; // data[1]存放了异步模块代码

  // 标记异步模块已加载成功
  var moduleId, chunkId, i = 0, resolves = [];
  for(;i < chunkIds.length; i++) {
    chunkId = chunkIds[i];
    if(installedChunks[chunkId]) {
      resolves.push(installedChunks[chunkId][0]);
    }
    installedChunks[chunkId] = 0;
  }

  // 把异步模块代码都存放到modules中
  // 此时万事俱备,异步代码都已经同步加载到主模块中
  for(moduleId in moreModules) {
    modules[moduleId] = moreModules[moduleId];
  }

  // 重点:执行resolve() = installedChunks[chunkId][0]()返回promise
  while(resolves.length) {
    resolves.shift()();
  }
};

// 记录哪些chunk已加载完成
var installedChunks = {
  "main": 0
};

// __webpack_require__依然是同步读取模块代码作用
function __webpack_require__(moduleId) {
  ...
}

// 加载异步模块
__webpack_require__.e = function requireEnsure(chunkId) {
  // 创建promise
  // 把resolve保存到installedChunks[chunkId]中,等待代码加载好再执行resolve()以返回promise
  var promise = new Promise(function(resolve, reject) {
    installedChunks[chunkId] = [resolve, reject];
  });

  // 通过往head头部插入script标签异步加载到chunk代码
  var script = document.createElement('script');
  script.charset = 'utf-8';
  script.timeout = 120;
  script.src = __webpack_require__.p + "" + ({}[chunkId]||chunkId) + ".bundle.js"
  var onScriptComplete = function (event) {
    var chunk = installedChunks[chunkId];
  };
  script.onerror = script.onload = onScriptComplete;
  document.head.appendChild(script);

  return promise;
};

var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
// 关键代码: window["webpackJsonp"].push = webpackJsonpCallback
jsonpArray.push = webpackJsonpCallback;

// 入口执行
return __webpack_require__(__webpack_require__.s = "./src/main.js");
})
({
"./src/add.js": (function(module, __webpack_exports__, __webpack_require__) {...}),

"./src/main.js": (function(module, exports, __webpack_require__) {
  // 同步方式
  var Add = __webpack_require__("./src/add.js").default;
  console.log(Add, Add(1, 2), 123);

  // 异步方式
  var asyncModuleWarp =function () {
    var _ref = _asyncToGenerator( regeneratorRuntime.mark(function _callee() {
      return regeneratorRuntime.wrap(function _callee$(_context) {
        // 执行到异步代码时,会去执行__webpack_require__.e方法
        // __webpack_require__.e其返回promise,表示异步代码都已经加载到主模块了
        // 接下来像同步一样,直接加载模块
        return __webpack_require__.e(0)
              .then(__webpack_require__.bind(null, "./src/async.js"))
      }, _callee);
    }));

    return function asyncModuleWarp() {
      return _ref.apply(this, arguments);
    };
  }();
  console.log(asyncModuleWarp().default, 234)
})
});

从上面源码可以知道,webpack实现模块的异步加载有点像jsonp的流程。在主js文件中通过在head中构建script标签方式,异步加载模块信息;再使用回调函数webpackJsonpCallback,把异步的模块源码同步到主文件中,所以后续操作异步模块可以像同步模块一样。
源码具体实现流程:

  1. 遇到异步模块时,使用__webpack_require__.e函数去把异步代码加载进来。该函数会在html的head中动态增加script标签,src指向指定的异步模块存放的文件。
  2. 加载的异步模块文件会执行webpackJsonpCallback函数,把异步模块加载到主文件中。
  3. 所以后续可以像同步模块一样,直接使用__webpack_require__("./src/async.js")加载异步模块。

注意源码中的promise使用非常精妙,主模块加载完成异步模块才resolve()

总结

  1. webpack对于ES模块/CommonJS模块的实现,是基于自己实现的webpack_require,所以代码能跑在浏览器中。
  2. 从 webpack2 开始,已经内置了对 ES6、CommonJS、AMD 模块化语句的支持。但不包括新的ES6语法转为ES5代码,这部分工作还是留给了babel及其插件。
  3. 在webpack中可以同时使用ES6模块和CommonJS模块。因为 module.exports很像export default,所以ES6模块可以很方便兼容 CommonJS:import XXX from 'commonjs-module'。反过来CommonJS兼容ES6模块,需要额外加上default:require('es-module').default。
  4. webpack异步加载模块实现流程跟jsonp基本一致。

参考文章

Vue实例选项顺序推荐

在Vue中,export default对象中有很多约定的API Key。每个人的顺序排放都可能不一致,但保持统一的代码风格有助于团队成员多人协作。

Vue官网文档中也有推荐顺序,文档中对选项顺序做了许多分类。但从工程项目角度思考,需要更加精简以及合理的排序。推荐如下规则进行排序:

  1. Vue扩展: extends, mixins, components
  2. Vue数据: props, model, data, computed, watch
  3. Vue资源: filters, directives
  4. Vue生命周期: created, mounted, destroy...
  5. Vue方法: methods

以下推荐顺序,基于团队小伙伴@akimoto整理的顺序,最终推荐版:

export default {
    name: '',
    /*1. Vue扩展 */
    extends: '', // extends和mixins都扩展逻辑,需要重点放前面
    mixins: [],   
    components: {},
    /* 2. Vue数据 */
    props: {},
    model: { prop: '', event: '' }, // model 会使用到 props
    data () {
        return {}
    },
    computed: {},
    watch:{}, // watch 监控的是 props 和 data,有必要时监控computed
    /* 3. Vue资源 */
    filters: {},
    directives: {},
    /* 4. Vue生命周期 */
    created () {},
    mounted () {},
    destroy () {},
    /* 5. Vue方法 */
    methods: {}, // all the methods should be put here in the last
}

Express API及源码解析

nodejs使得可以用javascirpt语言编写后台应用,但使用原生nodejs开发web应用非常复杂。Express是目前最流行的基于Node.js的Web开发框架,可以快速地搭建一个完整功能的网站。以下结合开发文档express源码,整理出常用的一些API以及它们的关系,使得读者理解更加通透。

Express

  • static class
    • Router() 创建一个router对象
    • static() 设置静态资源根目录,基于serve-static模块
  • instance
    • 路由相关
      • app.use(path, callback) 主要用来添加非路由中间件,底层调用router.use()
        • 匹配Path的方式:
          • 路径: /abcd
          • 路径模式: /abc?d
          • 正则表达式: //abc|/xyz/
          • 数组合集: ['/abcd', '/abc?e', //abc|/xyz/]
      • app.all/METHOD(path, callback [, callback ...]) 注册一个http请求路由
      • app.route(path) 获得route实例
    • 实例方法
      • app.get(name) 获取app上定义属性
      • app.set(name, value) 绑定或设置属性到app上
      • app.listen() 跟Node的http.Server.listen()一致

大部分情况app.use()和app.all()使用相似,最大不一样是中间件执行顺序。app.use()针对主进程,放前面跟放最后不一样;但app.all针对应用的路由,放的位置与中间件执行无关。stackoverflow

var express = require('express')
var logger = require('morgan')

// 中间件
app.use(logger()) // 每次都记录日志
app.use(express.static(__dirname+'/public'))

// 路由
app.get('/api', (req, res) => res.send('api router'))
app.listen(3000, () => console.log('success'))

Router

跟express路由API相似:

  • router.use(path, callback)
  • router.all/METHOD(path, [callback])
  • router.route()
var express = require('express');
var app = express();

// method方式路由
app.get('/api', (req, res) => res.send('api router'))
app.get('/api/:id', (req, res) => {
    res.send('api detail')
})

// method多回调路由
var cb0 = function (req, res, next) {
    console.log('CB0');
    next();
}
var cb1 = function (req, res, next) {
    console.log('CB1');
    next();
}
var cb2 = function (req, res) {
    res.send('Hello from C!');
}
app.get('/example/c', [cb0, cb1, cb2]);

// app.route方式路由
app.route('/example/d')
.get(function(req, res) {
    res.send('Get a random book');
})
.post(function(req, res) {
    res.send('Add a book');
})
.put(function(req, res) {
    res.send('Update the book');
});

// 子路由方式
var router = express.Router();
router.get('/user/:id', function (req, res) {
    res.send('OK');
});
router.post('/user/:id', function (req, res) {
    res.send('Post OK');
});
app.use('api', router);

app.listen(3000);

Request

Express Request扩展了node http.IncomingMessage类,主要是增强了一些获取请求参数的便捷API。源代码在这

  • req.headersextend http 返回header object对象
  • req.urlextend http 返回除域名外所有字符串
  • req.methodextend http 返回请求类型GET、POST等
  • req.get(name)/req.header(name) 底层调用node http 模块的req.headers
  • req.params 返回参数对象,对应的属性名由定义路由时确定。比如app.get('/user/:id')路由时,可以通过req。params.id取得参数
  • req.query 返回查询参数object对象。等同于require('url').parse(req.url,true).query;底层中使用parseurl模块
  • req.path 返回字符串。跟req.url比,不带query后缀
  • req.body post请求获取到数据。需要使用body-parser中间件
  • req.cookies 拿到cookies值。需要使用cookie-parser中间件
// http://localhost:3000/api/1?type=123
app.use((req, res, next) => {
    console.log(req.query) // { type: '123' }
    console.log(req.path) // /api/1
    console.log(req.params) // can got req.params.id
    console.log(req.body) // usually in post method
    console.log(req.cookies) // need  cookie-parser middleware

    // extend http.IncomingMessage
    console.log(req.url) // /api/1?type=123
    console.log(req.headers) // header object
    console.log(req.method) // GET
    next()
})

Response

Express Response扩展了node http.ServerResponse类,主要是增加一些便捷api以及返回数据时一些默认参数处理。源代码在这

  • 发送数据
    • res.write(chunk[, encoding][, callback])extend http 写入数据
    • res.end([data] [, encoding])extend http
    • res.send([body]) body可选:Buffer、object、string、Array。除非之前set过Content-Type,否则该方法会根据参数类型自动设置Content-Type,底层写入数据使用res.end()
    • res.json() 返回json对象。底层调用res.send()
    • res.redirect([status,] path) 302转发url
    • res.render(view [, locals] [, callback]) 输出对应html数据
    • res.sendStatus(statusCode) status和send的快捷键
  • 设置响应头
    • res.getHeader(name, value)extend http
    • res.setHeader(name, value)extend http
    • res.get(field) 底层调用res.getHeader()
    • res.set(field [, value])/res.header() 底层调用res.setHeader()
    • res.status(code) 底层直接赋值statusCode属性
    • res.type(type) 快捷设置Content-Type,底层调用res.set('Content-Type', type)
    • res.cookie(name, value, options) 获取cookie
res.status(404).end();
res.status(404).send('Sorry, we cannot find that!');
res.status(500).json({ error: 'message' });

res.sendStatus(200); // equivalent to res.status(200).send('OK')
res.type('json'); // => 'application/json'
res.set('Content-Type', 'text/plain');

路由机制源码解析

路由机制是express精髓。源码中,request、response、view模块都清晰易懂,可能就是router这块容易让人看糊涂。这里对express路由机制源码做下个人整理:

express与子路由有相同API

细心的读者可以发现,express实例和new Router()有一样的API:

  • express/router.use(path, callback)
  • express/router.all/METHOD(path, callback)。all只是METHOD的合集,故分为一类
  • express/router.route(path)

这是因为express实例中保存着一个单例模式的主Router对象(下文都叫主路由),这就意味着Router有的API都可以在express实例上。源码在application.js的137行

app.lazyrouter = function lazyrouter() {
  if (!this._router) {
    this._router = new Router({ // 单例模式的Router
      caseSensitive: this.enabled('case sensitive routing'),
      strict: this.enabled('strict routing')
    });

    // 默认应用两个中间件
    this._router.use(query(this.get('query parser fn')));
    this._router.use(middleware.init(this));
  }
};

express/router.use(path, callback)

use方法一般用于执行中间件。这里为了方便理解,把一些参数处理等干扰代码省略了。我们可以很明显的看到,express.use使用了主路由use方法。所以简单理解express.use(args) = router.use(args)

// application.js L187行
app.use = function use(fn) {
   // 获取单例主路由
  this.lazyrouter();
  var router = this._router;

  fns.forEach(function (fn) {
    if (!fn || !fn.handle || !fn.set) {
      // 交给router对象去处理
      return router.use(path, fn);
    }
  }, this);

  return this;
};

现在去看下router中use方法,同样去除一些参数处理等干扰代码。最终定义了Layer对象把路径和回调函数做了包装,并把layer压入stack中,方便调用时循环stack以执行匹配的回调函数。

// router/index.js L428行
proto.use = function use(fn) {
    // layer对象包装path和回调函数
    var layer = new Layer(path, {
      sensitive: this.caseSensitive,
      strict: false,
      end: false
    }, fn);
    // use通常是非路由中间件,故没有route实例
    layer.route = undefined;
    // 压入stack中,路由匹配时会从stack遍历
    this.stack.push(layer);

  return this;
};

express/router.route(path)

该方法返回一个Route对象,注意是Route对象,不是Router对象。代码很简单,还是拿到主路由并调用主路由的route方法。

// application L254行
app.route = function route(path) {
  this.lazyrouter();
  return this._router.route(path);
};

router.route方法是每次新建一个Route对象(存储了定义的路由METHOD方法),同样经过Layer包装,压入stack,并最终返回该Route实例。所以简单理解,express.route(path) = new Route(path)

重点讲下为什么需要layer.route = route。路由匹配的两个必备匹配条件:path路径 + method方法。express.use这种执行中间件方法只要求有path就可以;express.get/post/...需要同时给到path和method,express.get/post/...底层都会调用express.route以得到一个Route实例。Route实例存储了对应路由上哪些方法被注册,比如只有get方式可以匹配到。所以当实际匹配路由时,从router的stack遍历找到对应layer后,如果是中间件就不找了,如果是路由方法则需要通过layer找到对应Route实例,再继续匹配。

// router/index.js L491行
proto.route = function route(path) {
  // 创建了path下的Route
  var route = new Route(path);

  // 同样用layer包装。
  // 注意回调函数传递的是route.dispatch函数,这里是逻辑递增的关键
  // 保证了定义在路由上的多个中间件函数被按照定义的顺序依次执行
  var layer = new Layer(path, {
    sensitive: this.caseSensitive,
    strict: this.strict,
    end: true
  }, route.dispatch.bind(route));
  // route方法通常用于路由,需要知道具体的请求method
  // 所以需要从statck找到layer,再找到具体route
  // route实例上存储了对应path路由的哪些method
  layer.route = route; 
  this.stack.push(layer);

  // 返回该route实例
  return route;
};

express/router.all/METHOD(path, callback)

该方法用于注册一个get/post/...路由。从源码中可以看出,先实例化一个Route对象,最终执行的是该对象的METHOD方法。简单理解,express.get(args) = new Route().get(args)

// application L472行
methods.forEach(function(method){
    this.lazyrouter();
    // 新实例化Route对象,并返回
    var route = this._router.route(path);
    // 执行Route对象的get/post/...方法
    route[method].apply(route, slice.call(arguments, 1));
    return this;
});

接下来让我们看下Route对象下的METHOD方法。该方法也对回调函数进行了包装并且也存入stack中。由此可知,凡是路由机制API中有回调函数的,都会经过Layer进行包装。路由匹配到的时候会被调用

// router/route.js L92
methods.forEach(function(method){
  Route.prototype[method] = function(){
    var handles = flatten(slice.call(arguments));

    for (var i = 0; i < handles.length; i++) {
      var handle = handles[i];

      // 在Route对象中,调用get/post方法也用Layer包装,并存储在stack中
      var layer = Layer('/', {}, handle);
      layer.method = method;
      this.methods[method] = true;
      this.stack.push(layer); // 这里是Route对象的stack
    }

    return this;
  };
});

路由匹配调用

在哪里判断是否匹配呢?从源码看你能得到app.handle-->Router.handle。以下是抽取的主要代码以及详细注视,以下的代码解释中能理解上面提到的所有内容。随手画了个执行流程图:
image

proto.handle = function handle(req, res, out) {
  var self = this;
  // 拿到主路由的stack
  var stack = self.stack;

  // next方法循环处理stack
  next();

  function next(err) {
    var layer;
    var match;
    var route;

    // match为true以及idx小于stack长度才继续循环
    // 其他情况都跳出循环
    while (match !== true && idx < stack.length) {
      layer = stack[idx++];
      // 匹配path
      match = matchLayer(layer, path);
      route = layer.route;
      // 没有匹配到,继续下次循环
      if (match !== true) {
        continue;
      }

      // 无路由的中间件,跳出while循环(此时match = true)
      if (!route) {
        continue;
      }

      // 有路由的需要拿到route实例,再判断是否匹配到method
      var method = req.method;
      var has_method = route._handles_method(method);
      // 没有匹配到则继续循环,否则跳出循环
      if (!has_method && method !== 'HEAD') {
        match = false;
        continue;
      }
    }

    // 匹配到的layer都会执行到这
    // process_params主要处理express.param API,这里不展开
    self.process_params(layer, paramcalled, req, res, function (err) {
      if (err) {
        return next(layerError || err);
      }

      // layer的handle_request函数是执行回调函数
      // 把next函数传递下去是为了继续循环执行
      layer.handle_request(req, res, next);
    });
  }
Layer.prototype.handle_request = function handle(req, res, next) {
  var fn = this.handle;

  if (fn.length > 3) {
    // not a standard request handler
    return next();
  }

  try {
    // 暴露给外面的回调函数,包含三个参数req、res、next
    // 所以这就解释了为什么一定要执行next()方法才能路由链路一直走下去
    fn(req, res, next);
  } catch (err) {
    next(err);
  }
};

总结

  • Route模块对应的是route.js,主要是来处理路由信息的,每条路由都会生成一个Route实例。
  • Router模块下可以定义多个路由,也就是说,一个Router模块会包含多个Route模块。
  • exress实例化了一个new Router(),实际上注册和执行路由都是通过调用Router实例的方法。类似于中介者模式
  • 凡事有回调的都是用Layer对象包装,Layer对象中有match函数来检验是否匹配到路由,handle_request函数来执行回调
  • 路由流程总结:当客户端发送一个http请求后,会先进入express实例对象对应的router.handle函数中,router.handle函数会通过next()遍历stack中的每一个layer进行match,如果match返回true,则获取layer.route,执行route.dispatch函数,route.dispatch同样是通过next()遍历stack中的每一个layer,然后执行layer.handle_request,也就是调用中间件函数。直到所有的中间件函数被执行完毕,整个路由处理结束。

参考文章

axios用法和原理

axios是一个非常小巧而好用的http请求库,支持promise以及同时支持浏览器和node端。axios使用简单,配置灵活,也是vue官方推荐的请求库。另外axios源码层次清晰明了,非常适合阅读。

特性

  • 从浏览器中创建 XMLHttpRequest
  • 从 node.js 发出 http 请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求和响应数据
  • 取消请求
  • 自动转换JSON数据
  • 客户端支持防止 CSRF/XSRF

API

  • 全局
    • axios.request(config) 最终http请求都是执行这个方法
    • axios(config) 和axios.request()等价
    • axios(url[, config]) axios(config)快捷方式
    • axios.[METHODS](url, config) axios(config)快捷方式
  • 自定义实例
    • axios.create(config) 自定义配置,创建实例instance。调用方式和axios方法一致
  • 拦截器
    • axios.interceptors.request.use
    • axios.interceptors.response.use
// 以下实例等价
// 全局调用
axios({
  method:'get',
  url:'http://bit.ly/2mTM3nY',
  field: 123
}) // axios(config)
axios('http://bit.ly/2mTM3nY', {field: 123}) // axios(url[, config])
axios.get('http://bit.ly/2mTM3nY', {field: 123}) // axios.[METHODS](url, config)

// 自定义实例调用
const instance = axios.create({
  baseURL: 'http://bit.ly'
});
instance({
  method:'get',
  url:'2mTM3nY',
  field: 123
}) // instance(config)
instance.get('2mTM3nY', {field: 123}) // instance.[METHODS](url, config)

配置优先级:lib / defaults.js中的库默认值 -->实例的config属性--> 请求的config参数

为何axios有如此多使用方式

重点是createInstance方法,该方法拿到一个Function,该Function指向请求入口Axios.prototype.request,并且该Function还继承了Axios.prototype的每个方法,并且上下文指向同一个对象context。axios包默认导出是该Function,而自定义实例axios.create是一个工厂模式,最终都调用createInstance方法。源码在lib/default.js中:

function createInstance(defaultConfig) {
  var context = new Axios(defaultConfig);
  // instance指向了request方法,且上下文指向context
  // instance(config) = Axios.prototype.request(config)
  var instance = bind(Axios.prototype.request, context);

  // 把Axios.prototype上的方法扩展到instance对象上
  // 这样 instance 就有了 get、post、put等METHOD方法
  // 同时指定上下文为context,这样执行Axios原型链上的方法时,this会指向context
  utils.extend(instance, Axios.prototype, context);

  // 把context对象上的自身属性和方法扩展到instance上
  utils.extend(instance, context);

  return instance;
}

// 导出时就创建一个默认实例,所以可以通过axios(config)发出请求
var axios = createInstance(defaults);
axios.Axios = Axios;

// 工厂模式创建axios实例,其实最终都是调用createInstance方法。
// 所以实例调用方式和全局axios调用方式相同。instance(config) = axios(config)
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
module.exports = axios;
module.exports.default = axios; // 允许在ts中导入

Axios类是核心内容,该类request方法是所有请求的开始入口。源码在lib/core/Axios.js:

Axios.prototype.request = function request(config) {
  // 允许 axios('url'[, config]) = axios(config)
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }

  // 配置文件合并策略优先级
  config = mergeConfig(this.defaults, config);
  config.method = config.method ? config.method.toLowerCase() : 'get';

  // 拦截器中间件钩子
  // dispatchRequest是真正开始下发请求,执行config中设置的adapter方法
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);
  // 添加请求前钩子
  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });
  // 添加请求后钩子
  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

// 提供对request方法的METHOD快捷方式。axios.get(url, config) = axios(config)
utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  Axios.prototype[method] = function(url, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url
    }));
  };
});

utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  Axios.prototype[method] = function(url, data, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url,
      data: data
    }));
  };
});

智能的默认设置和扩展

  • 根据环境自动设置请求的adapter,同时支持自定义
  • 自动根据请求data数据类型,设置headers。比如Content-Type自动设置
  • 自动根据响应data数据类型,转为json
  • 支持自定义转换请求和响应数据

源码在lib/default.js中

var defaults = {
  // 根据环境选择默认的请求方式。支持node和浏览器,也可以自定义adapter
  adapter: getDefaultAdapter(),
  // 请求转换
  transformRequest: [function transformRequest(data, headers) {
    normalizeHeaderName(headers, 'Accept');
    normalizeHeaderName(headers, 'Content-Type');
    if (utils.isFormData(data) ||
      utils.isArrayBuffer(data) ||
      utils.isBuffer(data) ||
      utils.isStream(data) ||
      utils.isFile(data) ||
      utils.isBlob(data)
    ) {
      return data;
    }
    if (utils.isArrayBufferView(data)) {
      return data.buffer;
    }
    if (utils.isURLSearchParams(data)) {
      setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
      return data.toString();
    }
    if (utils.isObject(data)) { // 对象数据时,自动设置Content-Type为json格式。
      setContentTypeIfUnset(headers, 'application/json;charset=utf-8');
      return JSON.stringify(data);
    }
    return data;
  }],
  // 响应转换
  transformResponse: [function transformResponse(data) {
    /*eslint no-param-reassign:0*/
    if (typeof data === 'string') {
      try {
        data = JSON.parse(data);
      } catch (e) { /* Ignore */ }
    }
    return data;
  }],

  timeout: 0,
  xsrfCookieName: 'XSRF-TOKEN',
  xsrfHeaderName: 'X-XSRF-TOKEN',
  maxContentLength: -1,
  validateStatus: function validateStatus(status) {
    return status >= 200 && status < 300;
  }
};

for in和for of区别

for in

以任意顺序遍历一个对象的可枚举属性。遍历数组时,key为数组下标字符串;遍历对象,key为对象字段名。

数组

let arr = [{age: 1}, {age: 5}, {age: 100}, {age: 34}]
for (let key in arr) {
    console.log(key, arr[key])
}
// 打印
// 0 {age: 1}
// 1 {age: 5}
// 2 {age: 100}
// 3 {age: 34}

对象

let obj = {f1: 'test1', f2: 'test2'}
for (let key in obj) {
    console.log(key, obj[key])
}
// 打印
// f1 test1
// f2 test2

for in 缺点

  1. for in 迭代顺序依赖于执行环境,不一定保证顺序
  2. for in 不仅会遍历当前对象,还包括原型链上的可枚举属性
  3. for in 没有break中断
  4. for in 不适合遍历数组,主要应用为对象

for of

ES6引入的新语法。在可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments对象,NodeList对象)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句。

Object对象不是可迭代对象,故for of 不支持。for of有个很大的特点是支持数组的break中断。

数组

let arr = [{age: 1}, {age: 5}, {age: 100}, {age: 34}]
for(let {age} of arr) {
    if (age > 10) {
        break // for of 允许中断
    }
    console.log(age)
}
// 打印
// 1
// 5

优点

  1. for of 有与for in 一样的简洁语法(这也是两者容易混乱的点),但没有for in的缺点
  2. for of 保证顺序且不会仅遍历当前对象
  3. for of 可与break,continue,return配合

AI前端JS规范

变量

命名方式:小驼峰

命名规范:前缀名词

// bad
let setCount = 10

// good
let maxCount = 10

常量

命名方式:全部大写

命名规范:多个单词时使用分隔符_

// bad
const serverErrorCode = {
    success: 200,
    repeat: 444
}

// good
const SERVER_ERROR_CODE = {
    SUCCESS: 200,
    REPEAT: 444
}

函数

命名方式:小驼峰

命名规范:前缀动词

// bad
function wordClass() {}

// good
function saveWordClass() {}

常用动词:can、has、is、load、get、set

命名方式:大驼峰

命名规范:前缀名词

// bad
class person {}

// good
class Person {}

注释

单行

// 单行注释,注意前面的空格
let maxCount = 123

多行

/**
 * 多行注释
 * /
 

减少嵌套

确定条件不允许时,尽早返回。经典使用场景:校验数据

// bad
if (condition1) {
    if (condition2) {
        ...
    }
}

// good
if (!condition1) return
if (!condition2) return
...

减少特定标记值

使用常量进行自解释

// bad
type: 1 // 1代表新增  2代表修改

// good
const MODIFY_TYPE = {
    ADD: 1,
    EDIT: 2
}

type: MODIFY_TYPE.ADD

表达式

尽可能简洁表达式

// bad
if (name === ''){}
if (collection.length > 0){}
if (notTrue === false){}

// good
if (!name) {}
if (collection.length){}
if (notTrue){}

分支较多处理

对于相同变量或表达式的多值条件,用switch代替if

// bad
let type = typeof variable
if (type === 'object') {
    // ......
}
else if (type === 'number' || type === 'boolean' || type === 'string') {
    // ......
}

// good
switch (typeof variable) {
    case 'object':
        // ......
        break
    case 'number':
    case 'boolean':
    case 'string':
        // ......
        break
}

使用变量名自解释 V1.1

逻辑复杂时,建议使用变量名自解释,而不是晦涩难懂的简写。

// bad
function(value) {
    return !helpers.req(value) || this.entity.entVocabularyEntries.filter(item => item.vocabularyEntryName === value).length < 2
}

// good
function(value) {
    let entVocabularyList = this.entity.entVocabularyEntries
    let repeatCount = entVocabularyList.filter(item => item.vocabularyEntryName === value).length
    return !helpers.req(value) || repeatCount < 2
}

使用函数名自解释 V1.1

遵循单一职责的基础上,可以把逻辑隐藏在函数中,同时使用准确的函数名自解释。

// bad
if (modifyType === MODIFY_TYPE.ADD) {
    batchVariableAPI(data).then(() => {
        this.closeModal()
        this.$toast.show('添加变量成功')
    })
} else {
  updateVariableAPI(data).then(() => {
        this.closeModal()
        this.$toast.show('修改变量成功')
    })
}

// good
modifyType === MODIFY_TYPE.ADD  this._insertVariable(data) : this._updateVariable(data)

_insertVariable() {
    batchVariableAPI(data).then(() => this._successOperation('添加变量成功'))
}

_updateVariable() {
    updateVariableAPI(data).then(() => this._successOperation('修改变量成功'))
}

_successOperation(toastMsg) {
    this.closeModal()
    this.$toast.show(toastMsg)
}

其他规范

使用prettier格式化工具以及eslint校验

  • 格式自动化
  • 4个缩进
  • 全部单引号
  • 方法if / else / for / while / function / switch / do / try / catch / finally 关键字后有一个空格
  • 自动省略分号

.prettierrc配置:

module.exports = {
    printWidth: 100, // 设置prettier单行输出(不折行)的(最大)长度

    tabWidth: 4, // 设置工具每一个水平缩进的空格数

    useTabs: false, // 使用tab(制表位)缩进而非空格

    semi: false, // 在语句末尾添加分号

    singleQuote: true, // 使用单引号而非双引号

    trailingComma: 'none', // 在任何可能的多行中输入尾逗号

    bracketSpacing: true, // 在对象字面量声明所使用的的花括号后({)和前(})输出空格

    arrowParens: 'avoid', // 为单行箭头函数的参数添加圆括号,参数个数为1时可以省略圆括号

    // parser: 'babylon', // 指定使用哪一种解析器

    jsxBracketSameLine: true, // 在多行JSX元素最后一行的末尾添加 > 而使 > 单独一行(不适用于自闭和元素)

    rangeStart: 0, // 只格式化某个文件的一部分

    rangeEnd: Infinity, // 只格式化某个文件的一部分

    filepath: 'none', // 指定文件的输入路径,这将被用于解析器参照

    requirePragma: false, // (v1.7.0+) Prettier可以严格按照按照文件顶部的一些特殊的注释格式化代码,这些注释称为“require pragma”(必须杂注)

    insertPragma: false, //  (v1.8.0+) Prettier可以在文件的顶部插入一个 @format的特殊注释,以表明改文件已经被Prettier格式化过了。

    proseWrap: 'preserve' // (v1.8.2+)
}

.eslintrc.js规则:

module.exports = {
    root: true,
    env: {
        browser: true,
        node: true
    },
    extends: ['plugin:vue/essential'],
    parserOptions: {
        parser: 'babel-eslint'
    },
    plugins: ['vue'],
    // add your custom rules here
    rules: {
        'arrow-parens': 0, // allow paren-less arrow functions

        'generator-star-spacing': 0, // allow async-await

        'no-unused-vars': 'error', // disabled no ununsed var  `V1.1`

        'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', // no use debugger in production

        'indent': [2, 4, { SwitchCase: 1 }], // 4 space for tab for perttier

        'space-before-function-paren': ['error', 'never'], // no space in function name for perttier
    }
}

如果是vue-cli3项目,以上配置的eslint插件默认已安装;如果不是vue-cli3项目,需要npm安装对应包:npm install --save-dev babel-eslint eslint-plugin-vue

参考链接

百度JS规范

npm script技巧

npm不仅是js包管理工具,还可以为作为代码库配置工具。有些时候需要一些小脚本来约定规则或者监听文件变化,这时候npm script起到重要作用。

1. 串行和并行

使用&&将多个命令串行执行。比如我们经常提交代码时,先perriter格式化代码,然后检查eslint以及stylelint,最后再进行commitlint。依次执行,前面执行为false则停止。使用&将多个命令并行执行。

"scripts": {
    "precommit": "npm run format && npm run eslint && npm run stylelint && git add ."
}

2. 通配符执行相似指令

通配符需要配合npm-run-all包(更轻量和简洁的多命令运行)。--parallel参数表示并行

"scripts": {
    "precommit": "npm-run-all --parallel lint:*",
    "lint:js": "eslint --ext .js,.vue --ignore-path .gitignore",
    "lint:commit": "commitlint -e $GIT_PARAMS","
}

3. 原生钩子

npm脚本有pre和post两个钩子。eg:build脚本命令的钩子就是prebuild和postbuild。

"scripts": {
    "build": "webpack",
    "prebuild": "echo before build",
    "postbuild": "echo after build"
}

执行build时按照如下顺序执行:

npm run prebuild && npm run build && npm run postbuild

npm 默认提供如下命令钩子:

  • install
  • uninstall
  • start
  • restart
  • build
  • test
  • stop
  • version

4. 监听文件变动

gulp中watch非常实用,但npm script也能实现文件变动后自动运行npm脚本。这就需要安装onchange包。onchange帮助我们在文件增删改时执行对应npm命令,非常实用。

安装onchange:

npm install onchange --save-dev

scripts监听(示例监听svg文件变化,以处理最新svg文件):

"scripts": {
    "dev": "webpack & npm run watch:svg",
    "watch:svg": "onchange 'assets/svg/*.svg' -- npm run svg",
    "svg": "vsvg -s ./assets/svg -t ./assets/icon",
}

5. git钩子

这也是非常实用功能之一,可以利用git钩子构建代码约束。经常用到的工具包是husky,通过husky源码知道,它替换了项目中.git/hooks钩子。项目中常用钩子是precommit,prepush, commit-msg

安装husky:

npm install husky --save-dev

约束:

"scripts": {
    "precommit": "npm run format && npm run eslint"
}

官网脚手架思考与实践

一个项目从0到1过程中,首先一个难点是完成项目框架搭建,每次新建项目花大部分时间在配置上。所以可以在这方面做一些有益团队的工作。本文从官网项目角度思考,借助vue-cli工具,整理实践出本脚手架,代码见这里

思考内容:

  • 官网技术选型

  • 项目目录结构

  • 代码规范约束

  • 官网组件库思考

1. 官网技术选型

笔者部门前端主要使用Vue技术栈,本脚手架选型时依然选择Vue作为基础。考虑到官网需要在搜索引擎中提高曝光率,所以SEO特性必须添加。本脚手架使用Vue官方推荐的SSR集成解决方案Nuxt作为项目基础。使用本脚手架开发,需要提前学习Nuxt相关知识。

2. 目录结构

做一个好的脚手架,需要思考项目结构合理性。好的结构让开发者迅速定位,并且代码分明别类,干净整洁。这里基于Nuxt架构的layout,pages,store,plugins结构,加入scss,img,svg,components,api结构(存放结构不一定在同一级),丰富官网必备内容。

  • layout/pages/store/plugins: nuxt框架结构,十分好用
  • scss: 设置css格局。通过sass-resources-loader更方便操作scss的变量和mixin,所有页面默认引入,不需要额外@import
  • img/svg: 官网有大量本地图片,有png,gif,jpg以及特殊的svg,有些图片有点击事件,有些站外链接,有些站内链接。这里定义他们的存放位置,一个原因使得结构清晰,更重要的是能将所有本地图片合成一个icon组件,方便统一调用。
  • components: 基础组件和页面组件,都是轻量级文件。u-link/u-icon等做了重点优化,具体看下章节。
  • api: 官网相对请求较少,但基本都会有接口调用,故统一放api.js中。本脚手架也使用async/await语法糖,帮助解决‘回调地狱’。
|-- official-website-template
    |-- nuxt.config.js                   -- nuxt配置
    |-- package.json                     -- 项目依赖以及npm脚本
    |-- assets
    |   |-- css                          -- scss
    |   |   |-- global.scss              -- 全局css
    |   |   |-- reset.scss               -- reset css
    |   |   |-- variables.scss           -- 全局scss变量或mixin
    |   |-- img                          -- 图片存放,icon组件会默认找到该文件夹
    |   |-- svg                          -- svg存放,icon组件会默认找到该文件夹
    |-- components                       -- 组件
    |   |-- u-footer.vue                 -- 页尾布局占位
    |   |-- u-header.vue                 -- 页头导航占位
    |   |-- common                       -- 全局Vue组件,该文件夹下的组件自动导入,文件名为组件名
    |   |   |-- u-banner.vue             -- 轮播组件
    |   |   |-- u-button.vue
    |   |   |-- u-icon.vue               -- 提供快捷本地图片访问
    |   |   |-- u-input.vue
    |   |   |-- u-link.vue               -- 统一站内/站外导航组件
    |   |   |-- u-modal.vue
    |   |   |-- u-section.vue
    |   |   |-- u-select-option.vue
    |   |   |-- u-select.vue
    |   |   |-- u-tab.vue
    |   |   |-- u-tabs.vue
    |   |-- index                        -- 页面逻辑组件
    |-- layouts                          -- Nuxt结构,全局模板
    |   |-- default.vue
    |-- pages                            -- Nuxt结构,页面路由route
    |   |-- index.vue
    |-- plugins                          -- Nuxt结构,插件
    |   |-- third.js                     -- 第三方插件导入
    |   |-- vue-global.js                -- vue全局导入
    |-- static                           -- Nuxt结构,静态文件
    |   |-- favicon.ico
    |-- store                            -- Nuxt结构,Vuex
    |   |-- index.js
    |-- utils                            -- 工具库
        |-- api.js                       -- api层
        |-- http.js                      -- 基础http请求

3. 代码规范约束

这里包括开发规范配置以及提交规范配置。统一的团队代码规范十分重要,可以使得大家代码都一致,同时减少出错。通过一些工具,将代码规范整合在本脚手架中。详细内容可以看笔者相关文章eslint + stylelint + prettier + husky团队规范

  • prettier: 对所有代码统一格式化,使得代码看上去干净整洁。
  • eslintstylelint: 对js及css做规则约束,防止出现语法方面错误。
  • husky: 对提交的代码验证,不通过则不允许提交到远程仓库,保证了git仓库的整洁。

4. 官网组件库思考

每个项目都需要用到组件库,特别是后台管理类系统,一个好的组件库能让效率提高很多。但目前市场上的大部分组件库,设计的时候就主打大而全,这就造成改动其中的逻辑或样式十分困难。笔者从官网业务角度思考,常用的组件库就link,button,icon,input,select等几个组件,而且不同官网项目,样式差别较大,样式修改不可避免。所以本脚手架封装常用的组件,都是轻量级单文件,修改十分方便。

u-link

SPA应用有站内链接(router-link)和站外链接(a),该组件针对此进行统一。同时该组件也是button,icon组件的基础

<u-link to="/demo">站内链接</u-link>
<u-link href="https://www.baidu.com" target="_blank">站外链接</u-link>

u-button

官网中最常用组件之一。该组件除常规支持颜色大小设置,应该也支持link导航功能。

<u-button size='s' color='primary' @click="test">Button组件</u-button>
<u-button size='s' color='primary' href="https://www.baidu.com" target="_blank">Button组件</u-button>

u-icon

在官网中,经常会使用到视觉给出的图片或者线上图片地址。图片种类也很丰富,有svg,png,gif等,另外图片有站内链接,站外链接或不链接。所以很有必要对图片做统一处理。

<u-icon name="close" scale="4" href="https://www.baidu.com" />
<u-icon src="test.png" />
<u-icon src="https://www.baidu.com/pics/1" />

u-input/u-select/u-modal/u-tab/...

常用组件,都是轻量级,略。

Array API与V8源码解析

在阅读You-Dont-Need-Lodash-Underscore源码时,发现很多关于array数组方法的技巧,由此了解之前对array数组方法有很多细节点没有深入应用。故重新翻开V8 array源码,记录下一些array高级API以及其源码。

array.reduce(callback[, initialValue])

对每项执行reducer函数,返回迭代结果。

  • callback(accumulator, currentValue, currentIndex, array)
  • initialValue
    • 如果有设置,则作为初始值循环
    • 如果没有设置,则使用array第一项作为初始值
// 无初始值
[1, 2, 3].reduce((pre, cur, index) => {
    l(pre,cur)
    return pre + cur
})
// first loop: 1 2
// second loop: 3 3

// 有初始值
[1, 2, 3].reduce((pre, cur, index) => {
    l(pre,cur)
    return pre + cur
}, 0)
// first loop: 0 1
// second loop: 1 2
// third loop: 3 3

源码在1223行

function ArrayReduce(callback, current) {
  CHECK_OBJECT_COERCIBLE(this, "Array.prototype.reduce");

  // Pull out the length so that modifications to the length in the
  // loop will not affect the looping and side effects are visible.
  var array = TO_OBJECT(this);
  var length = TO_LENGTH(array.length);
  return InnerArrayReduce(callback, current, array, length,
                          arguments.length);
}

function InnerArrayReduce(callback, current, array, length, argumentsLength) {
  if (!IS_CALLABLE(callback)) {
    throw %make_type_error(kCalledNonCallable, callback);
  }

  var i = 0;
  // 当只有callback参数时,设置current为第一个参数
  find_initial: if (argumentsLength < 2) {
    for (; i < length; i++) {
      if (i in array) {
        current = array[i++]; // 修改current值
        break find_initial;
      }
    }
    throw %make_type_error(kReduceNoInitial);
  }

  // callback迭代
  for (; i < length; i++) {
    if (i in array) {
      var element = array[i];
      // 每次return返回的值作为current值
      current = callback(current, element, i, array);
    }
  }
  return current;
}

array.slice([begin[, end]])

复制数组。返回数组从下标begin到end(不包含end)的新数组,原数组不变

  • begin
    • 如果begin省略,则从0开始
    • 如果begin超过数组长度,则直接返回[]空数组
    • begin为负数,一般等同于begin+arr.length
  • end
    • 如果end省略,则为arr.length
    • 如果end超过数组长度,则为arr.length
    • end为负数,一般等同于end+arr.length
// base
var arr = [1, 2, 3, 4, 5]

console.log(arr.slice()) // [1, 2, 3, 4, 5]
console.log(arr.slice(2)) // [3, 4, 5]
console.log(arr.slice(-2, 4)) // [4] 等于 arr.slice(-2+5, 4)
console.log(arr.slice(-2)) // [4, 5] 等于 arr.slice(-2+5, +5)

V8源码在587行:

function ArraySlice(start, end) {
  CHECK_OBJECT_COERCIBLE(this, "Array.prototype.slice");

  var array = TO_OBJECT(this);
  var len = TO_LENGTH(array.length);
  var start_i = TO_INTEGER(start); // 默认是0
  var end_i = len; // 默认是array.length

// 定义end则赋值
  if (!IS_UNDEFINED(end)) end_i = TO_INTEGER(end);

// 处理start为负数或大于数组长度处理
  if (start_i < 0) {
    start_i += len;
    if (start_i < 0) start_i = 0;
  } else {
    if (start_i > len) start_i = len;
  }

// 处理end为负数或大于数组长度
  if (end_i < 0) {
    end_i += len;
    if (end_i < 0) end_i = 0;
  } else {
    if (end_i > len) end_i = len;
  }

// 创建指定长度的array数组
  var result = ArraySpeciesCreate(array, MaxSimple(end_i - start_i, 0));

// start超过end,直接返回[]
  if (end_i < start_i) return result;

  if (UseSparseVariant(array, len, IS_ARRAY(array), end_i - start_i)) {
    // 应对array的变种时,进行处理
    %NormalizeElements(array);
    if (IS_ARRAY(result)) %NormalizeElements(result);
    SparseSlice(array, start_i, end_i - start_i, len, result);
  } else {
    SimpleSlice(array, start_i, end_i - start_i, len, result);
  }

  result.length = end_i - start_i;

  return result;
}

以上源码也给我们解释了[].slice.call(object)返回对应数组。

let arrayLike = {
 '0':'a',
 '1':'b',
 '2':'c',
 length: 3 // 长度不符合则超出的长度对象都是undefined
}
console.log([].slice.call(arrayLike)) //[a,b,c]

array.splice(start[, deleteCount[, item1[, item2[, ...]]]])

增加/删除数组。通过add/remove改变最终数组

  • start
    • 当大于数组长度时,则认为是array.length
  • deleteCount
    • 当这个值省略或者大于array.length - start时,start后面的都会删除
var arr = [1, 2, 3, 4, 5]

// splice会改变原数组,以下注视都是单独console.log
arr.splice(1, 0, 10); // [ 1, 10, 2, 3, 4, 5 ]
arr.splice(1, 1, 10); // [ 1, 10, 3, 4, 5 ]
arr.splice(100, 1, 10); // [ 1, 2, 3, 4, 5, 10 ]
arr.splice(1) // [ 1 ]
arr.splice(1, 100) // [ 1 ]
arr.splice(1, 100, ['new1', 'new2']) // [ 1, [ 'new1', 'new2' ] ]
arr.splice(1, 100, 'new1', 'new2') // [ 1, 'new1', 'new2' ]
function ArraySplice(start, delete_count) {
  CHECK_OBJECT_COERCIBLE(this, "Array.prototype.splice");

  var num_arguments = arguments.length;
  var array = TO_OBJECT(this);
  var len = TO_LENGTH(array.length);
  // start处理,超过长度取array.length
  var start_i = ComputeSpliceStartIndex(TO_INTEGER(start), len);
  // deleteCount处理
  var del_count = ComputeSpliceDeleteCount(delete_count, num_arguments, len,
                                           start_i);
  var deleted_elements = ArraySpeciesCreate(array, del_count);
  deleted_elements.length = del_count;
  var num_elements_to_add = num_arguments > 2 ? num_arguments - 2 : 0;

  if (del_count != num_elements_to_add && %object_is_sealed(array)) {
    throw %make_type_error(kArrayFunctionsOnSealed);
  } else if (del_count > 0 && %object_is_frozen(array)) {
    throw %make_type_error(kArrayFunctionsOnFrozen);
  }

  var changed_elements = del_count;
  if (num_elements_to_add != del_count) {
    // If the slice needs to do a actually move elements after the insertion
    // point, then include those in the estimate of changed elements.
    changed_elements += len - start_i - del_count;
  }

  // 删除先slice,再move
  if (UseSparseVariant(array, len, IS_ARRAY(array), changed_elements)) {
    %NormalizeElements(array);
    if (IS_ARRAY(deleted_elements)) %NormalizeElements(deleted_elements);
    SparseSlice(array, start_i, del_count, len, deleted_elements);
    SparseMove(array, start_i, del_count, len, num_elements_to_add);
  } else {
    SimpleSlice(array, start_i, del_count, len, deleted_elements);
    SimpleMove(array, start_i, del_count, len, num_elements_to_add);
  }

  // 如果有第三、四...数据,从start处开始插入数据
  // Insert the arguments into the resulting array in
  // place of the deleted elements.
  var i = start_i;
  var arguments_index = 2;
  var arguments_length = arguments.length;
  while (arguments_index < arguments_length) {
    array[i++] = arguments[arguments_index++];
  }
  array.length = len - del_count + num_elements_to_add;

  // Return the deleted elements.
  return deleted_elements;
}

Vue JSX插件依赖及语法实践

Vue JSX插件依赖及语法实践

文章内容主要分两块。

第一块是了解jsx运行环境,因为jsx只是语法糖,最终都是需要babel来转译语法,所以需要配置相关babel插件。vue-cli3脚手架工具生成的应用工程默认支持jsx/tsx,省去了自己配置的繁琐,但了解相关babel插件对理解和书写jsx非常有帮助。

第二块是实践jsx在vue中的语法以及相关案例。了解jsx是如何生成最终的VNode。tsx应用Demo代码放在github vue-tsx-demo

1. 环境基础babel

vue-cli3自动生成的app项目中,babel.config.js预设了
presets: ["@vue/app"],该插件为babel-preset-app

里面包含插件,主要是babel解析,以支持许多扩展语法。比如jsx、es6语法等:

"@babel/core": "^7.9.0",
"@babel/helper-compilation-targets": "^7.8.7",
"@babel/helper-module-imports": "^7.8.3",
"@babel/plugin-proposal-class-properties": "^7.8.3",
"@babel/plugin-proposal-decorators": "^7.8.3",
"@babel/plugin-syntax-dynamic-import": "^7.8.3",
"@babel/plugin-syntax-jsx": "^7.8.3",
"@babel/plugin-transform-runtime": "^7.9.0",
"@babel/preset-env": "^7.9.0",
"@babel/runtime": "^7.9.2",
"@vue/babel-preset-jsx": "^1.1.2",
"babel-plugin-dynamic-import-node": "^2.3.0",

里面重点插件有:

1.1 Babel JSX插件集合

1.2 Babel预设插件集合

  • @babel/preset-env babel智能预设
    • 集成了browserslist,该插件包含其他核心插件:
    • 众多ES6 Stage语法支持,比如:
    • 参数targets:为项目支持/目标的环境
      • 默认转换所有ECMAScript 2015+代码
      • chrome,opera,edge,firefox,safari,ie,ios,android 确定最低版本要求
      • node
      • electron
    • 参数targets.esmodules: 定位为支持ES模块的浏览器(直接支持ES6模块语法,浏览器能自己解析import语法,可显著减少包体积)。注意:指定esmodules目标时,浏览器目标将被忽略
    • 参数modules:启用将ES6模块语法转换为其他模块类型的功能。"amd" | "umd" | "systemjs" | "commonjs" | "cjs" | "auto" | false,默认为"auto"。设置为false不会转换模块
    • 参数targets.browsers: 同上targets作用
    • 参数target.node: 同上targets作用

1.3 其他

  • @babel/core: 根据本地配置的config文件(babel7考虑到monorepos项目存在,所以采用了babel.config.json配置文件,以作用全局),把代码转换。(所有babel插件必备前置包)
  • @babel/cli: 客户端执行,依赖上面@babel/core。

2. JSX在Vue中语法

react和vue底层vnode diff对比不是使用相同的数据结构,所以导致两者jsx书写方式有些许不同。目前两者大部分jsx语法一致,是因为有各种babel插件辅助做了这部分事。但对于动态属性这样自由化较高的地方,需要我们知道两者本质区别(即不同的VNode数据结构)。

vue template模板本质上最终生成render函数,而render函数本质上是生成VNode,所以有必要了解这个VNode数据结构。Vue2.x VNode diff核心算法借鉴的是snabbdom库,所以数据结构也有snabbdom数据结构的影子,比如事件需要放置在on属性上,方便最终patch时挂载到真实dom元素上(react则自定义模拟事件系统,所有事件都冒泡到顶层document处理)。更多VNode信息可查看Vue官方文档 - 深入数据对象

以下查看jsx转译为h函数:

render (h) {
  return (
    <div
      // Component props
      propsMsg="hi"
      // Normal attributes or component props.
      id="foo"
      // DOM properties are prefixed with `domProps`
      domPropsInnerHTML="bar"
      // event listeners are prefixed with `on` or `nativeOn`
      onClick={this.clickHandler}
      nativeOnClick={this.nativeClickHandler}
      // other special top-level properties
      class={{ foo: true, bar: false }}
      style={{ color: 'red', fontSize: '14px' }}
      key="key"
      ref="ref"
      // assign the `ref` is used on elements/components with v-for
      refInFor
      slot="slot">
    </div>
  )
}

以上jsx语法糖等同于如何h函数生成VNode:

render (h) {
  return h('div', {
    // Component props
    props: {
      msg: 'hi'
    },
    // Normal HTML attributes
    attrs: {
      id: 'foo'
    },
    // DOM props
    domProps: {
      innerHTML: 'bar'
    },
    // Event handlers are nested under "on", though
    // modifiers such as in v-on:keyup.enter are not
    // supported. You'll have to manually check the
    // keyCode in the handler instead.
    on: {
      click: this.clickHandler
    },
    // For components only. Allows you to listen to
    // native events, rather than events emitted from
    // the component using vm.$emit.
    nativeOn: {
      click: this.nativeClickHandler
    },
    // Class is a special module, same API as `v-bind:class`
    class: {
      foo: true,
      bar: false
    },
    // Style is also same as `v-bind:style`
    style: {
      color: 'red',
      fontSize: '14px'
    },
    // Other special top-level properties
    key: 'key',
    ref: 'ref',
    // Assign the `ref` is used on elements/components with v-for
    refInFor: true,
    slot: 'slot'
  })
}

使用jsx代替vue需要解决的系列问题:

  1. 与vue兼容,能被识别
  2. 支持使用v-model v-on(babel插件解决,见以上)
  3. 支持使用vue watch/computed/methods
  4. 支持使用created/mounted等生命周期
  5. 支持css module
  6. 支持动态props传值问题
  7. 支持插槽
  8. 支持Vue原型链上问题,如$xxx、ref等功能。

2.1 基础语法

bable jsx插件会通过正则匹配的方式在编译阶段将书写在组件上属性进行“分类”。 onXXX的均被认为是事件,nativeOnXXX是原生事件,domPropsXXX是Dom属性。

class,staticClass,style,key,ref,refInFor,slot,scopedSlots这些被认为是顶级属性,至于我们属性声明的props,以及html属性attrs,不需要加前缀,插件会将其统一分类到attrs属性下,然后在运行阶段根据是否在props声明来决定属性归属(即属于props还是attrs)

export default {
  name: "button-counter",
  props: ["count"],
  methods: {
    onClick() {
      this.$emit("change", this.count + 1);
    }
  },
  render() {
    return (
      <button onClick={this.onClick}>You clicked me {this.count} times.</button>
    );
  }
};

2.2 动态属性

在React中所有属性都是顶级属性,直接使用{...props}就可以了,但是在Vue中,你需要明确该属性所属的分类,如一个动态属性value和事件change,你可以使用如下方式(延展属性)传递:

const dynamicProps = {
  props: {},
  on: {},
}
if(haValue) dynamicProps.props.value = value
if(hasChange) dynamicProps.on.change = onChange
<Dynamic {...dynamicProps} />

尽量使用明确分类的方式传递属性,而不是要babel插件帮你分类及合并属性。

2.3 指令

常用的v-if和v-for,使用js语法中的if/for语句就能实现了。v-model属于prop + input事件语法糖,也可以使用babel插件自动实现。

// v-show,同理v-if
render(){
       return (
         <div>
           {this.show?'你帅':'你丑'}
         </div>
       )
     }

// v-for
render(){
        return (
          <div>
            {this.list.map((v)=>{
              return <p>{v}</p>
            })}
          </div>
        )
      }

// v-model: 传值和监听事件改变值(babel插件已支持)
data(){
        return{
          text:'',
        }
      },
      methods:{
        input(e){
          this.text=e.target.value
        }
      },
      render(){
        return (
          <div>
            <input type="text" value={this.text} onInput={this.input}/>
            <p>{this.text}</p>
          </div>
        )
      }

2.4 slot插槽

slot直接通过this.$slots对象拿到,scopedSlot通过this.$scopedSlots对象拿到($scopedSlots每项是待调用函数)。

export default {
  render(h) {
    return <div>
        {
          (this.title || this.$slots.header) && (
            <div class="header">
              <span class="title">{this.title}</span>
              {this.$slots.header}
            </div>
          )
        }
        {this.$slots.default}
      </div>
  },
}

2.5 组件

不需要注册,直接使用

import Todo from './Todo.jsx'

export default {
  render(h) {
    return <Todo /> // no need to register Todo via components option
  },
}

2.6 functional函数

export default {
    functional:true,
    render(h,context){
        return (
          <div class="red" {...context.data}>
            {context.props.data}
          </div>
        )
      }
    }

2.7 v-model

安装@vue/babel-sugar-v-model babel插件后即可自动解析v-model,官方更推荐使用vModel或者value + onInput事件

<el-input vModel_trim={inputValue}/>
// 或者使用
<el-input 
 value={this.inputValue}
 onInput={val => this.inputValue = val.trim()}/>

参考文章

ES6-新增特性一览

ecma-263是ES6规格的官网文档,该文档是英文版。如果英文稍差的同学,推荐@阮一峰老师的ECMAScript 6 入门

以下默认陈述的是ES6标准,部分标注ES7、ES8标准是为了表明其最终发布时间(严谨)。其实大部分在2015年6月(ES6发布时间)都进入了草案阶段(Stage 2),故在babel等转译工具下,都可以使用这些特性在前端工程项目中。

1. let/const取代var

2. 字符串模板

3. 对象解构

  • Destructuring
  • enhanced object literals({foo} === {foo:foo})

4. 新数据类型Symbol

5. 新数据结构Map/Set/WeakMap/WeakSet

6. Proxy、Reflect

7. 扩展

  • Array
    • Array.from()
    • Array.of()
    • Array.copyWithin()
    • Array.find()
    • Array.findIndex()
    • Array.fill()
    • Array.includes()ES7
  • Object
    • Object.keys()
    • Object.values()ES8
    • Object.entries()ES8
    • Object.assign()
    • Object. is()
  • Function
  • Number
    • Number.isNuN()
    • Number.isFinite()
    • Number.parseInt()
    • Number.parseFloat()
    • Number.isInteger()
    • Number.isSafeInteger()
  • Math
    • Math.max(x, y)
    • Math.trunc(x)
    • Math.sign(x)
    • Math.acosh(x)
    • Math.asinh(x)
    • Math.atanh(x);
    • Math.cbrt(x)
    • Math.clz32(x)
    • Math.cosh(x)
    • Math.expm1(x)
    • Math.fround(x)
    • Math.hypot(...values)
    • Math.imul(x, y)
    • Math.log1p(x)
    • Math.log10(x)
    • Math.log2(x)
    • Math.tanh(x)

8. 异步

  • Promise
    • Promise.prototype.then
    • Promise.prototype.catch
    • Promise.prototype.finallyES9
    • Promise.all()
    • Promise.rece()
  • Iterator
    • Iterator接口
    • for of
  • Generator
    • yield*
  • async/awaitES8

9. Class类

  • class
  • extends
  • decoratorES7

10. Module

  • import
  • export

参考文档

mongodb指南

MongoDB是目前最流行的NoSQL数据库之一。MongoDB和Node.js特别般配,因为MongoDB是基于文档的非关系型数据库,文档是按BSON(JSON的轻量化二进制格式)存储的,增删改查等管理数据库的命令和JavaScript语法很像。

概念解析

在mongodb中基本的概念是数据库、集合、文档。下表将帮助更容易理解Mongodb中的一些概念:

SQL概念 MongoDB概念 解释
database database 数据库。一致
table collection 数据库表
row document 数据库表一行
column field 数据库表一列
primary key primary key 主键。mongodb默认有主键_id
table joins 表连接。mongodb不支持

Command

  • 全局
    • mongo 进入db命令
    • show dbs 显示所有可用dbs
    • db 显示当前db
  • 数据库
    • use [dbName] 使用或新建数据库
    • db.dropDatabase() 删除当前数据库
    • show collections 显示当前数据库所有集合(表)
    • db.collections.drop() 删除当前数据库所有集合(表)
  • 增删改查
      • db.[collectionName].insert(object) 数据库插入一行数据
      • db.[collectionName].insertOne(object)3.2+ 数据库插入一行数据
      • db.[collectionName].insertMany(object)3.2+ 数据库插入多行数据
      • db.[collectionName].save(object) 指定_id=update(),否则是insert()
    • 更新 db.[collectionName].update(query,update)
    db.col.update({'title':'MongoDB 教程'},{$set:{'title':'MongoDB'}})
    • 删除 db.[collectionName].remove(query,justOne)
    • 查询 db.[collectionName].find(query, projection)
  • 条件
    • limit(Number)
    • skip(Number)
    • sort() 可以通过参数指定排序的字段,并使用 1 和 -1 来指定排序的方式
db.col.find({}).limit(5).skip(1).sort({"likes":-1})

在mongodb中,Collection类似于传统SQL的table;Document类似于SQL中的一行记录row;Field类似于SQL中的一列column。

Mongoose

Mongoose是在node下对MongoDB进行管理的数据对象模型(ODM)库。它管理着数据结构定义、校验、数据之间的关系,并可以使node数据转换成mongodb数据库数据。

Schema

定义document的结构、默认值、校验等。是一种以文件形式存储的数据库模型骨架,不具备数据库的操作能力。支持schema.pre('save', ...)钩子函数(当model.save()执行时触发);支持schema.plugin()以使用自定义插件。

Schema Type支持如下类型:

  • Boolean
  • Buffer
  • Date
  • Mixed (A generic / flexible data type)
  • Number
  • ObjectId
  • String

Schema API

  • Schema.statics.[Statics-Methods] Model中添加静态方法,this指向Model
  • Schema.methods.[Methods] Model.prototype中添加方法
  • Schema.pre('save/init/remove/validate') 回调钩子。this指向Schema
  • Schema.plugin(self-plugin) 应用插件

Model

Model是Schema的包装,具有操作数据库的能力

  • Model:由Schema发布生成的模型,具有抽象属性和行为的数据库操作。
  • Model.prototyp(这里可以叫Entity)e: 由Model创建的实体,他的操作也会影响* 数据库。
  • 关系:Schema生成Model,Model创造Entity,Model和Entity都可对数据库操作造成影响,但Model比Entity更具操作性。

Model API

  • Model
      • Model.create(documents, callback)
      • Model.insertMany()
      • Model.find(conditions, [fields], [options], [callback])
      • Model.findById(id, [fields], [options], [callback])
      • Model.findOne(conditions, [fields], [options], [callback])
      • Model.count(conditions, [callback])
      • Model.findByIdAndUpdate(id, document, callback)
      • Model.findOneAndUpdate([conditions], [update], [options], [callback])
      • Model.remove(conditions, [callback])
      • Model.findByIdAndRemove(id, [options], [callback])
      • Model.findOneAndRemove(conditions, [options], [callback])
  • Model.prototype
    • Model.prototype.save()(callback)
    • Model.prototype.update(query, document, callback)
    • Model.prototype.remove()

Schema类似于SQL的表的定义;Model是一个高层次的接口

let mongoose = require('mongoose')
mongoose.connect('mongodb://localhost:27017/test') // 记得先连接到数据库

/** 定义表结构 **/
let emailSchema = new mongoose.Schema({
  email: String,
  date: Date
})
// 回调钩子
emailSchema.pre('save',  function(next) {
    if(!this.date) this.date = new Date() // this指向Schema
    next()
})
// 定义Model静态方法
emailSchema.statics.getLeoRows = function() {
    return this.find({author: 'leo'}) // this指向Model
}

/** 定义操作层 **/
let EmailModel = mongoose.model('Email', emailSchema)

// 静态方法,常用于数据库逻辑
EmailModel.getLeoRows().then(data => console.log(data))

// 新增
let msg = new EmailModel({
  email: 'leo'
})
msg.save().then(doc => console.log(doc))

// 查询
EmailModel.find({ email: 'leo' }).then(doc => console.log(doc))

// 更新
EmailModel.findOneAndUpdate({ email: 'leo' },{ email: 'leoupdate' }.then(doc => console.log(doc))

// 删除
EmailModel.findOneAndRemove({ email: 'leoupdate'}).then(doc => console.log(doc))

参考文章

AI前端Git规范

Git分支命名

  • master:主分支,负责记录上线版本的迭代,该分支代码与线上代码是完全一致的。
  • develop:开发分支,该分支记录相对稳定的版本,所有的feature分支和bugfix分支都从该分支创建。其它分支为短期分支,其完成功能开发之后需要删除
  • feature/*:特性(功能)分支,用于开发新的功能,不同的功能创建不同的功能分支,功能分支开发完成并自测通过之后,需要合并到 develop 分支,之后删除该分支。
  • bugfix/*:bug修复分支,用于修复不紧急的bug,普通bug均需要创建bugfix分支开发,开发完成自测没问题后合并到 develop 分支后,删除该分支。
  • release/*:发布分支,用于代码上线准备,该分支从develop分支创建,创建之后由测试同学发布到测试环境进行测试,测试过程中发现bug需要开发人员在该release分支上进行bug修复,所有bug修复完后,在上线之前,需要合并该release分支到master分支和develop分支。
  • hotfix/*:紧急bug修复分支,该分支只有在紧急情况下使用,从master分支创建,用于紧急修复线上bug,修复完成后,需要合并该分支到master分支以便上线,同时需要再合并到develop分支。

Git Commit Message格式

type : subject

type 指提交类型:

  • type: commit 的类型
  • feature: 新特性
  • fix: 修改问题
  • style: 代码格式修改
  • test: 测试用例修改
  • docs: 文档修改
  • refactor: 代码重构
  • misc: 其他修改, 比如构建流程, 依赖管理

subject 指提交描述

对应内容是commit 目的的简短描述,一般不超过50个字符

推荐文章:

约定式提交

必须知道的 Git 分支开发规范

Git 在团队中的最佳实践--如何正确使用Git Flow

优雅的提交你的 Git Commit Message

谈谈前端天花板问题

问题

相信这个问题,对于许多从事前端开发的人来说,一直是萦绕在心头的事。互联网行业这几年飞速发展,加上前端入门门槛低,会HTML、JS、CSS就属于前端范畴,前端人员大量扩展。

从工程角度而言,现在公司层面项目较多使用Vue、React作为View层库,两者的生态都很好,提供了方便的cli脚手架工具以及其他优秀的配套工具,开箱即用。从工程结果看,一两年的开发人员和3-n年资深前端开发人员差距不大,都能很好的完成项目开发以及准时上线,无非是代码组织以及质量可能不同而已。那如何体现前端人员之间的差距,以及该往哪些方向提升自己的能力?

个人认为,应该从两个方向突破这问题:硬实力软实力

硬实力

硬实力很好理解,就是一步一步的技术成长。作为技术人员,应该多积累一些项目经验以及技术沉淀。推荐读者可以在团队中多做一些技术分享以及codereview,分享受益最大的一定是分享者。另外github是一个很好的学习分享平台,从中有取之不尽的学习资源。

对技术学习方向有迷茫的同学,可以先了解下Javascript发展史。JS 23年(1995年)发展过程中,前18年不怎么受待见,仅仅是在网页中添加一些互动脚本,完全没有工程化的痕迹。主要原因有以下:

  • 性能不佳,执行速度不够快。经过几次浏览器大战,厂商的JS执行引擎执行速度大大提高,最出名的就是google的V8 JS引擎。

  • 语法特性的缺失,缺少Class等Java面向对象特性。ES6的标准给JS语言带来质的提升,优雅的Class、Decorator等Java语言有的,JS也可以有。

  • 缺少模块化,难以复用。ES6模块化、commonjs模块化解决方案带来代码复用,npm这个超级生态更是给所有前端开发一个福音。

  • 缺少优秀的IDE以及配套调试工具,动态语言容易导致无法预料的bug。ESLint、Stylelint等代码检查工具,以及Chrome Developer Tools调试工具,让前端开发更舒适,减少了bug出错率以及调试效率。

  • 应用场景局限在web浏览器中,无法做到跨端跨平台。随着NodeJS的诞生,JS可以应用更多场景:Web前端、后端(NodeJS)、桌面端(ElectronJS、NW)、移动端(React-Native、Weex、Cordova)、嵌入式( Duktape)、机器学习(Tensorflow.js)。大前端

从现在前端规模看,以上问题都很好的解决了。以前只能在其他语言做的桌面端、移动端、机器学习等内容,现在也在大前端的范畴了。所以现在前端工程建立起来了,前后端分离也更加彻底了。同时这也给前端人员提供了更广大的舞台以及更高的要求:前端除传统的HTML、JS、CSS三驾马车外,还需要了解后端服务知识,比如http协议细节、Nginx服务器知识、数据库知识等。总之,前端开发人员,可以从以上维度挑一些感兴趣的方向进行研究,比如V8引擎的设计、ES6语法糖原理、多端应用的工程化:NodeJS、Electron、ReactNative等。

软实力

软实力指团队合作中,作为技术人员与产品、交互、视觉、测试、市场等相关人员的沟通以及协作。

简单理解,就是你可以与任何一个非技术人员讨论相关工作内容。有人会说这是情商,我认为情商只是作为软实力的必备之一。举个例子,有些开发人员总是很烦恼产品或交互在提测快上线时,加新需求或改动需求,然后开始上线前的讨价还价沟通。造成这种局面仅是产品或交互的问题吗?为什么团队在开始阶段就不先确定规则:提测阶段不改需求?前端真的从开始就有好好理解产品需求并适当提供改进意见?交互、视觉、测试等中间职责链有对产品进行思考?一个好的团队知道效率的重要性,有效的沟通会大大增加团队的执行效率。

窥一斑而知全豹,前端人员也是半个产品,半个交互,半个测试,对工程的每个环节有自己的理解和思考。至此,也不能用前端来形容这个角色了。以为软实力就是更好更有效率的把工作完成?error,这只是其中之一,软实力更多的影响你的生活,你的人生,包括你的做事风格,思维方式等。这条路上,我也是个学习者。

总结

每个公司都有相应职级对应不同的技术title,就像一个梯子,硬实力逐级向上。一步一个脚印,踏实积累经验,多分享与学习,达到对应职级所具有的硬实力。相比较于硬实力的明线,软实力是个看不见并难以感觉的暗线。但软实力对整个人生影响巨大,每个人的软实力都是独特的。愿你我都是一个不断成长的人。

《敏捷开发》读后总结

前端开发在软件工程中,占据着很重要的一环。上游对接的是视觉、设计、后端等,下游对应的是用户反馈。更多时候,开发不是单纯的写好代码完成需求就行,写出优雅可读性高的代码只是硬技术之一。还需要在整个软件生产过程中发挥个人软技能,比如如何让团队运作高效,如何让产品可持续优化迭代。毕竟所有人都需要对结果负责,而不是对过程负责。

在软件工程中,你可能已经应用上了该书的一些总结或技巧,只是不自知罢了,笔者在阅读这本书时产生了许多共鸣。以下跟随这本经典图灵奖丛书——《高效程序员的45个习惯-敏捷开发修炼之道》,为敏捷之道提供指引。

态度决定一切

  • 做事
    • 把矛头对准解决问题的办法,而不是人
    • 敏捷团队重成果胜于重过程
  • 欲速则不达
    • 不要坠入快速的简单修复中
    • 深层次的思考是区别优秀程序员和拙劣代码工人的区别
    • 投入时间和精力保持代码整洁、敞亮
  • 对事不对人
    • 开会技巧
      1. 设定最终期限。防止陷入无休止的争辩中。没有最好的方案,只有更合适的方案。设定期限能在为难时做出决断。
      2. 逆向思维。每个人都需要意识到权衡的必要性。尽可能找到优点最多缺点最少的方案,少带个人情感。
      3. 设立仲裁人。确保会议正常进行,打算大篇会议之外的讨论以及假大空式发言。
      4. 支持已经做出的决定。
  • 排除万难,奋勇前进

敏捷编码

  • 代码要清晰地表达意图
    • 开发代码时,更应该注重可读性,而不是图自己方便。
    • 让自己和别人可以读懂一年前的代码,而且只读一遍就知道它的运行机制。
  • 用代码沟通
    • 代码被阅读的次数远远多于被编写的次数。
    • 代码优雅而清晰。比如变量名使用正确、空格使用得当、逻辑分离清晰以及表达式简洁。
    • 良好而有意义的命名方式。
    • 代码自解释。比如:枚举
    • 注释描述代码意图和约束。
  • 动态评估取舍
    • 根据现有资源,对手上问题评估,选出最合适的解决方案。
    • 过早的优化是万恶之源。
  • 增量式编程
    • 经常评估代码质量,并不时进行许多小调整。
    • 留心许多可以改进的微小方面,改善代码可读性。
  • 保持简单
    • 目标:简单、可读性高的代码。
    • 简单的解决方案,也必须满足功能需要。
    • 太简洁不等于简单,那样无法达到沟通的目的。
  • 编写内聚的代码
    • 让类的功能尽量集中,让组件尽量小。
    • 每个类或组件只做一件事,职责需要清晰。
  • 告知,不要询问
    • 查询和修改分离,单个对象做好自己的职责就好。
  • 根据契约进行替换
    • 基于接口,替换代码来扩展系统。
    • 多使用委托而不是继承。

敏捷调试

再好的敏捷项目,都会发生bug、错误等。这时候需要进行敏捷调试。调试时面对的真正问题,是无法用固定的时间来限制。有些项目可能调试一天也没有找到问题。对于一个项目来说,这种没有准确把握时间的消耗是不可接受的。

  • 记录解决问题的日志。将曾经遇到的问题,记录在wiki上进行维护,大家一起维护这份日志,或许从里面会有一些解题思路。注意记录时的关键字。原则上记录问题的时间不能超过解决问题的时间。
  • 警告就是错误。尽量减少错误信息
  • 对问题各个击破。尽量减少耦合,得到单元测试的模块。
  • 报告所有的异常。不要压制异常,及时抛出。
  • 提供有用的错误信息

敏捷协作

团队之间的协作,保持高效率。

  • 定期安排会面时间
    • 立会可以让团队达成共识。
    • 保证会议短小精悍不跑题。
    • 时间尽量在早上,也不要在刚上班。
  • 架构师必须写代码
    • 新系统的设计者必须亲自投入到实现中
  • 实行代码集体所有制
  • 成为指导者
    • 激励别人,让他们更出色,同时提升团队整体实力
    • 解释自己知道的,可以让自己理解更加深入
    • 别人提出问题,可以发现不同视角
    • 帮助团队成员提升水平同时提高自己
    • 不必局限自己团队,也可以是个人blog或一小段代码
    • 成为指导者,意味着分享,而不是固守
  • 允许大家自己想办法
    • 引领大家思考如何解决问题
    • 指出正确方向,而不是直接提出解决方案
  • 准备好后再共享代码
    • 不要提交未完成的代码
  • 做代码复查
    • 代码复查可以得到质量较高且稳定的代码
    • 无论经验丰富多与少,都需要对代码复查
    • 代码复查需要思考,而不是单纯的变量名和代码风格。检查列表:
      • 代码能否被读懂和理解?
      • 是否有明显错误?
      • 代码是否对其他部分有不良影响?
      • 是否存在重复代码?
      • 是否可以改进和重构?
  • 及时通报进展与问题

引入敏捷

  • 管理者指南
    • 让大家知道敏捷开发是让开发人员工作变得轻松
    • 项目周转
      • 立会
      • 孤立对架构师带到项目中,参与日常
      • 开展代码复查
      • 让客户和用户也参与进来
    • 基本环境
      • 版本控制
      • 单元测试
      • 自动构建
    • 敏捷协作
  • 程序员指南
    • 单元测试保证质量
    • 以身作则

reactnative-mac调试技巧

工欲善其事,必先利其器。上篇文章快读搭建并运行一个App应用,但离工程化开发还是远不够的。调试程序是开发过程中必不可少的,高效的调试技能可以提高开发效率,也可以及时发现bug。

启动RN应用时,同时会启一个package server的node应用(可以理解为webpack功能),每次修改js代码,不需要重新run xcode,该server服务会把最新的js代码编译并提供给Native代码调用。

调试技巧清单

  • React Native调试菜单

  • chrome developer tool

  • react-devtools

  • Charles抓包工具

  • 真机调试

React Native调试菜单

Command⌘ + D即可调出调试菜单,里面有我们经常使用的调试功能。

Developer Menu

提示:如果Command⌘ + D无法打开,是因为模拟器与键盘断开连接了,可以在Xcode中Hardware menu->Keyboard->"Connect Hardware Keyboard"

Reload

重新编译App,相当于Web的F5。快捷键Command⌘ + R

Hot Reloading

热加载,做过Webpack工程化项目的人再熟悉不过。如果每次修改代码后需要手动刷新,效率值大大降低,热加载可以在保存代码后,自动进行增量包编译,实现模拟器的自动刷新。

Enable Live Reload

ReactNative还给开发者提供了自动刷新的功能,也可以对代码自动刷新。这里和Hot Reloading的区别是Live Reload是全量刷新,每次保存代码都会自动生成bundle包并发送到手机上,使得应用初始化。

Show Inspector

方便的查看到当前选中元素的位置、样式、层级关系、盒子模型信息等等,类似Chrome Elements Tab

Chrome Developer Tools

强大的Chrome可以像调试js那样来调试React Native应用。方式类似webpack的sourcemap,可以对源代码进行断点调试

第一步:启动远程调试

在Developer Menu下单击"Debug JS Remotely" 启动JS远程调试功能。Chrome自动打开Tab页:“http://localhost:8081/debugger-ui”

第二步:打开开发者工具

快捷键:Command⌘ + Option⌥ + I

第三部:断点调试

选中需要调试的代码行。

image

tips:在控制台(Console)上打印变量,执行脚本等操作。在开发调试中非常有用

react-devtools

rn调试菜单'Show Inspector'虽然可以让开发者可以看到层级和相关样式,但展现在app中过小,也无法像Chrome elements一样,实时修改。这里推荐使用react-devtools工具,让你轻松完成这一切。配合debugger或show inspector更强大

安装

npm install -g react-devtools

使用

react-devtools

image

tips:该工具同样支持ReactJS哦

Charles抓包工具

reactnative的网络请求,在chrome中无法捕捉,所以需要使用抓包工具查看网络请求详细信息。Mac推荐Charles工具

真机调试

IPhone

目前网上大部分真机调试的文章都是基于以前的版本,在笔者最新的版本v0.52已经不再需要代码替换IP地址来得到打包的JSBundle。IOS本机测试需要Apple ID账号,如果需要发布到AppStore,则需要Devloper ID。经过测试,流程如下:

  1. USB连接真机,xcode中设备选择真机

  2. 单击项目名,Target的ProjectName和ProjectNameTest中Signing选项,Team中选择你已经绑定到Apple ID账号。如果没有选择列表,该地方会让你登陆一个账号。

  3. 单击运行即可在真机中查看App运行。晃动手机也可以调出调试菜单

image

Android

  1. 根目录下执行npm start,确保package server启动,localhost:8081可以访问

  2. 使用adb reverse命令。adb reverse tcp:8081 tcp:8081

  3. 晃动设备,打开开发者菜单,点击Dev Settings -> Debug server host for device -> 输入本机IP和8081端口

  4. 完成后晃动设备,选择Reload JS即可

相关链接

React中文网

react-devtools

H5 Video踩坑记录

H5 Video踩坑记录

临时接手一个即将上线的公司项目,纯H5活动页,内容不多,但对还原度和各机型兼容性(尤其是Android机型)有极高要求。涉及的问题很多,这里重点说下在H5中Video的一系列坑。插个技术选型问题,不复杂的活动页建议使用jquery技术栈,而不是使用vue和reactjs等。后者的优点在于组件系统,可复用度高,适合大型项目。活动页一般UI改动频繁,动效多,适合jquery插件生态,添加也方便。笔者半道接替该vue项目,中间要加一些新特性,还得看看有没有对应的vue轮子,十分麻烦。

效果请戳:H5 Video(在移动端模式查看)

1.基本video属性设置

  1. poster:视频未播放前的代替图片,如果未设置该属性,默认使用视频第一帧(但小部分机型兼容性不好)。建议添加

  2. muted: 静音. 建议添加

  3. webkit-playsinline/playsinline: 视频播放时局域播放,不脱离文档流 。基本保证iphone手机在H5页面内播放。个别不支持可以引入第三方库iphone-inline-video。建议添加

  4. x5-video-player-type="h5"/x5-playsinline: 启用同层H5播放器,保证anroid手机在H5页面内播放,但在各android机型下表现不一。建议添加

<video
    ref="video"
    :poster="startSource"
    muted
    x-webkit-airplay="allow"
    x5-video-player-type="h5" x5-playsinline
    webkit-playsinline playsinline>
    <source :src="videoSource" type="video/mp4" />
</video>

2.自动播放

先说结论:如果需要微信/网易云音乐/微博/QQ/浏览器等各平台完美自动播放,不行。正确的解决方案:让视觉设计引导用户点击屏幕,进行播放视频。或者如果产品能接受,只要用户接触屏幕就开始播放(监听touchstart事件)。错误方式:video标签autoplayjs执行video.playload完成后执行play()

只在微信端传播。微信浏览器是经过特殊处理的,可以通过回调WeixinJSBridgeReady解决,适用于iPhone和anroid。注意自动播放的视频要无音轨或者手动muted。见示例代码:

<!-- 必须加在微信api资源 --> 
<script src="http://res.wx.qq.com/open/js/jweixin-1.0.0.js"></script>

let that = this
if (window.WeixinJSBridge) {
    WeixinJSBridge.invoke('getNetworkType', {}, function (e) {
        video.play()
    }, false);
} else {
    document.addEventListener("WeixinJSBridgeReady", function () {
        WeixinJSBridge.invoke('getNetworkType', {}, function (e) {
            video.play()
        });
    }, false);
}

3.视频开始短暂黑屏

部分android机型点击播放视频时,会出现短暂1~2s的黑屏。该问题出现可能是还没请求完成可顺利播放的视频。

解决方案:在视频上叠加一个div,把它的背景图换成首帧图。监听timeupdate事件,有视频播放时移除该div。

<div @click="play">
      <video
        ref="video"
        :class="{'playing': playing}"
        :poster="startSource"
        x-webkit-airplay="allow"
        x5-video-player-type="h5"
        x5-playsinline
        webkit-playsinline playsinline>
        <source :src="videoSource" type="video/mp4" />
      </video>
      <div :class="['cover-start']" v-if="!playing"></div>
    </div>
this.videoNode.addEventListener('timeupdate', () => {
    // 当视频的currentTime大于0.1时表示黑屏时间已过,已有视频画面,可以移除浮层
    if (this.videoNode.currentTime > 0.1 && !this.playing) {
        this.playing = true
    }
}, false)

4.部分Android机跳到x5 player播放视频

有些android在微信或浏览器,播放视频会跳到x5 player播放器中。这种video位于页面最顶层,无法被遮盖,说不定播完会推送腾讯视频信息,而且不会自动关掉。

解决方案:利用timeupdate事件,当视频快要结束时,手动remove掉整个视频。

this.videoNode.addEventListener('timeupdate', () => {
    // 兼容Android X5在浏览器行为.时间为视频时长
    if (this.videoNode.currentTime > 56.9) {
        this.isShowVideo = false
    }
}, false)

5.视频canplay的坑

换了引导用户的视频方案后,前面有个loading页面。产品希望视频加载好后,loading消失并视频可点击。但是ios下canplay和canplaythrough事件都不会执行回调。ios是点击播放后才会去加载视频流。android下会执行canplay事件回调,但视频流也是边下边播。所以无法准确获得视频可加载时间点

总结:H5现在视频标准不完善,除了timeupdateended事件外,其他事件慎用。

6.safari可以缩放视频

通常情况在meta的viewport中设置user-scalable=no即可。但是IOS 10以后的safari中,apple为了提高Safari中网站的辅助功能,即使网站在视口中设置该属性,用户也可以手动缩放。

解决方案:

// IOS10 Safari不识别meta,故需要js hack
document.documentElement.addEventListener('touchstart', function (event) {
if (event.touches && event.touches.length > 1) {
    event.preventDefault()
}
}, false)

Vue-Router写法推荐

使用路由懒加载,实现方式是结合Vue异步组件和Webpack代码分割功能。

优点:

  • 减小包体积,提高加载速度
  • 当页面>20个时,组件定义需要拉到编辑器顶部才知道具体路径

bad

import IntentionList from '@/pages/intention/list'
import Variable from '@/pages/variable'
...

{
    path: '/intention/list',
    name: 'ilist',
    component: IntentionList
},
{
    path: '/variable',
    name: 'variable',
    component: Variable
}

good

{
    path: '/intention/list',
    name: 'ilist',
    component: () => import('@/pages/intention/list')
},
{
    path: '/variable',
    name: 'variable',
    component: () => import('@/pages/variable')
}

import语法需要Babel添加syntax-dynamic-import插件。最新当vue-cli 3.0中默认添加该特性,不需要额外引用。

Vue Dialog弹窗解决方案

Vue Dialog弹窗解决方案

在做业务代码的modal弹窗时,总是围绕visible变量以及控制visible变量逻辑,能否简化弹窗相关逻辑呢?

如果想直接使用该解决方案,可以安装对应npm包,详细说明文档请在github中查看:@springleo/el-dialog-helper

该方案配合ElementUI或AntdV等组件库的modal组件更佳。

背景

在业务中,如下代码一定不会陌生:

<template>
    <div>
        your page logic
        <Dialog1
            :visible="dialogVisible1"
            :id="id" :name="name" ...
            @close="() => dialogVisible1 = false" />
        <Dialog1
            :visible="dialogVisible1"
            :id="id" :name="name" ...
            @close="() => dialogVisible1 = false" />
        <Dialog1
            :visible="dialogVisible1"
            :id="id" :name="name" ...
            @close="() => dialogVisible1 = false" />
        ...
    </div>
</template>

<script>
import Dialog1 from './dialog1'
import Dialog2 from './dialog2'
import Dialog3 from './dialog3'

export default {
    components: { Dialog1, Dialog2, Dialog3, ... }
    data() {
        return {
            dialogVisible1: false,
            dialogVisible2: false,
            dialogVisible3: false,
            ...
        }
    },
    methods: {
        openDialog1() { this.dialogVisible1 = true },
        openDialog2() { this.dialogVisible2 = true },
        openDialog3() { this.dialogVisible3 = true },
        ...
    }
}
</script>

它存在的问题在于:

  1. Dialog弹窗过多时,visible变量也相应增加
  2. 每个Dialog在组件中都需要注册并相应的初始化,繁琐并增加页面组件初始化时间
  3. visible变量控制繁琐

有没有更好的方式呢?

好的方案

作为参照,我们可以很快联想到各个组件库的$confirm实现ConfirmDialog的实现。

它们主要实现方式一致,主要体现在API不同:

如使用Promise方式的ElementUI $confirm API

this.$confirm(props)
    .then(() => {})
    .catch(() => {})

另外一种是使用回调函数的方式,如Antdv $confirm API

this.$confirm({
    ...props,
    onOk: () => {},
    onCancel: () => {},
})

但ConfirmDialog是有固定的业务组件,而我们定义的Dialog是无法确定的,有没有办法让自定义Dialog拥有Promise API的调用方式?

目标

从个人业务实践角度讲,较好的目标API是使用js API调用弹窗 + Promise API进行控制回调。如下:

import Dialog1 from 'dialog1'

this.$openDialog(Dialog1)(props)
    .then(data => {})
    .catch(err => {})

$confirm是因为有固定流程以及样式的ConfirmDialog组件,所以实现起来较为明确而简单,底层源码里也是直接引入ConfirmDialog组件,在这之上再进行包装。

但我们这里需要做通用的Dialog弹窗方式,该如何实现呢?

实现机制

要实现上述API的Dialog弹窗解决方案,需要做到2步: 1. dialog组件自动挂载到页面 2. API设计Promise化

1. dialog组件自动挂载到页面

通过vue源码我们知道,一个.vue文件,其实就是个Object对象(tempalte模板会被编译为对象的render函数)。同时也知道Vue Option API是通过new Vue({ Option API })方式转换为组件实例的。

此时我们这里如何把Object对象转换为Vue组件呢?Vue官方提供了Vue.extend这个API来返回Vue构造器。有了该构造函数,只需要实例化即可把Object Vue对象转为真正的具有上下文关系的Vue组件。同时执行$mount()方法即可挂载到指定节点上,并在UI上更新。

const $openDialog = (component, propsData) => {
    const ComponentConstructor = Vue.extend(component);
    let instance = new ComponentConstructor({
      propsData,
    }).$mount(document.body);
    return instance
}

2. API设计Promise化

以上只是考虑了通过js api方式,手动添加Dialog,还需要考虑当用户关闭弹窗时,如何正常销毁Dialog。同时考虑到现实业务中,弹窗关闭通常都由弹窗内逻辑控制,所以需要设计相关API,把弹窗内逻辑和当前页逻辑进行解耦。

销毁Dialog的DOM,必然需要找到包裹的parent DOM,所以需要使用闭包来保存parent DOM。

Vue2.x组件实例本身也是一个发布订阅系统,其支持通过$emit$once方式进行事件发布和订阅。所以当Dialog弹窗内完成业务时,只需要发布关闭事件即可,完全的业务方自主可控。同时为了业务方使用简化,API设计为Promise,使用.then/.catch来代替弹窗业务成功/失败。

const $openDialog = (component) => {
  // 闭包存储
  const div = document.createElement('div');
  const el = document.createElement('div');
  div.appendChild(el);
  document.body.appendChild(div);

  const ComponentConstructor = Vue.extend(component);
  return (propsData = {}, parent = undefined) => {
    // 手动弹窗
    let instance = new ComponentConstructor({
      propsData,
      parent, // 父级上下文,设置了此参数可获得$store/$router等Provide对象
    }).$mount(el);

    // 关闭弹窗
    const destroyDialog = () => {
      if (instance && div.parentNode) {
        instance.$destroy();
        instance = null
        div.parentNode && div.parentNode.removeChild(div);
      }
    };

    // 使用.then/.catch来代替弹窗业务成功/失败
    return new Promise((resolve, reject) => {
      instance.$once("done", data => {
        destroyDialog();
        resolve(data);
      });
      instance.$once("cancel", data => {
        destroyDialog();
        reject(data);
      });
    });
  }
}

另外方案中还考虑了antdv/element-ui modal的便捷性,增加了visible控制,最终的解决方案源码可以看 lq782655835/el-dialog-helper

正则表达式一张图总结

正则表达式对于每个开发者都非常重要,用的好能在一些关键时刻让自己变得轻松。推荐个正则可视化工具:regulex,帮助学习者直观验证。

正则方法

regex.exec(string)

如果匹配成功,exec() 方法返回一个数组;匹配失败,返回 null

let regexExec = /#(.*)$/.exec('http://localhost:8081/#/demo')
/*
[ '#/demo',
'/demo',
index: 22,
input: 'http://localhost:8081/#/demo' ]
*/

regex.test(string)

查看正则表达式与指定的字符串是否匹配。返回 true 或 false

let regexTest = /#(.*)$/.test('http://localhost:8081/#/demo')
// true

string.match(regex)

类似regex.exec(string)

let stringMatch = 'http://localhost:8081/#/demo'.match(/#(.*)$/)
// [ '#/demo',
// '/demo',
// index: 22,
// input: 'http://localhost:8081/#/demo' ]

string.search(regex)

匹配成功,search() 返回正则表达式在字符串中首次匹配项的索引。否则,返回 -1。类似regex.test()

let stringSearch = 'http://localhost:8081/#/demo'.search(/#(.*)$/)
// 22

其他正则相关语法@jawil总结的十分详细,故转载在此:

ES6-解构赋值及原理

基础语法

数组

// 基础类型解构
let [a, b, c] = [1, 2, 3]
console.log(a, b, c) // 1, 2, 3

// 对象数组解构
let [a, b, c] = [{name: '1'}, {name: '2'}, {name: '3'}]
console.log(a, b, c) // {name: '1'}, {name: '2'}, {name: '3'}

// ...解构
let [head, ...tail] = [1, 2, 3, 4]
console.log(head, tail) // 1, [2, 3, 4]

// 嵌套解构
let [a, [b], [d]] = [1, [2, 3], 4]
console.log(a, b, d) // 1, 2, 4

// 解构不成功为undefined
let [a, b, c] = [1]
console.log(a, b, c) // 1, undefined, undefined

// 解构默认赋值
let [a = 1, b = 2] = [3]
console.log(a, b) // 3, 2

对象

// 对象属性解构
let { f1, f2 } = { f1: 'test1', f2: 'test2' }
console.log(f1, f2) // test1, test2

// 可以不按照顺序,这是数组解构和对象解构的区别之一
let { f2, f1 } = { f1: 'test1', f2: 'test2' }
console.log(f1, f2) // test1, test2

// 解构对象重命名
let { f1: rename, f2 } = { f1: 'test1', f2: 'test2' }
console.log(rename, f2) // test1, test2

// 嵌套解构
let { f1: {f11}} = { f1: { f11: 'test11', f12: 'test12' } }
console.log(f11) // test11

// 默认值
let { f1 = 'test1', f2: rename = 'test2' } = { f1: 'current1', f2: 'current2'}
console.log(f1, rename) // current1, current2

函数参数

// 参数解构
function func1({ x, y }) {
    return x + y
}
func1({ x: 1, y: 2}) // 3

function func1({ x = 1, y = 2 }) {
    return x + y
}
func1({x: 4}) // 6

String/Map/Set

// String
let [ a, b, c, ...rest ] = 'test123'
console.log(a, b, c, rest) // t, e, s, [ 't', '1', '2', '3' ]

// Map
let [a, b] = new Map().set('f1', 'test1').set('f2', 'test2')
console.log(a, b) // [ 'f1', 'test1' ], [ 'f2', 'test2' ]

// Set
let [a, b] = new Set([1, 2, 3])
console.log(a, b) // 1, 2

解构原理

解构是ES6提供的语法糖,其实内在是针对可迭代对象Iterator接口,通过遍历器按顺序获取对应的值进行赋值。这里需要提前懂得ES6的两个概念:

  • Iterator
  • 可迭代对象

Iterator概念

Iterator是一种接口,为各种不一样的数据解构提供统一的访问机制。任何数据解构只要有Iterator接口,就能通过遍历操作,依次按顺序处理数据结构内所有成员。ES6中的for of的语法相当于遍历器,会在遍历数据结构时,自动寻找Iterator接口。

Iterator作用:

  • 为各种数据解构提供统一的访问接口
  • 使得数据解构能按次序排列处理
  • 可以使用ES6最新命令 for of进行遍历
function makeIterator(array) {
    var nextIndex = 0
    return {
      next: function() {
        return nextIndex < array.length ?
            {value: array[nextIndex++]} :
            {done: true}
        }
    };
  }


var it = makeIterator([0, 1, 2])

console.log(it.next().value) // 0
console.log(it.next().value) // 1
console.log(it.next().value) // 2

可迭代对象

可迭代对象是Iterator接口的实现。这是ECMAScript 2015的补充,它不是内置或语法,而仅仅是协议。任何遵循该协议点对象都能成为可迭代对象。可迭代对象得有两个协议:可迭代协议迭代器协议

可迭代协议:对象必须实现@@iterator方法。即对象或其原型链上必须有一个名叫Symbol.iterator的属性。该属性的值为无参函数,函数返回迭代器协议。

属性
Symbol.iterator 返回一个对象的无参函数,被返回对象符合迭代器协议。

迭代器协议:定义了标准的方式来产生一个有限或无限序列值。其要求必须实现一个next()方法,该方法返回对象有done(boolean)和value属性。

属性
next 返回一个对象的无参函数,被返回对象拥有两个属性:done和value
done - 如果迭代器已经经过了被迭代序列时为 true。这时 value 可能描述了该迭代器的返回值。如果迭代器可以产生序列中的下一个值,则为 false。这等效于连同 done 属性也不指定。
value - 迭代器返回的任何 JavaScript 值。done 为 true 时可省略。

通过以上可知,自定义数据结构,只要拥有Iterator接口,并将其部署到自己的Symbol.iterator属性上,就可以成为可迭代对象,能被for of循环遍历。

// 自定义可迭代对象
let obj = {
    [Symbol.iterator] : function() {
        return{
            next: function() {
                return { value: 1, done: true }
            }
        }
    }
}

for (let item of obj) {
    console.log(item) // 不会报错,因为obj已经是可迭代对象
}

解构语法糖

String、Array、Map、Set等原生数据结构都是可迭代对象,可以通过for of循环遍历它。故可以通过ES6解构语法糖依次获取对应的值。

// String
let str = 'test'
let iterFun = str[Symbol.iterator]
let iterator = str[Symbol.iterator]()
let first = iterator.next() // 等效于 let [first] = 'test'
console.log(iterFun, iterator, first)
// 打印
// [Function: [Symbol.iterator]], {}, { value: 't', done: false }

// Array
let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();

// 以下等效于 let [first, second, third, four] = ['a', 'b', 'c']
let first = iter.next() // { value: 'a', done: false }
let second = iter.next() // { value: 'b', done: false }
let third = iter.next() // { value: 'c', done: false }
let four = iter.next() // { value: undefined, done: true }

原生object对象是默认没有部署Iterator接口,即object不是一个可迭代对象。因为遍历时,不知道到底哪个属性先遍历,哪个属性后遍历,需要开发者手动指定。不过object部署Iterator接口没有必要,因为ES6提供了Map数据结构。实际上对象被解构时,会被当作Map进行解构。所以虽然Map和Object很多地方相似,但ES6引入Map、Set对象是有其原因的。

参考文章

阮一峰ECMAScript 6

Mozilla - for of

Mozilla - Iteration protocols

TypeScript开发Vue应用

本文旨在帮助读者如何在Vue应用中配置TypeScript,使得使用TypeScript强类型验证开发Vue应用。

快速开发

如果想一键快速搭建TypeScript的Vue环境,github有人对vue-cli添加了typescript模板,可直接用来开发。vue-cli官方也正在开发类似的模板。如果对配置TypeScript有兴趣或者对现有应用改造,请接着看下文。

vue init SimonZhangITer/vue-typescript-template <project-name>

初始化项目

笔者使用vue-cli的master版本为基础,建立webpack项目。

vue init webpack my-project

引入TypeScript包

既然需要用到typescript,那就要加入一些core包和第三方支持包。

// 安装vue的官方插件
npm i vue-class-component vue-property-decorator --save

// ts-loader typescript 必须安装,其他的相信你以后也会装上的
npm i ts-loader typescript tslint-loader --save-dev

配置webpack

  1. 找到./build/webpack.base.conf.js

  2. 将文件main.js改为main.ts,webpack.base.conf.js入口后缀也改下

  3. resove.extensions加入.ts

resolve: {
    extensions: ['.js', '.vue', '.json', '.ts'],
    alias: {
      '@': resolve('src')
    }
  }
  1. 添加module.rules,使得webpack能解析.ts
{
    test: /\.tsx?$/,
    loader: 'ts-loader',
    exclude: /node_modules/,
    options: {
    appendTsSuffixTo: [/\.vue$/],
    }
}
  1. TypeScript 默认并不支持 *.vue 后缀的文件,创建src/typing/vue-shim.d.ts文件, typescript会自动加载解析.d.ts后缀文件。
declare module "*.vue" {
  import Vue from "vue";
  export default Vue;
}
  1. 改造.vue文件,尝试改为ts写法。将App.vue改为如下:
<template>
 <div id="app">
   <img src="./assets/logo.png">
   <router-view/>
 </div>
</template>

<script lang="ts">
import Vue from 'vue'
import Component from 'vue-class-component'

@Component({
 name: 'app'
})
export default class App extends Vue {}
</script>

配置TypeScript以及环境

修改完以上步骤,npm run dev依然会报错。因为typescript配置以及vue-cli给我们提供的默认配置,依然有些问题。应用typescript中,webpack3/4对应ts-loader有版本要求。所以这里给出我的配置文件,依次替换即可

  1. 根目录新增tsconfig.json文件,提供typescript配置
{
 "include": [
   "src/*",
   "src/**/*"
 ],
 "exclude": [
   "node_modules"
 ],
 "compilerOptions": {
   // types option has been previously configured
   "types": [
     // add node as an option
     "node"
   ],
   // typeRoots option has been previously configured
   "typeRoots": [
     // add path to @types
     "node_modules/@types"
   ],
   // 以严格模式解析
   "strict": true,
   // 在.tsx文件里支持JSX
   "jsx": "preserve",
   // 使用的JSX工厂函数
   "jsxFactory": "h",
   // 允许从没有设置默认导出的模块中默认导入
   "allowSyntheticDefaultImports": true,
   // 启用装饰器
   "experimentalDecorators": true,
   "strictFunctionTypes": false,
   // 允许编译javascript文件
   "allowJs": true,
   // 采用的模块系统
   "module": "esnext",
   // 编译输出目标 ES 版本
   "target": "es5",
   // 如何处理模块
   "moduleResolution": "node",
   // 在表达式和声明上有隐含的any类型时报错
   "noImplicitAny": true,
   "lib": [
     "dom",
     "es5",
     "es6",
     "es7",
     "es2015.promise"
   ],
   "sourceMap": true,
   "pretty": true
 }
}
  1. package.json.eslintrc.js.postcssrc.js修改

package.json

"devDependencies": {
    "@types/node": "^8.0.58",
    "autoprefixer": "^7.1.2",
    "babel-core": "^6.22.1",
    "babel-eslint": "^7.1.1",
    "babel-helper-vue-jsx-merge-props": "^2.0.3",
    "babel-loader": "^7.1.1",
    "babel-plugin-syntax-jsx": "^6.18.0",
    "babel-plugin-transform-runtime": "^6.22.0",
    "babel-plugin-transform-vue-jsx": "^3.5.0",
    "babel-preset-env": "^1.3.2",
    "babel-preset-stage-2": "^6.22.0",
    "chalk": "^2.0.1",
    "copy-webpack-plugin": "^4.0.1",
    "css-loader": "^0.28.0",
    "eslint": "^3.19.0",
    "eslint-config-standard": "^10.2.1",
    "eslint-friendly-formatter": "^3.0.0",
    "eslint-loader": "^1.7.1",
    "eslint-plugin-html": "^3.0.0",
    "eslint-plugin-import": "^2.7.0",
    "eslint-plugin-node": "^5.2.0",
    "eslint-plugin-promise": "^3.4.0",
    "eslint-plugin-standard": "^3.0.1",
    "extract-text-webpack-plugin": "^3.0.0",
    "file-loader": "^1.1.4",
    "friendly-errors-webpack-plugin": "^1.6.1",
    "html-webpack-plugin": "^2.30.1",
    "node-notifier": "^5.1.2",
    "optimize-css-assets-webpack-plugin": "^3.2.0",
    "ora": "^1.2.0",
    "portfinder": "^1.0.13",
    "postcss-import": "^11.0.0",
    "postcss-loader": "^2.0.8",
    "rimraf": "^2.6.0",
    "semver": "^5.3.0",
    "shelljs": "^0.7.6",
    "ts-loader": "^3.2.0",
    "typescript": "^2.6.2",
    "uglifyjs-webpack-plugin": "^1.1.1",
    "url-loader": "^0.5.8",
    "vue-class-component": "^6.1.2",
    "vue-loader": "^13.3.0",
    "vue-style-loader": "^3.0.1",
    "vue-template-compiler": "^2.5.2",
    "webpack": "^3.6.0",
    "webpack-bundle-analyzer": "^2.9.0",
    "webpack-dev-server": "^2.9.1",
    "webpack-merge": "^4.1.0"
  },

.eslintrc.js

module.exports = {
 root: true,
 parser: 'babel-eslint',
 parserOptions: {
   sourceType: 'module'
 },
 env: {
   browser: true,
 },
 extends: 'standard',
 plugins: [
   'html'
 ],
 // add your custom rules here
 rules: {
   // allow async-await
   'generator-star-spacing': 'off',
   // allow debugger during development
   'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
 }
}

.postcssrc.js

module.exports = {
 "plugins": {
   "postcss-import": {},
   // "postcss-url": {},
   "autoprefixer": {}
 }
}

参考文章

vue-typescript-dpapp-demo

vue-typescript-starter

Vue2.0源码分析 - 框架结构

看过Vue源码有一段时间了,对里面的研究也有一些心得。对于Vue源码分析文章,笔者以前不是很想写,因为网上有太多优质的Vue源码分析文章,笔者读源码时也受益于此。但思维的转变,发生在一次团队Code Review。笔者所在的团队主要使用Vue技术栈(团队小伙伴都是多面手,一专多能),但有些小伙伴受限于业务开发,Vue停留在API使用层。所以笔者组织大家对Vue进行源码分析,各自分工并且每周Code Review分享给所有人,借此提升团队的工作效率,也形成一个良好的团队氛围。故以文字记录下来,一方面沉淀自己,另一方面也为团队code review做备忘录。

笔者认为,读源码需要从全局到局部,先了解整个源码的目录结构,懂得项目里有哪些内容,然后列举出来,再通过个人兴趣研究点,逐个攻破。比如通读Vue目录结构,里面有core/compile/weex/ssr等,再提取出研究的web核心:双向数据绑定/组件系统/事件/生命周期/ast编译/vnode等。读源码切忌一行一行分析,这样容易陷入局部思考,也容易有挫折感。源码也不是只读一遍就能理解,所以需要反复回溯,细细思考。

源码分析顺序:

  • 目录结构
  • 核心内容
  • 重点突破
  • 回归溯源

目录结构

思维导图可以清晰的了解到全局内容。看目录结构,除去build打包,类型定义以及ssr/weex,能了解到我们研究的核心就compiler、core、platforms.web三个文件夹。

image

核心内容

通过分析,按照工作常用API权重配比,优先研究vue core,接下来插件plugin,最后再其他细枝末节detail

image

整体流程

下图整理了vue初始化流程以及代码流转,建议配合Vue.js技术揭秘文章去了解。

image

git团队规范

总结团队git规范以及常用的git命令操作。Mac推荐可视化软件Sourcetree

git规范

  1. 一张图原理

image

  1. 分支规范
  • 核心分支

    • master
    • develop
  • 临时分支

    • feature
    • release
    • fix

临时分支建议隔段时间清理一下。建议分支命名:type + function + date, 如做了select组件特性,命名为feature-select-0812

image

  1. 提交规范

type: 功能简述 + 详情

type: feature、 enhance、 fix、 test、docs

命令式提交代码

  1. 提交到本地仓库
git add .
git commit -m 'i'
  1. 提交到远程仓库
git push origin master

本地项目关联远程仓库

  1. 前提:本地项目已存在 && 远程仓库已新建

  2. 本地仓库转为本地版本库

cd your-project
git init
git add .
git commit -m 'init'
  1. 关联远程仓库(important)
git remote add origin <URL>
  1. 推送远程仓库
  • 远程不冲突
git push -u origin master
  • 远程冲突

    1. 拉取远程仓库最新文件
    git pull origin master --allow-unrelated-histories
    1. 手动解决冲突,然后再重新本地提交,再推送远程提交。
    solve conflict
    git add .
    git commit -m 'conflict'
    git push -u origin master

打tag

git这个功能相当于,给项目做一个总结,存档当前代码。

查看tag

git tag

新建tag并推送

git tag -a <tag-name> -m <comment>
git push origin --tags

Electron工程踩坑记录

Electron工程踩坑记录

最近公司有个新产品线,需要将应用打包成客户端,提供私有化部署。考虑到Web线上已经实现大部分需求,技术选型使用Electron。本文不是帮助读者的扫盲文,只是记录下项目工程中遇到的坑,所以阅读本文需要web和electron知识。

应产品要求,私有化部署主要考虑windows端,mac端其次。框架选型使用electron-vue脚手架(这里也强烈推荐),该脚手架包含Vue技术栈单页应用 + electron + 打包完整流程。内置Vuex,Vue-Router,Webpack,electron-builder等。下面的大部分实践源码放在这

1. 自定义标题栏

这应该是每一个使用electron实现web客户端都会遇到的问题,使用原生的外边框,第一太丑,第二也不统一。

解决方案:frame + css drag

frame: false: 主进程中设置窗体参数。去掉默认的标题栏

-webkit-app-region: drag: 渲染进程中设置css。对应的组件可以进行拖动了

mainWindow = new BrowserWindow({
    height: 350,
    width: 550,
    useContentSize: true,
    resizable: isDev, // 是否可调整大小
    alwaysOnTop: !isDev, // 应用是否始终在所有顶层之上
    transparent: true, // 透明边框
    frame: false, // 不使用默认边框
    center: true
})
.u-header {
    position: relative;
    width: 100%;
    height: 50px;
    line-height: 50px;
    -webkit-app-region: drag; /* as window header */
}

2. 标题栏按钮无效 -- only windows

该bug只在windows平台上显示,mac上正常。在header组件中设置为drag,导致组件里的元素都无法点击。

解决方案:在需要点击的元素上添加no-drag。-webkit-app-region: no-drag;详细看此issue

3. 自定义标题栏无法实现css hover -- only windows

当设置了为drag时,在windows上会屏蔽所有的鼠标事件,所以hover不起作用。这是一个由操作系统导致的问题,故无法修复,相关issue

解决方案:去掉-webkit-app-region: drag;即可。

如果要同时保留可拖动并且hover上有变化,在windows暂时无法实现,需要对此进行取舍或改变交互设计。

4. 打包后程序调试

electron-vue在开发环境默认启用electron-debug插件开启调试。但打包完成,交付到测试同学手里,需要在错误的时候打开开发者工具定位问题。

解决方案:通过注册快捷键,调开web的开发者模式。

globalShortcut.register('CommandOrControl+Shift+L', () => {
    let focusWin = BrowserWindow.getFocusedWindow()
    focusWin && focusWin.toggleDevTools()
})

5. 文本不可选择

既然作为客户端,就应该像个客户端程序,不能对展示型的文本进行用户选择。

解决方案:使用css -webkit-user-select: none;

html {
    -webkit-tap-highlight-color: transparent;
    -webkit-text-size-adjust: 100%;
    height: 100%;
    -webkit-user-select: none; /* disable user select text */
}

6. 打包参数设置

electron应用需要进行打包,变成exe可执行文件给用户。推荐使用最新的electron-builder进行打包(electron-vue脚手架中有提供该选项)。这里对常用的设置进行说明

scripts: {
    /** 打包成windows系统 **/
    "build": "node .electron-vue/build.js && electron-builder --win",
    /** 打包成macos系统 **/
    "build:mac": "node .electron-vue/build.js && electron-builder --mac",
},
"build": {
    /** 最终可执行文件名称:${productName}-${version}.${ext} **/
    "productName": "sight-electron-app",
    "appId": "sight.controller",
    /** 压缩形式,默认normal;store打包最快,适合测试;maximum打包体积最小,适合生产模式 **/
    "compression": "maximum",
    /** 是否将多个文件合并为tar风格的归档模式 **/
    "asar": true,
    "directories": {
      "output": "build"  /** 打包结果目标地址 **/
    },
    "files": [
      "dist/electron/**/*" /** 需要打包的文件地址 **/
    ],
    /** 不同平台设置 **/
    "mac": {
      "icon": "build/icons/icon.icns"
    },
    "win": {
      "icon": "build/icons/icon.ico"
    },
    "linux": {
      "icon": "build/icons"
    }
}

7. 触摸板放大缩小 -- only mac

在macOS系统中,触摸板的放大缩小手指指令,会导致electron程序内的webFrame内容也跟着放大缩小。

解决方案:在renderer进程中设置其缩放范围require('electron').webFrame.setZoomLevelLimits(1, 1)

8. web端唤起本地客户端

electron提供该API能力:app.setAsDefaultProtocolClient(protocol[, path, args])

9. 禁止多开窗口

多次双击window 的exe文件,会开启多个窗口;mac下默认开1个,但通过命令还是可以多开。

解决方案:判断单实例:app.makeSingleInstance(callback)

/**
 * 防止应用多开。bugfix:sholudQuit总是返回true,故暂时注释以下代码
 * 当进程是第一个实例时,返回false。
 * 如果是第二个实例时,返回true,并且执行第一个实例的回调函数
 */
const shouldQuit = app.makeSingleInstance((commandLine, workingDir) => {
    if (mainWindow) {
        mainWindow.isMinimized() && mainWindow.restore()
        mainWindow.focus()
    }
})
if (shouldQuit) {
    app.quit()
}

10. 网络状态检测

客户端经常遇见断网情况处理,当网络断开时需要给用户提示,当网络连接时继续服务。通常web情况下是采取轮询服务器方式,但这种方式比较消耗服务器性能。这里可以利用electron的node工具包public-ip进行判断。public-ip查询dns获取公网ip地址,如果能拿到值表示联网正常。本来到此可以很好的解决,但产品要求的客户端,既要提供公共部署,也需要进行无外网情况下的私有化部署

解决方案:public-ip + 轮询方式。优先进行公网IP查询,如果成立则返回网络状态良好,如果查询不到再进行服务器心跳检查。实现方式参考is-online

11. 日志监听

每个系统的异常监控都必不可少,特别是私有化部署客户端这种模式,日志记录显得必不可少。由于electron拥有node的环境,结合window.onerror收集错误信息,前端把日志记录在本地文件。当出现问题时,用户可以直接把日志文件发给开发者,从而定位原因。如果是网络版模式,可以通过Ajax收集错误信息。如果是程序异常崩溃,window.onerror可能没法监测的到,好在electron提供了CrashReporter收集

解决方案:推荐electron-log + CrashReporter

const log = require('electron-log')

log.transports.file.level = 'info'
log.transports.file.format = '{h}:{i}:{s}:{ms} {text}'
log.transports.file.maxSize = 5 * 1024 * 1024
log.transports.console.level = false

12. 自动更新

该需求停留在调研,这篇文章讲的非常详细,待实践好再来续更

最后,附上@changkun的electron深度总结思维导图,总结的非常棒,许多细节使笔者受益良多。出处

H5 Video踩坑记录

临时接手一个即将上线的公司项目,纯H5活动页,内容不多,但对还原度和各机型兼容性(尤其是Android机型)有极高要求。涉及的问题很多,这里重点说下在H5中Video的一系列坑。插个技术选型问题,不复杂的活动页建议使用jquery技术栈,而不是使用vue和reactjs等。后者的优点在于组件系统,可复用度高,适合大型项目。活动页一般UI改动频繁,动效多,适合jquery插件生态,添加也方便。笔者半道接替该vue项目,中间要加一些新特性,还得看看有没有对应的vue轮子,十分麻烦。

效果请戳:H5 Video(在移动端模式查看)

1.基本video属性设置

  1. poster:视频未播放前的代替图片,如果未设置该属性,默认使用视频第一帧(但小部分机型兼容性不好)。建议添加

  2. muted: 静音. 建议添加

  3. webkit-playsinline/playsinline: 视频播放时局域播放,不脱离文档流 。基本保证iphone手机在H5页面内播放。个别不支持可以引入第三方库iphone-inlie-video。建议添加

  4. x5-video-player-type="h5"/x5-playsinline: 启用同层H5播放器,保证anroid手机在H5页面内播放,但在各android机型下表现不一。建议添加

<video
    ref="video"
    :poster="startSource"
    muted
    x-webkit-airplay="allow"
    x5-video-player-type="h5" x5-playsinline
    webkit-playsinline playsinline>
    <source :src="videoSource" type="video/mp4" />
</video>

2.自动播放

先说结论:如果需要微信/网易云音乐/微博/QQ/浏览器等各平台完美自动播放,不行。正确的解决方案:让视觉设计引导用户点击屏幕,进行播放视频。或者如果产品能接受,只要用户接触屏幕就开始播放(监听touchstart事件)。错误方式:video标签autoplayjs执行video.playload完成后执行play()

只在微信端传播。微信浏览器是经过特殊处理的,可以通过回调WeixinJSBridgeReady解决,适用于iPhone和anroid。注意自动播放的视频要无音轨或者手动muted。见示例代码:

<!-- 必须加在微信api资源 --> 
<script src="http://res.wx.qq.com/open/js/jweixin-1.0.0.js"></script>

let that = this
if (window.WeixinJSBridge) {
    WeixinJSBridge.invoke('getNetworkType', {}, function (e) {
        video.play()
    }, false);
} else {
    document.addEventListener("WeixinJSBridgeReady", function () {
        WeixinJSBridge.invoke('getNetworkType', {}, function (e) {
            video.play()
        });
    }, false);
}

3.视频开始短暂黑屏

部分android机型点击播放视频时,会出现短暂1~2s的黑屏。该问题出现可能是还没请求完成可顺利播放的视频。

解决方案:在视频上叠加一个div,把它的背景图换成首帧图。监听timeupdate事件,有视频播放时移除该div。

<div @click="play">
      <video
        ref="video"
        :class="{'playing': playing}"
        :poster="startSource"
        x-webkit-airplay="allow"
        x5-video-player-type="h5"
        x5-playsinline
        webkit-playsinline playsinline>
        <source :src="videoSource" type="video/mp4" />
      </video>
      <div :class="['cover-start']" v-if="!playing"></div>
    </div>
this.videoNode.addEventListener('timeupdate', () => {
    // 当视频的currentTime大于0.1时表示黑屏时间已过,已有视频画面,可以移除浮层
    if (this.videoNode.currentTime > 0.1 && !this.playing) {
        this.playing = true
    }
}, false)

4.部分Android机跳到x5 player播放视频

有些android在微信或浏览器,播放视频会跳到x5 player播放器中。这种video位于页面最顶层,无法被遮盖,说不定播完会推送腾讯视频信息,而且不会自动关掉。

解决方案:利用timeupdate事件,当视频快要结束时,手动remove掉整个视频。

this.videoNode.addEventListener('timeupdate', () => {
    // 兼容Android X5在浏览器行为.时间为视频时长
    if (this.videoNode.currentTime > 56.9) {
        this.isShowVideo = false
    }
}, false)

5.视频canplay的坑

换了引导用户的视频方案后,前面有个loading页面。产品希望视频加载好后,loading消失并视频可点击。但是ios下canplay和canplaythrough事件都不会执行回调。ios是点击播放后才会去加载视频流。android下会执行canplay事件回调,但视频流也是边下边播。所以无法准确获得视频可加载时间点

总结:H5现在视频标准不完善,除了timeupdateended事件外,其他事件慎用。

6.safari可以缩放视频

通常情况在meta的viewport中设置user-scalable=no即可。但是IOS 10以后的safari中,apple为了提高Safari中网站的辅助功能,即使网站在视口中设置该属性,用户也可以手动缩放。

解决方案:

// IOS10 Safari不识别meta,故需要js hack
document.documentElement.addEventListener('touchstart', function (event) {
if (event.touches && event.touches.length > 1) {
    event.preventDefault()
}
}, false)

打个招聘广告

网易人工智能事业部-通用技术组

要求

1、计算机及相关专业本科以上学历,2年以上web前端开发经验;
2、熟练掌握前端相关技术(HTML/CSS/JavaScript/HTML5/CSS3...),了解http协议以及相关开发调试工具;
3、熟练掌握一个前端框架(React, Angular2, Vue, …);
4、对web服务器端开发(nodejs、java)有一定的了解和实践, 对web系统安全有一定的了解;最好会一门后台语言(Java,Python,.Net, ...)。
5、良好软件工程**,良好的编程能力和编程习惯;
6、工作认真负责,乐观开朗,善于团队合作;
7、有较强的逻辑分析、问题排查能力;
8、具有很强的学习能力和对新技术的追求精神。

投递请到邮箱地址[email protected],内推资格哦

200错误统一处理推荐

对于接口层来说,后端经常定义的结构如下:

{
    code: [Number], // 状态码
    desc: [String], // 详细描述
    detail: [Array, Object] // 前端需要的数据
}

bad

batchAddVariable({ globalParamList: validList })
    .then(res => {
        if (res === SERVER_ERROR_CODE.SUCCESS) { // !!!Bad: how many interface, how many judge 200
            this.close(true)
            this.$toast.show(res.detail.colletion) // !!!Bad: always get detail data
        } else { // !!!Bad: too much nest,reading difficulty
            this.$toast.show(res.desc)
            if (res === SERVER_ERROR_CODE.REPEAT) {
                ...
            }
        }
    })

good

batchAddVariable({ globalParamList: validList })
    .then(data => {
        this.close(true)
        this.$toast.show(data.colletion)
    })
    .catch(res => {
        if (res === SERVER_ERROR_CODE.REPEAT) {
            ...
        }
    })

解决方案

http层axios拿到数据后进行统一处理

import Vue from 'vue'
import axios from 'axios'

const service = axios.create({
    baseURL: rootURL, // api的base_url
    timeout: 15000, // 请求超时时间
})

// request拦截器
service.interceptors.request.use(
    config => {
        if (config.method === 'post' || config.method === 'put' || config.method === 'delete') {
            config.headers['Content-Type'] = 'application/json'
            if (config.type === 'form') {
                config.headers['Content-Type'] = 'multipart/form-data'
            } else {
                // 序列化
                config.data = JSON.stringify(config.data)
            }
        }

        return config
    },
    error => {
        Promise.reject(error)
    }
)

// respone拦截器
service.interceptors.response.use(
    response => {
        const res = response.data
        if (res.code === SERVER_ERROR_CODE.SUCCESS) { // 统一处理
            return res.detail // 直接返回数据
        } else {
            Vue.prototype.$toast.show(res.desc) // 错误统一报出
            return Promise.reject(res)
        }
    },
    error => {
        return Promise.reject(error)
    }
)

export default service

到此基本就可以很优雅的写逻辑代码了。不过还有个点可以继续优化。通常情况,后台返回非200错误,只需要$toast提示结果就行,catch代码部分可以省略。类似如下:

batchAddVariable({ globalParamList: validList })
    .then(data =>  this.close(true))
    // .catch(() => {}) // 业务通常这里不需要写

多么简洁的代码,但Promise执行reject代码,浏览器会报Uncaught (in promise)错误。这是因为中断了Promise操作但又没有对其进行处理,故由此错误。只需要使用unhandledrejection全局处理即可。

// Promise Catch不报错
window.addEventListener('unhandledrejection', event => event.preventDefault())

VSCode快捷键及常用插件

本文集合VSCode常用的快捷键和插件,希望能提高读者的开发效率。

  • 快捷键
  • 常用插件

快捷键

Command + shift + P

打开命令面板,可以执行VSCode的任何一条命令

 Command + P

  1. 直接输入文件名,快速打开文件
  2. ? 列出当前可执行的动作
  3. ! 显示Errors或Warnings,也可以Ctrl+Shift+M
  4. : 跳转到行数,也可以Ctrl+G直接进入
  5. @ 跳转到symbol(搜索变量或者函数),也可以Ctrl+Shift+O直接进入
  6. @:根据分类跳转symbol,查找属性或函数,也可以Ctrl+Shift+O后输入:进入
  7. # 根据名字查找symbol,也可以Ctrl+T
  8. > 可以回到主命令框模式

Commond + +  字体调大一号
Commond + -  字体调小一号

常用插件

Common

  1. Eslint

eslint代码检查插件,注意前提需要安装eslint  npm install eslint -g

  1. stylelint

  2. Project Manager

  3. Auto Import

  4. Auto Close Tag

  5. HTML Snippets

  6. vscode-icons

  7. Auto-Open Markdown Preview

  8. SVG Viewer

  9. Sort lines

Vue

  1. Vetur (推荐)
  2. VueHelper

React

  1. React-Native/React/Redux snippets for es6/es7

Nginx反向代理

什么是反向代理

当我们有一个服务器集群,并且服务器集群中的每台服务器的内容一样的时候,同样我们要直接从个人电脑访问到服务器集群服务器的时候无法访问,必须通过第三方服务器才能访问集群

这个时候,我们通过第三方服务器访问服务器集群的内容,但是我们并不知道是哪一台服务器提供的内容,此种代理方式称为反向代理。

image

image

反向代理优点

  • 保护网站安全,所有请求都先经过代理服务器。

  • 负载均衡,把请求转发到压力较小的服务器。

  • 可以做一些中间层设置,比如缓存静态资源

Nginx基础

安装

brew install nginx
nginx -v // 显示版本号则安装成功

命令

nginx目录

cd /usr/local/etc/nginx

启动nginx

nginx // 默认8080端口启动成功,可访问http://localhost:8080/

关闭nginx

nginx -s stop

重启nginx

nginx -s reload // 每次修改完nginx.conf文件就需要重启nginx

config配置

以下是典型的负载均衡nginx配置:

  1. 用户输入http://test-openai.com 时,访问80端口
  2. nginx监听到80端口被访问,匹配到的/路径,被反向代理到http://dramatic-offical-website
  3. dramatic-offical-website集群管理着一堆机器地址,从而实现负载均衡。
  4. 如果匹配到http://test-openai.com/images/ 路径,则直接映射/data下的文件
# 虚拟主机配置
server {
    server_name test-openai.com; # 请求到达的服务器名
    listen 80; # 监听80端口
    listen 443 ssl; # https默认端口是443

    # 对 / 所有做负载均衡+反向代理
    location / {
        proxy_pass http://dramatic-offical-website; # 代理到目标地址
    }

    # 静态文件,nginx自己处理
    location /images/ {
        root /data; # 映射到/data目录下
    }
}

# 设定负载均衡后台服务器列表
upstream dramatic-offical-website {
    server 10.192.106.133;
    server 10.192.106.134;
}

一个程序员的成长之路

做技术过程中,曾经疑惑技术的核心竞争力是什么?是青春饭?是否最终都要转管理岗?随着年纪和角色的改变,自己也在不断探索这些问题。当看到@fouber的这篇演讲时,内心产生很多共鸣,现在精简整理出来,希望能让更多技术人看到自己的方向和定位。

以下是全名直播CTO张云龙在FDCon2018的演讲,以思维导图精简整理。

image

MVC、MVP、MVVM区别

软件中最核心的,最基本的东西是什么? 是数据,我们写的所有代码,都是围绕数据的。

围绕着数据的产生、修改等变化,出现了业务逻辑。

围绕着数据的显示,出现了不同的界面技术。

MVC

网上很多资料对MVC看似有矛盾,其实是因为MVC模式主流分为主动MVC被动MVC两种。

主动MVC

主动MVC也是对应着传统MVC理论**,其中的主动是表示,Model变化会主动通知View更新。

Modal: 封装与应用程序的业务逻辑相关的数据以及对数据的处理方法。不要认为Modal是数据库的Entity层,其实理解为业务层更恰当。

View: 负责数据的展示,因为是Modal主动更新View,所以View需要事先订阅Modal的变化

Controller: M和V之间的连接器,接受View层的变化并更新到Modal上

被动MVC

这是常规Web MVC框架使用的模式,如ASP .NET MVC,Struts。Controller是一个核心层,负责管理View和Modal。

被动MVC中,模型Modal对视图View和控制器Controller一无所知,仅仅是被使用。视图也不会主动订阅Modal的更新。视图的显示是根据控制器来决定。

image

实际项目应用MVC

实际项目中,对MVC的应用往往采用更灵活的方式,除了每层各司其职外,还需要加入用户的交互指令。

如果你熟悉ASP .NET MVC,一定对以下这张图不陌生。

image

如果你熟悉Backbond,则更复杂些。用户既可以通过发送DOM事件到View,View再要求Model发生改变。也可以通过URL改变触发Controller层,从而改变View。Backbond View层比较重而Controller比较轻。

image

MVP

MVP是MVC的一种变种,其跟传统的MVC不同的表现在:

  • View层和Modal层没有直接关系,都是通过Presenter传递

  • Presenter与View层通信是双向的

MVVM

MVVM跟MVP基本类似,Presenter替换为ViewModal。其区别是MVVM通过双向数据绑定(通过事件同步到ViewModal和View)来进行View和ViewModal的同步。Vue、Angular、Ember都是采用这种模式。

MVVM使得前后端分离更加彻底。前端不再仅仅是UI层展示,可以将后端更多的业务逻辑搬到前端进行处理,后台除了提供常规的数据库业务数据,有更多的精力去专注于保持系统稳定和可扩展性。

参考文章:

阮一峰 MVC,MVC,MVP 和 MVVM 的图示

Scaling Isomorphic Javascript Code

AI前端CSS规范

分号

每个属性声明后面都要加分号。

命名

  1. 不使用id选择器
  2. 适用有意义的名词命名
  3. 单词全部小写,名词超过1个时,使用-分隔符

属性声明顺序

原则:整体到局部,外部到内部,重要属性优先

.element {
    display: block;
    float: left;
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: 100;
    margin: 0 100px;
    padding: 50px; // padding习惯写到margin后面
    width: 100px;
    height: 100px;
    border: 1px solid #e5e5e5; border-radius: 3px;
    font: normal 13px "Helvetica Neue", sans-serif;
    color: #333;
    text-align: center;
    line-height: 1.5;
    background-color: #f5f5f5;
    opacity: 1;
}

其他规范

使用prettier格式化工具约束,推荐配置如下:

  • 格式自动化
  • 4个缩进
  • 全部单引号
  • 属性:后有空格
  • 颜色全部小写
  • 小数点前面0自动添加
module.exports = {
    printWidth: 100, // 设置prettier单行输出(不折行)的(最大)长度

    tabWidth: 4, // 设置工具每一个水平缩进的空格数

    useTabs: false, // 使用tab(制表位)缩进而非空格

    semi: false, // 在语句末尾添加分号

    singleQuote: true, // 使用单引号而非双引号

    trailingComma: 'none', // 在任何可能的多行中输入尾逗号

    bracketSpacing: true, // 在对象字面量声明所使用的的花括号后({)和前(})输出空格

    arrowParens: 'avoid', // 为单行箭头函数的参数添加圆括号,参数个数为1时可以省略圆括号

    // parser: 'babylon', // 指定使用哪一种解析器

    jsxBracketSameLine: true, // 在多行JSX元素最后一行的末尾添加 > 而使 > 单独一行(不适用于自闭和元素)

    rangeStart: 0, // 只格式化某个文件的一部分

    rangeEnd: Infinity, // 只格式化某个文件的一部分

    filepath: 'none', // 指定文件的输入路径,这将被用于解析器参照

    requirePragma: false, // (v1.7.0+) Prettier可以严格按照按照文件顶部的一些特殊的注释格式化代码,这些注释称为“require pragma”(必须杂注)

    insertPragma: false, //  (v1.8.0+) Prettier可以在文件的顶部插入一个 @format的特殊注释,以表明改文件已经被Prettier格式化过了。

    proseWrap: 'preserve' // (v1.8.2+)
}

参考连接

百度CSS规范指南

腾讯CSS规范指南

Google CSS规范指南

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.