上一篇我们熟悉了一下 Inferno.js 的 DOM Diff 算法。今天我们来看 Vue.js 框架的 DOM Diff 算法。

Vue.js 的作者没有编写自己的算法,而是使用了 Snabbdom,并做了适当的修改。

DOM Diff 的关键算法在 https://github.com/snabbdom/snabbdom/blob/v0.7.1/src/snabbdom.ts#L179

同样,我们依然假设有原始的 DOM 集合A为 “dfibge”,更新后的集合B为 “igfheb”。

创建4个指针 oldStartIdxoldEndIdxnewStartIdxnewEndIdx,初始分别指向A的起始点、结束点和B的起始点、结束点。显然:

1
2
3
4
oldStartIdx=0
oldEndIdx=5
newStartIdx=0
newEndIdx=5

第一步,比较 A[oldStartIdx] 和 B[newStartIdx],如果相同,则 oldStartIdx++、newStartIdx++。
第二步,比较 A[oldEndIdx] 和 B[newEndIdx],如果相同,则 oldEndIdx++、newEndIdx++。

在本例中,以上两步全部不满足,跳过。

第三步,比较 A[oldStartVnode] 和 B[newEndVnode],如果相同,则节点发生了右移。
第四步,比较 A[oldEndIdx] 和 B[newStartIdx],如果相同,则节点发生了左移。

在本例中,以上两步全部不满足,跳过。

第五步,在A中搜索 B[newStartIdx],即 i,找到则把A中的 i 移到 A[oldStartVnode] 前面并 newStartIdx++、oldStartVnode++,否则则创建它。

在本例中,A变成 idfbge

返回第一步。


可以看到这个算法类似于优化后的插入排序。按照此算法,A的变换路径为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
A:dfibge
B:igfheb

=>

A:idfbge
B:igfheb
^

=>

A:igdfbe
B:igfheb
^

=>

A:igfdbe
B:igfheb
^

=>

A:igfhdbe
B:igfheb
^

=>

A:igfhedb
B:igfheb
^

=>

A:igfhebd
B:igfheb
^
=>

A:igfheb
B:igfheb
^

用一句话概括此算法的核心就是:依次遍历B集合,在A集合中找到对应项,放到与在B集合中相同的位置上。只不过 Snabbdom 使用了双向同时遍历来进行优化。

事实上,React 的 DOM Diff 算法与此也是非常类似的,只不过受限于 Fiber,只进行了单向搜索。但是即便如此, React 也引入了优化策略,尽量使得更多的元素不必移动。从本质上来看,Inferno 和 React 都利用递增子序列来进行了优化,但是 Inferno 使用算法来保证是最大递增子序列,而 React 的子序列是一定从第一个元素开始的,因此不一定是最大子序列。这在尾部元素移动到首部的时候,差异表现得更明显。

已经关注 Inferno.js 有两年的时间,终于在刚刚不久的过去建立起了官网和文档。

值得特别关注的是,Inferno.js 提供了类 React API 的同时,在 DOM Diff 算法上借鉴了 ivijs,取得了更高的效率,从而有较强的性能表现。

优秀的 DOM Diff 算法的核心都是在将一个 DOM 集合转换成另一个 DOM 集合的同时,尽可能地复用已有 DOM 并具有较少的 DOM 移动操作,这是由于 DOM 操作(甚至访问)的成本较高。

Inferno.js 在两个 VDom 之间进行比较,从而避免了频繁访问 DOM 的性能开销。下面我们看一下它实现的 DOM Diff 算法。

我们假设有原始的 DOM 集合A为 “dfibge”,更新后的集合B为 “igfheb”。

首先得到集合B中元素在A中的原始位置,如果在A中不存在则为-1,得到:

1
2
3
var A       = [d, f, i,  b, g, e];
var B = [i, g, f, h, e, b];
var sources = [2, 4, 1, -1, 5, 3];

现在我们获取该数组的最大递增子序列的序列位置,为:

1
var seq = [0, 1, 4];// [2, 4, 5]

这个数组的意义在于:“新数组中第0、1、4位置的元素在原始数组中是无需移动的”,这是我们能获取最小移动步数的关键。

一共6个成员,3个无需移动,那么一定有3个需要操作

现在我们从后往前遍历集合B,观察每个成员的位置是否存在于 seq 中,存在则不必操作,不存在则需移动。

首先我们需要先删除B中存在,但A中不存在的元素 d,此时:

1
A=[f, i, b, g, e]

观察B中最后一个元素:

1
2
3
A=[f, i, b, g, e]
B=[i, g, f, h, e, b]
^ pos=5

位置是5,不存在于 seq 中,需要把集合A中的 b 移到最后,此时:

1
A=[f, i, g, e, b] --- (1)

观察B中倒数第二个元素:

1
2
3
A=[f, i, b, g, e]
B=[i, g, f, h, e, b]
^ pos=4

位置是4,存在于 seq 中,无需操作。

观察B中倒数第三个元素:

1
2
3
A=[f, i, b, g, e]
B=[i, g, f, h, e, b]
^ pos=3

在A中不存在,我们需要创建 h,并放到 e 的前面,此时:

1
A=[f, i, g, h, e, b] --- (2)

观察B中倒数第四个元素:

1
2
3
A=[f, i, g, h, e, b]
B=[i, g, f, h, e, b]
^ pos=2

位置是2,不存在于 seq 中,需要把集合A中的 f 移到 h 的前面,此时:

1
A=[i, g, f, h, e, b] --- (3)

观察B中倒数第五个元素:

1
2
3
A=[i, g, f, h, e, b]
B=[i, g, f, h, e, b]
^ pos=1

位置是1,存在于 seq 中,无需任何操作。

观察B中倒数第六个元素,也是最后一个元素:

1
2
3
A=[f, i, b, g, e]
B=[i, g, f, h, e, b]
^ pos=0

位置是0,存在于seq中,也无需任何操作。

此时,所有操作都已结束,仅需3步,集合A已经转换成了集合B。其中,对 d 的移除和对 h 的创建是不可避免的,除此之外,仅进行了两次DOM移动,也印证了上面提到的 6-3=3 的操作步骤。

我们在移动 DOM 的时候都是执行的“插在XXX之前”,是因为 DOM 中的 insertBefore 方法,如果存在 insertAfter,那么从前往后操作也是等效的。

我们都知道 JavaScript 是通过原型链继承的,在不支持 class、extends 语法的环境里,继承又该如何实现呢?

首先我们来整理实现继承后的效果表征,假设存在两个类(函数)A 和 B,我们要实现 B 继承于 A,那么需要满足:

  1. new B() instanceof A 为真;
  2. B.prototype.constructor === B 为真;
  3. 如果 A.foo = 1,则 B.foo = 1 为真,即静态变量可继承;
  4. 在普通方法 foo 中可以调用 super.foo() 调用基类方法;
  5. 在构造方法中可以调用 super() 调用基类构造方法

在很多年前,jQuery 作者 John Resig 曾写过一个继承的实现,在那个还在 ES3 语法的时代已经非常难得。在今天看来,这种实现方式的语义已经远远落后,同时也不能实现静态变量。


下面我们来一步一步地去理解 babel 的实现方式。

首先定义两个类(函数),以及子类的实例:

1
2
3
4
function A() {}
function B() {}

var b = new B();

想实现 B 继承于 A,那边必然 b instanceof B 为真,根据 instanceof 的意义,一定有:

1
b.__proto__.__proto__... === A.prototype

我们知道:

1
b.__proto__ === B.prototype

那么我们只需要:

1
B.prototype.__proto__... === A.prototype

于是我们覆写 B.prototype

1
B.prototype = Object.create(A.prototype)

这样一定有 A.prototype.isPrototypeOf(B.prototype) 为真。

现在我们已经实现了:

1
b instanceof A === true

也就是说第一条我们已经实现了,下面看第二条,只需要:

1
B.prototype.constructor = B;

或者更优雅一些:

1
2
3
4
5
6
7
8
B.prototype = Object.create(A.prototype, {
constructor: {
value: B,
enumerable: false,
writable: true,
configurable: true
}
});

现在我们要实现静态成员的继承:

1
Object.setPrototypeOf(B, A);

目前为止,我们定义的是两个空类,现在我们为他们增加 name 属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
function A() {}
function B() {}

A.prototype = Object.create(null, {
constructor: {
value: A,
enumerable: false,
writable: true,
configurable: true
},
name: {
get: function() {
return 'A';
},
enumerable: true,
configurable: true
}
});

B.prototype = Object.create(A.prototype, {
constructor: {
value: B,
enumerable: false,
writable: true,
configurable: true
},
name: {
get: function() {
return 'B';
},
enumerable: true,
configurable: true
}
});

Object.setPrototypeOf(B, A);

console.log(new A().name); // => A
console.log(new B().name); // => B

现在我们要在 B 的 name 中获取父类的 name 属性,那么我们就需要找到 B.prototype 中的 name。

在 B 环境中,灵活的做法是通过 B.prototype 找到 A.prototype,根据 *B.prototype = Object.create(A.prototype)*,我们知道:

1
A.prototype === Object.getPrototypeOf(B.prototype)

于是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
function A() {}
function B() {}

A.prototype = Object.create(null, {
constructor: {
value: A,
enumerable: false,
writable: true,
configurable: true
},
name: {
get: function() {
return 'A';
},
enumerable: true,
configurable: true
}
});

B.prototype = Object.create(A.prototype, {
constructor: {
value: B,
enumerable: false,
writable: true,
configurable: true
},
name: {
get: function() {
var sup = Object.getPrototypeOf(B.prototype);
var desc = Object.getOwnPropertyDescriptor(sup, 'name');
return desc.get.call(this) + 'B';
},
enumerable: true,
configurable: true
}
});

Object.setPrototypeOf(B, A);

console.log(new A().name); // => A
console.log(new B().name); // => AB

这就实现了 super.name 的效果。当然,我们假设 B 的父类是定义了 name 的,如果没有找到 name,那应该沿着原型链继续向上寻找。因此更健壮的写法是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
function A() {}
function B() {}

A.prototype = Object.create(null, {
constructor: {
value: A,
enumerable: false,
writable: true,
configurable: true
},
name: {
get: function() {
return 'A';
},
enumerable: true,
configurable: true
}
});

B.prototype = Object.create(A.prototype, {
constructor: {
value: B,
enumerable: false,
writable: true,
configurable: true
},
name: {
get: function() {
var sup = B.prototype, desc;
do {
sup = Object.getPrototypeOf(sup);
if (!sup) {
break;
}
desc = Object.getOwnPropertyDescriptor(sup, 'name');
} while(!desc);

return (desc ? desc.get.call(this) : undefined) + 'B';
},
enumerable: true,
configurable: true
}
});

Object.setPrototypeOf(B, A);

console.log(new A().name); // => A
console.log(new B().name); // => AB

这里没有考虑 name 不是 getter 的情形,不过原理类似,不再冗述。

最后我们来实现在构造方法中调用 super()。显然,实质是在 B 函数中找到函数 A,根据 *Object.setPrototypeOf(B, A)*,我们有:

1
2
3
function B() {
Object.getPrototypeOf(B).call(this)
}

我们为 A 添加一个 age 构造参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
function A(age) {
this.age = age;
}
function B(age) {
Object.getPrototypeOf(B).call(this, age);
}

A.prototype = Object.create(null, {
constructor: {
value: A,
enumerable: false,
writable: true,
configurable: true
},
name: {
get: function() {
return 'A';
},
enumerable: true,
configurable: true
}
});

B.prototype = Object.create(A.prototype, {
constructor: {
value: B,
enumerable: false,
writable: true,
configurable: true
},
name: {
get: function() {
var sup = B.prototype, desc;
do {
sup = Object.getPrototypeOf(sup);
if (!sup) {
break;
}
desc = Object.getOwnPropertyDescriptor(sup, 'name');
} while(!desc);

return (desc ? desc.get.call(this) : undefined) + 'B';
},
enumerable: true,
configurable: true
}
});

Object.setPrototypeOf(B, A);

console.log(new B(18).age); // => 18

于是,一个基本的继承的手写版本就实现了,当然,babel 还考虑了更多细节,比如构造方法有返回值的情况等等,但基本的继承原理就是这个样子。

下图很好地反应了诸多对象的关系。值得一提的是, b 也可能成为下一级类的 prototype,你可以清晰地看到通过 __proto__ 搜索原型链的轨迹。

js-inherit


在现代浏览器的不断迭代下,已经有大部分版本都实现了对 class 关键字和继承的原生支持,相信在不久的将来,就无需在这么麻烦地实现继承了。无论如何,JavaScript 继承的原理仍然是不变的,上面这些内容有助于理解原型链是怎么样工作的。

开头一个问题是:什么是 Plain Object

并没有看到有官方去专门定义它,更可能它只不过是业界的一种通俗叫法,因此也没有严格的定义。但我们在汉语环境里通常叫它“纯对象”。

业界解释:https://www.quora.com/What-is-a-plainObject-in-JavaScript

下面我们来看一下常见 Library 对 isPlainObject 函数的实现。

jQuery

jQuery 3.3 版本中的 isPlainObject 定义在这里

为便于阅读,核心代码经过整理后如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function isPlainObject(obj) {
var proto, Ctor;

// (1) null 肯定不是 Plain Object
// (2) 使用 Object.property.toString 排除部分宿主对象,比如 window、navigator、global
if (!obj || ({}).toString.call(obj) !== "[object Object]") {
return false;
}

proto = Object.getPrototypeOf(obj);

// 只有从用 {} 字面量和 new Object 构造的对象,它的原型链才是 null
if (!proto) {
return true;
}

// (1) 如果 constructor 是对象的一个自有属性,则 Ctor 为 true,函数最后返回 false
// (2) Function.prototype.toString 无法自定义,以此来判断是同一个内置函数
Ctor = ({}).hasOwnProperty.call(proto, "constructor") && proto.constructor;
return typeof Ctor === "function" && Function.prototype.toString.call(Ctor) === Function.prototype.toString.call(Object);
}

lodash

lodash 4.0.6 版本中的 isPlainObject 定义在这里

基本与 jQuery 版本相同,多了一个 Ctor instanceof Ctor 的条件,满足此条件的仅有 FunctionObject 两个函数。

1
2
3
4
5
6
7
8
9
10
11
function isPlainObject(value) {
if (!value || typeof value !== 'object' || ({}).toString.call(value) != '[object Object]' ) {
return false;
}
var proto = Object.getPrototypeOf(value);
if (proto === null) {
return true;
}
var Ctor = hasOwnProperty.call(proto, 'constructor') && proto.constructor;
return typeof Ctor == 'function' && Ctor instanceof Ctor && Function.prototype.toString.call(Ctor) === Function.prototype.toString.call(Object);
}

redux

redux 从 4.0.0 开始在测试中使用了 isPlainObject,代码在这里

它的实现比较简单。

1
2
3
4
5
6
7
8
9
10
function isPlainObject(obj) {
if (typeof obj !== 'object' || obj === null) return false

let proto = obj
while (Object.getPrototypeOf(proto) !== null) {
proto = Object.getPrototypeOf(proto)
}
// proto = null
return Object.getPrototypeOf(obj) === proto
}

我们并没有一个能判断 Plain Object 的清晰逻辑,大概能理出来的思路是:

  1. 先判断 obj 本身是否满足我们熟悉的合法对象概念;
  2. 判断 obj 的构造函数是不是 Object

至于判断 prototype 是不是 null,无非是一种 shortcut 罢了。

已经不关注社区有大半年了,或者起码有一年了,毕竟被持续增长的业务缠了也有一年了,虽然很“充实”,但毫无疑问地说,也是一无所获。

因为这一年来始终在 React Naitve 体系上工作,传统 Web 的知识也忘得大半了。还记得去年专门写过一篇文章来总结2016年前端社区的现状,一年过去了,如果我不是特别孤陋寡闻的话,那么,我认为,现状并没有改变多少。

框架

React

React 经历了一次许可证的“惊心动魄”,虽然新版本的许可证已经是安全的,但有一定研发实力的公司都已经在造自己的轮子。当然,从开发者的角度,肯定是使用成熟的方案更舒服,显然近在咫尺的理想也不见得能容易实现。

React 16 启用了新的引擎,这些细节是值得玩味的。

除此之外,React 依然拥有大量的开发者用户,就像去年一样。

Angular

未曾想到的是,Angular 2 还立足未稳,Angular 4 就出现了。刚刚看到一篇文章,作者抱怨自己的项目使用了 Angular 2 的 beta 开发之后,暂且不表 TypeScript 语法的绑定,光是要快速 follow 短时间内几十个版本的升级就已经相当心累了。

当然你可以说这位开发者在一开始就不应该使用 beta 版本,但是要等到正式版本发布的话,究竟是什么时候呢?我还记得 Angular 2 的核心包去掉了 beta 后缀的时候,其官方维护的一众功能包,比如动画等等,依旧是在 alpha/beta 状态。好吧,即使等到所有包都达到了起码的 stable 状态,Angular 4 的出现又是几个意思呢?

可以预见,Angular 1 时代的用户,相当一部分会放弃继续使用 Angular 的升级版本。曾经也一味推崇 Angular 2 的我,也渐渐失去了耐心。说到底,还是 Angular 的开发团队自己玩死的。

Vue

其实去年的这个时候,我是比较看衰 Vue 的,语法不喜欢、个人维护难以为继,和 React、Angular 甚至 Ember 完全没法比。

不过 Vue.js 的流行程度经过这一年来,毫无减弱的趋势,我想至少有一下几方面原因:

  1. 稳定,用户不必在业务迭代的同时,额外烦恼于持续升级基础框架;
  2. 简单,上手容易,具有普通开发经验的人,也可以在十分钟内写出一个较为复杂的应用;
  3. 生态工具较为丰富,特别是重要工具由作者维护,质量有保证且唯一,不用担心选择恐惧症

说白了,对于真正的业务开发环境,稳定和简单是特别重要的两个特性,不求能有多激进的特性,但求省心。

另外,Vue 的 ssr 方面也有足够的能力和性能支持,这在今天的 web 应用中,还是比较有吸引力的。React 生态也仅有一个简单的 next.js 还算勉强能用,而 Angular 和 Ember 基本都属于残次品。

Ember

Ember 属于比较小众的框架,我已经关注它有两年了,毕竟推广不多,很多人不知道,社区发展也有限。过去这一年依旧表现平平。

React-Native

如果说去年上半年我起码还在 React Native 上积累了点经验的话,那么下半年基本就达到了瓶颈了。许可证是一方面,公司环境是另一方面,但这都不重要。重要的是,我依旧不看好这个东西。

“行百里者半九十”,我们已经写了成千上万行代码,过亿的用户,以及良好的开发上线流程,但依旧避免不了要时刻面对框架底层缺陷带给我们的额外工作量与业务损失。作为前端开发者,过去面对浏览器兼容性起码还有前辈多年的经验积累,但 React Native 带给我们的就是大大提升了自身的技术钻研能力。

工程化

Webpack

Webpack 也从 2 升级到了3,但变化俨然远远不如从 1 到 2。使用配置难度依旧居高不下,想我这种上了年纪的人,没得商量,每次使用都要再看一遍 doc。

Rollup

Rollup 的定位十分明确,与 Webpack 不构成竞争关系。写 library 用 Rollup,写 app 用 Webpack。

npm

jspmbower 几乎已成骨灰,yarn 依旧不支持 scope。

新方向

webassembly

在下一代人上小学之前能看到定稿的 webassembly 我就已经满足了。

webcomponents

在下一代人上小学之前能看到普及的 webcomponents 我就已经满足了。

数据

断断续续学习 D3,但明显精力不如从前。

微信小程序

如我所料,半温不火。令人鄙视的是,支付宝和百度都还在模仿,简直了。

我的工作

去年我还在研究一种解决方案,能够兼顾的开发效率、运行时性能这两个看似简单却互为矛盾的指标。最后确实有了一个雏形。今年,我已经不再感兴趣,没有别的,就一个原因,想的太多,跟本没有那么复杂的使用场景。

总体来看过去一年,对于个人来讲是毫无建树的,今年上半年,我可能需要拓展一下视野,找一找我不会做的事情做一做。

Unicode 是字符集(charset),UTF-8、UTF-16 只是 Unicode 的编码规则(encoding)。

标准的 UTF-8 对 Unicode 分为4种变长的编码格式。

区间 字节数 编码格式
0~0x7F 1 0xxxxxxx
0x80~0x7FF 2 110xxxxx 10xxxxxx
0x800~0xFFFF 3 1110xxxx 10xxxxxx 10xxxxxx
0x10000~0x10FFFF 4 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx

也就是说,对于英文字母这种ASCII字符,UTF-8与ASCII是兼容的,都采用1字节存储。

对于希伯来语字母 א,UTF-8采用两个字节存储,方法是把它的 Unicode 码 0x05d0 的二进制形式拆成两部分,后6位前补10,再前数5位前补110,拼到一起(余下全为0),即为 0xd790。

0x05D0 = b0000010111   010000
            11010111 10010000 = 0xD790

同样道理,汉字“我”采用3字节,Unicode 码拆成3部分:

0x6211 = b0110   001000   010001
      11100110 10001000 10010001 = 0xE68891

总结来看,字节起始为0则为单字节字符,起始为110、1110、11110分别代表有2个、3个、4个字节组合代表一个字符。其它字节起始必然全为10,代表不是字符起始位置。

断断续续用了半年时间才读完了《咨询的奥秘》(今天一口气读了好几章),深感作为大牛咨询师,作者深谙计算机职场的人性,其中众多“法则”的总结无不精辟入骨,虽然鄙人不懂咨询,但从咨询的眼光和角度去看职场,不免有种“不识庐山真面目,只缘身在此山中”的彻悟。

草草读过一遍甚感粗糙,因此决定第二遍细细研读。下面我努力每一章都抽出时间写下读后感。

忽然发现,此书英文原版第一版竟出版于1986年,而内容时至今日依旧不过时。

第一章 咨询为什么这么难

咨询第一定律——不管客户和你说什么,问题总会有

是的,任何系统和设计总会有或多或少的问题,可能是无足轻重的功能补充,也可能是深藏不露的致命缺陷,或大或小,但总会有。

因为没有事物是完美的,所以缺陷的存在是必然的。每当有人向我滔滔不绝讲了一大堆关于他的想法,我都习惯性的点头表示“同意”,并很少打断。如果如此结束谈话,自然相安无事,我最担心的是他补上一句——“有没有什么问题?”。

问题总会有,一旦我一时提不出来任何问题,我知道,我一定是没有理解他的观点,他的话被我当做了耳旁风。

在面试新员工的时候,我也必然地在最后问对方有没有什么问题问我,往往我得到的答案是没有。在这个角度上,我并不喜欢没人任何问题的求职者。

在会议的末尾,主持人经常询问大家有没有问题,绝大多数情况下,会议室是沉默的。

显然问题的提出有助于消除歧义,增进理解,提高沟通效率;没有问题可能是不负责任、拖延症、懒惰的表现。

虽然别人的问题会一直存在,但不必指出得过于强烈,要照顾自尊,因此:

永远不要承诺百分之十以上的改进

这就相当于指出对方存在“很大”的问题,人非圣贤,这样指出问题无疑会招致对方强烈的反感。

如果出现意外:

如果不小心让改进超过百分之十,要确保没人注意到它

处心积虑,为对方留足面子,处世之道。

咨询第二定律——不管一开始看起来什么样,它永远是人的问题

诸多系统的异常在开始往往都被归咎于异常的客观事实,这种偏见非常地常见,人们在潜意识里往往不相信自己出了错。

另一种理解是,无论是哪个环节出了问题,都可以归咎于人的管理失职,毕竟只有人才有主观能动性。一般来讲,人们对于难以预防的灾害,最多能够原谅一次,从第二次开始,都可以归咎于负责人对于经验总结的失察,于是:

马文定律——不管客户在做什么,都要建议他们做些别的

抛开咨询师和客户的关系不谈,由于当局者迷,当事人往往会深陷思维定势,不妨换个角度思考,很多时候只是睡一觉、喝杯茶,问题就迎刃而解。

咨询第三定律——永远别忘了客户是按小时付费,而不是按照解决方案付费的

这一句很容易被误解,它实际上是对人性的极其深刻的讽刺——我只要向老板表态我已经在解决问题了,这是最重要的,至于最终有没有解决——原本就不可能被解决啊,否则依我的能力,早就解决了!

这也导出了另一个推论:

你要是在意功劳记在谁头上,那就啥事也干不成了

人们需要保留一种底线的面子,不能做出证明他一无是处的事情,谁都不想承认自己犯有严重的错误。

咨询第四定律——要是他们没聘用你,不要帮他们解决问题

很容易理解,没主动问你,你就别瞎BB,招人烦。

树莓酱定律——铺的越广,摊得越薄

精力有限,战线不能拉得太长,顾此失彼。这也是信息守恒的一种体现。

温伯格双胞胎定律——大部分时间,在世界上大多数地方,不管人们有多努力,都不会发生什么大事

这印证了一句话,“努力了不一定成功,不努力一定不成功”,似乎,即使努力,成功的几率也是相当低的,很令人沮丧。但这是自然规律,熵增的趋势是极其平缓的。

书中有一句话很经典:对于世界上大多数系统而言,对于其下一时刻行为的最佳预测,就是和上一时刻做同样的事情

在互联网这种信息高速传播的时代,常常会有新事物出现,往往冠以“革命性”的名头。然经过详细了解后就可以发现,大多数都只是旧瓶装新酒,仅仅对旧事物做了一定的改进,暂且不关心改进是否会带来其它的副作用,微小改进就难以称之为任何“革命”。遇到这种新事物的时候,先不要盲目追宠,相信温伯格双胞胎定律。

鲁迪黄萝卜理论——一旦你干掉了头号问题,二号问题就升级了

问题是没完没了的,队列不空,多次出栈后仍有队头。


所以,“咨询为什么这么难”?世界上的问题是无穷无尽的,而人们又不愿意被指出自己有问题,为了照顾人家的情绪,既不能改进过多,又不能包揽全部功劳。即便如此,纵然你再努力,能够做出实质性改变的机会仍然不大。作者在最后抛出了做咨询的三条困难定律:

  • 要是不能接受失败,做顾问就永远不会成功
  • 一旦消灭了头号问题,二号问题就升级了
  • 帮助自己要比帮助别人更难

自从去年11月中旬在公司内部转岗后,我就一直从事着React-Native业务研发工作,算起来也有大半年了,在这项技术的应用、运维上也算是有了点经验。

往往在产品上应用一项新技术的KickOff上,都必然要涉及该项技术的明显领先优势,React-Native 给人的一贯明显优势无非是:

  1. 开发效率提升
  2. 与Native一致的用户体验
  3. 无需发版,随时上线

那么如果工程做得好的话,也要必然提到它的劣势——不够稳定

我以前向来是个React-Native黑,我不认为它的稳定性足以支撑一项C端产品,更何况是亿级用户产品。

因此,当初做React-Native算是有些被迫的,毕竟万事俱备只欠开发,毕竟要用事实说话,毕竟挣钱吃饭重要。


初期的开发工作可谓极其艰苦,不但没有开发经验,而且基础调试设施不完善,效率很低,更何况连实体测试机都不够。但最终总算是上线了。

另我十分惊讶的是,不稳定性带来的客户端Crash比预想中的要低得多得多。在平台的横向比较上,Android要比iOS的奔溃率高,但还不算很离奇,勉强可以忍受。

我从来不会想到这么一个残缺不堪的东西竟然真的没有掉链子,由此我反思了一下,想起了我党在40年前的那句话:

实践是检验真理的唯一标准

实践是检验真理的唯一标准

是的,不能因为外界的评论甚至是自己的无知,就对新事物抱有成见。除非自己亲自实践,否则无权瞎BB。

经过这么一来,我对手头上的这个项目信心大增,业务的迭代速度即将迎来一次飞跃。

然,下面才是冷静之后的现实,更说明了海量事实的总体分布就像那宇宙微波背景辐射一样的均匀各项同性。

宇宙微波背景辐射

线上追踪问题困难

React 异步渲染的性质,导致了在渲染视图中出现crash,崩溃堆栈是不会追溯到你的业务代码中的。

crash stack

在这种情形下,代码被压缩根本就不是问题,问题是你完全不知道哪里出了故障,只能靠猜。

年初我就强行猜了一次,幸运地解决了一个bug。

然后如果说线上问题毕竟不在现场,不好追踪的话,那么如果同样的问题发生在了调试阶段,你能看到的只是样式错乱的页面,也不会报给你任何错误信息,就很难让人接受了。

因此,【】是开发React-Native的一项重要技能。

石头剪子布

Android 无头问题居多

这依然算是固有的稳定性问题。此类问题有两个特点:

  1. 偶发不可复现;
  2. 逻辑上不可能

不可复现的原因很繁杂,可能与设备型号、操作系统版本、软件版本、当前运存容量、用户操作路径都有关系。还有就是一些原则上根本不可能发生的错误。

这些问题由于迟迟无法解决,长时间耽误了我们全量的进程。最后属于不了了之,算是向bug妥协了。

性能差

相信我,KickOff 中“与Native具有相同或相近的性能”的说辞是不成立的,特别是在Android上。

为了提升性能,我们不得不把能耗高的部分交给Native去实现,但这样显然又减弱了效率的优势。

兼容客户端版本

如果说Web可以不考虑浏览器环境,做到始终如一的版本发布,没人会有异议,但React-Native不行。

React-Native是运行在客户端上的,它的行为和格调都必须与当前客户端的环境相匹配。

这意味着,一套React-Native代码必须考虑到多个客户端环境。特别是在视觉大改版的情况下,React-Native融入两套视觉代码,是什么感觉你造吗?

根据版本号的if-else逻辑目前已充斥相当一部分逻辑代码中,代码越来越不够优雅。

文本渲染不准确

我们已经有充分的证据证明React-Native在渲染文本特别是多行文本时存在明显的bug。这导致我们不能按照设计的预期严格的排版。

此问题很深奥,而且以目前的能力来看,基本无解。

坑多多

这些坑不一定是不可解决的,其实大部分我们都可以通过规避来解决,但问题是如果不进行全网搜索,你很难提前预知风险,也就严重影响了效率,往往一个奇葩的问题能耽误你一天的时间。

https://github.com/super-fe/superfe-rn-troubleshooting/issues


总结来看,应用React-Native技术带来的收益并非是特别明显的,在我看来,它最大的贡献是把一拨做业务的人悄无声息地分成了两拨,显然迭代效率就上来了。

客观上,你不能无视上面提到的各种问题,但这些问题说严重就严重,说不严重也不严重,总体看来是可勉强接受的,相对于传统的客户端开发方式,依然是各向同性的

从最初的腹黑到今天的淡然面对,深深感到世界系统的复杂性以及熵增的强大扭矩,革命性没那么容易,起码React-Native不是。

本文首发于 http://www.infoq.com/cn/articles/2016-review-frontend,此处为原稿。

2016年马上过去了,像过去六年中的每一年一样,Web前端领域又产生了“面目全非”而又“耳目一新”的变化,不但旧事物持续不断地被淘汰,新事物也难保坐久江山,大有岌岌可危之势。开源界如群雄逐鹿,不断生产新的概念、新的框架、新的工具,去年中一些流行的技术今年大多得到了进一步的演进和升级,活跃度非常高,却仍然不能保证前端的未来属于它们。在今年整体资本市场冷却的大环境下,to B业务的创业公司显现出了较强的生命力,这种类型的业务也给Web前端的工作带来了明显的差异性,工程师整体技能方向也展露出一丝不一样的分支。本文将从下至上、由低到高的维度盘点过去一年中Web前端领域发生的重要事件以及影响未来2017的关键性因素。视野所限,不尽完整。

一、更新的网络与软件环境

1.1 HTTP/2 的持续普及

今年中,几乎所有的现代桌面浏览器都已经支持了HTTP/2协议,移动端依靠降级为Spdy依旧可以覆盖几乎所有平台,这样使得从协议上优化页面的性能成为了可能。

同时,前端静态资源打包的必要性成为了一定程度上的争论焦点,打包合并作为传统的前端性能优化方案,它的存留对前端工程化影响极大,Facebook公司著名的静态资源动态打包方案的优越性也会被弱化。社区上多篇文章纷纷发表对HTTP/2的性能实验数据,却不仅相同。

在2017年,我们相信所有大型站点都会切换HTTP/2,但依旧不会放弃对静态资源打包合并的依赖。而且。对于Server Push等高级特性,也不会有太多的应用。

1.2 Internet Explorer 8

三年前还在考虑兼容IE6的前端技术社区,在前不久天猫宣布不再支持IE8后又引起了一股躁动。IE8是Windows XP操作系统支持的最高IE版本,放弃IE8意味着放弃了使用IE的所有XP用户。

其实在2016年的今天,前端社区中框架、工具的发展早已不允许IE8的存在,angular 早在1.3版本就果断放弃了IE8,React 也在年初的v15版本上宣布放弃。在PC领域,你依旧可以使用像Backbone一样的其它框架继续对IE进行支持,但无论是从研发效率上还是从运行时效率上,放弃它都是更好的选择。

由于对HTML5兼容性不佳,在2017年,相信IE9也会逐渐被社区放弃,以取得更好的性能、更少的代码体积。

二、如何编写(Java)Script

2.1 ES2016?ES2017?babel!

去年定稿的ES2015(亦称ES6)带来了大量令人激动的新语言特性,并快速被V8和SpiderMonkey所实现。但由于浏览器版本碎片化问题,目前编写生产环境代码仍然以ES5为主。今年年中发布的ES2017带来的新特性数量少的可怜,但这正好给了浏览器厂商消化ES2015的时间,在ES2017到来之前喘口气——是的,明年的ES2017势必又会带来一大波新特性。

JS解释引擎对新特性的支持程度并不能阻碍狂热的开发者使用他们,在接下来的很长时间,业界对babel的依赖必然有增无减。babel生态对下一代ECMAScript的影响会进一步加大,人们通过先增加新的babel-plugin,后向ECMA提案的方式成为了ECMAScript进化的常态。开发者编写的代码能直接运行在浏览器上的会越来越少。

但使用babel导致的编译后代码体积增大的问题并没有被特别关注,由于polyfill可能被重复引入,部署到生产环境的代码带有相当一部分冗余。

2.2 TypeScript

作为ECMAScript语言的超集,TypeScript在今年取得了优异的成绩,angular 2放弃了传说中的AtScript,成为了TypeScript的最大客户。人们可以像编写Java一样编写JavaScript,有效提升了代码的表述性和类型安全性。

但凡事有两面,TypeScript的特快也在不断升级,在生产环境中,你可能需要一套规范来约束开发者,防止滥用导致的不兼容,这反而增加了学习成本、应用复杂性和升级安全性。个中优劣,仍需有大量的工程实践去积累经验。

此外,TypeScript也可以看做一种转译器,与babel有着类似的新特性支持。在2017年,我们期待TypeScript与babel会发展成怎样的一种微妙关系。

2.3 promise、generator 与 async/await

在回调地狱问题上,近两年我们不断被新的方案乱花了眼。过去我们会利用async来简化异步流的设计,直到“正房”Promise的到来。但它们只是callback模式的语法糖,并没有完全消除callback的使用。

ES2015带来的generator/yield似乎成为了解决异步编程的一大法宝,虽然它并非为解决异步编程所设计的。但generaor的运行是十分繁琐的,因此另一个工具co又成为了使用generator的必备之选。Node.js社区的koa框架初始就设计为使用generator编写洋葱皮一样的控制流。

但昙花一现,转眼间async/await的语法,配合Promise编写异步代码的方式立即席卷整个前端社区,虽然async/await仍然在ES2017的草案中,但在今天,不写async/await立刻显得你的设计落后社区平均水平一大截。

在Node.js上,v7已经支持在harmony参数下的async/await直接解释,在明年4月份的v8中,将会正式支持,届时,koa 2的正式版也会发布,几乎完全摒弃了generator。

2.4 fetch

受到回调问题的影响,传统的XMLHttpRequest有被fetch API 取代之势。如今,成熟的polyfill如whatwg-fetchnode-fetchisomorphic-fetch在npm上的每日下载量都非常大,即便对于兼容性不好的移动端,开发者也不愿使用繁琐的Ajax。借助async/await的语法,使用fetch API能让代码更简洁。

三、node.js服务与工具

3.1 Koa 2

Koa与流行的Express属于“同根生”的关系,它们由同一团队打造。相比Express,新的Koa框架更轻量、更灵活。但Koa的设计在短时间内曾经出现了较大的变动,这主要受到了async/await语法对异步编程的影响。在v2版本中,koa的middleware抛弃generator转而支持async,所有第三方middleware实现,要么自行升级,要么使用koa-convert进行包装转换。

目前koa在node.js社区的HTTP服务端框架中受到关注度比较高,不过其在npm上latest目前仍处于1.x阶段,预计在2017年4月份发布node.js v8后,就会升级到2.x。

Koa的轻量级设计意味着你需要大量第三方中间件去实现一个完整的web应用,目前鲜有看到对koa的大规模重度使用,因此也就对其无从评价。相信在明年,越来越多的产品应该会尝试部署koa 2,届时,对第三方资源的依赖冲突也会尖锐起来,这需要一个过程才能让koa的生态完备起来。预计在2018年,我们会得到一个足够健壮的koa技术栈。这会促进node.js在服务端领域的扩展,轻量级的web服务将会逐渐成为市场上的主流。

四、框架纷争

4.1 jQuery已死?

今年六月份jQuery发布了3.0版本,距离2.0发布已经有三年多的时间,但重大的更新几乎没有。由于老旧浏览器的逐渐放弃和升级,jQuery需要处理的浏览器兼容性问题越来越少,专注于API易用性和效率越来越多。

随着如angular、react、ember、vue等大量具备视图数据单双向绑定能力的框架被普及,使用jQuery编写指令式的代码操作DOM的人越来越少。早在2015年便有人声称jQuery已死,社区中也进行了大量雷同的讨论,今天我们看到确实jQuery的地位已大不如前,著名的sizzle选择器在今天已完全可由*querySelector**原生方法替代,操作DOM也可以由框架根据数据的变动自动完成。

明年jQuery在构建大型前端产品的过程中的依赖会被持续弱化,但其对浏览器特性的理解和积淀将对现有的和未来的类angular的MVVM框架的开发依旧具有很大的借鉴意义。

4.2 angular 2

好事多磨,angular 2的正式版终于在今年下半年发布,相比于1.x,新的版本几乎是完全重新开发的框架,已经很难从设计中找到1.x的影子。陡峭的学习曲线也随之而来,npm、ES2015 Modules、decorator、TypeScript、zone.js、rxjs、JIT/AOT、e2e test,几乎都是业界这两年中的最新概念,这着实给初学者带来了不小的困难。

angular 2也更面向于开发单页应用(SPA),这是对ES2015 Modules语法描述的模块进行打包(bundle)的必然结果,因此angular 2也更依赖于webpack等“bundler”工具。

虽然angular 声称支持TypeScript、ECMAScript和Dart三种语言,不过显然业界对Dart没什么太大兴趣,而对于ECMAScript和TypeScript,两种语言模式下angular 2在API和构建流程上都有着隐式的(文档标注不明的)差异化,这必然会给开发者以困扰。加上业界第三方工具和组件的支持有限,TypeScript几乎是现在开发者唯一的选择。

此外,angular团队已声明并没有完全放弃对1.x组件的支持,通过特有的兼容API,你可以在2.x中使用针对1.x开发的组件。鉴于不明确的风险,相信很少有团队愿意这样折腾。

现在在产品中使用angular 2,在架构上,你需要考虑生产环境和开发环境下两种完全不同的构建模式,也就是JIT和AOT,这需要你有两套不一样的编译流程和配置文件。在不同环境下模块是否符合期望,可以用e2e、spec等方式来进行自动化测试,好的,那么angular 2的测试API又可能成了技术壁垒,它的复杂度可能更甚angular本身。可以确信,在业务压力的迫使下,绝大部分团队都会放弃编写测试。

总之angular 2是一个非常具有竞争力的框架,其设计非常具有前瞻性,但也由于太过复杂,很多特性都会成为鸡肋,被开发者所无视。由于react*和vue的竞争,angular 2对社区的影响肯定不如其前辈1.x版本,且其更高级的特性如Server Render还没有被工程化实践,因此相信业界还会持续观望,甚至要等到下一个4.x版本的发布。

4.3 vue 2.0

vue绝对是类MVVM框架中的一匹黑马,由作者一人打造,更可贵的是作者还是华人。vue在社区内的影响非常之大,特别是2.0的发布,社区快速生产出了无数基于vue的解决方案,这主要还是受益于其简单的接口API和友好的文档。可见作为提供商,产品的简单易用性显得尤为重要。在性能上,vue基于ES5 setter,得到了比angular 1.x脏检查机制成倍的性能提升。而2.0在模块化上又更进一步,开发难度更低,维护性更好。可以说vue准确地戳中了普通web开发者的痛点。在国内,vue与weex达成了合作,期待能给社区带来怎样的惊喜。

4.4 react

目前看来,react似乎仍是今年最流行的数据视图层解决方案,并且几乎已经成为了每名前端工程师的标配技能。今年react除了版本从0.14直接跃升至15,放弃了IE8以外,并没有更多爆发式的发展。人们对于使用JSX语法编写web应用已经习以为常,就像过去十年间写jQuery一样。

react的代码在维护性能上显而易见,如果JSX编写得当,在重渲染性能上也具备优势,但如果只部署在浏览器环境中,那么首屏性能将会受到负面影响,毕竟在现阶段,纯前端渲染仍然快不过后端渲染,况且后端具备天生的chunked分段输出优势。我们在业界中可以看到一些负面的案例,比如某新闻应用利用react全部改写的case,就是对react的一种误用,完全不顾其场景劣势。

围绕着react发展的替代品和配套工具依旧很活跃,preact以完全兼容的API和小巧的体积为卖点,inferno以更快的速度为卖点,等等。每个框架都想在Virtual DOM上有所创新,但它们的提升都不是革命性的,由此而带来的第三方插件不兼容性,这种风险是开发者不愿承担的,笔者认为它们最大的意义在于能为react的内部实现提供另外的思路。就像在自然界,生物多样性是十分必要的,杂交能带来珍贵的进化优势。

4.5 react-native

今年是react-native(一下简称RN)支持双端开发的第一年,不断有团队分享了自己在RN上的实践成果,似乎前途一片大好,RN确实有效解决了传统客户端受限于发版周期、H5受限于性能的难题,做到了鱼和熊掌兼得的理想目标。

但我们仍然需要质疑:首先,RN目前以两周为周期发布新版本,没有LTS,每个版本向前不兼容。也就是说,你使用0.39.0的版本编写bundle代码,想运行在0.35.0的runtime上,这几乎会100%出问题。在这种情况下,如何制定客户端上RN的升级策略?如果升级,那么业务上如何针对一个以上的runtime版本编写代码?如果不升级,那么这意味着你需要自己维护一个LTS。要知道目前每个RN的版本都会有针对前版本的bug fix,相信没有团队有精力可以在一个老版本上同步这些,如果不能,那业务端面对的将是一个始终存在bug的runtime,其开发心理压力可想而知。

其次,虽然RN声称支持Android与iOS双端,但在实践中却存在了极多系统差异性,有些体现在了RN文档中,有一些则体现在了issue中,包括其它一些问题,github上RN的近700个issue足以让人望而却步。如果不能高效处理开发中遇到的各种匪夷所思的问题,那么工期就会出现严重风险。此外,RN在Android和iOS上的性能也不尽相同,Android上更差一些,即便你完成了整个业务功能,却还要在性能优化上消耗精力。并且无论如何优化,单线程模型既要实现流畅的转场动画,又要操作一系列数据,需要很高的技巧才能保证可观的性能表现。在具体的实践中,对于H5,往往由于时间关系,业务上先会上一个还算过得去的版本,过后再启动性能优化。然而对于RN,很有可能达到“过得去”的标准都需要大量的重构工作。

再次,RN虽然以Native渲染元素,但毕竟是运行在JavaScript Core内核之上,依旧是单线程,相对于H5这并没有对性能有革命性质的提升。Animated动画、大ListView滚动都是老生常谈的性能瓶颈,为了解决一些复杂组件所引起的性能和兼容性问题,诸多团队纷纷发挥主动能动性,自己建设基于Native的高性能组件,这有两方面问题,一是不利于分发共享,因为它严重依赖特定的客户端环境,二是它仍依赖客户端发版,仍需要客户端的开发,违背了RN最最重要的初衷。可以想象,在大量频繁引用Native组件后,RN又退化成了H5+Hybrid模式,其UI的高性能优势将会在设备性能不断升级下被削弱,同时其无stable版本反而给开发带来了更多不可预测的风险变量。

最后,RN仍然难以调试和测试,特别是依赖了特定端上组件之后,本地的自动化测试几乎成为了不可能,而绝大多数客户端根本不支持自动化测试。而调试只能利用remote debugger有限的能力,在性能分析上都十分不便。

可以说RN的出现带给了移动开发以独特的新视角,使得利用JavaScript开发Native成为了可能,NativeScript、Weex等类似的解决方案也发展开来。显然RN目前最大的问题仍然是不够成熟和稳定,利用RN替代Native依然存在着诸多风险,这对于重量级的、长期维护的客户端产品可能并不是特别适合,比如Facebook自己。RN的优势显而易见,但其问题也是严重的,需要决策者对个方面利弊都有所了解,毕竟这种试错的成本不算小。

由于时间关系,市场上并没有一个产品在RN的应用上有着足够久的实践经验,大部分依然属于“我们把RN部署到客户端了”的阶段,我们也无法预测这门技术的长久表现,现在评价RN的最终价值还为时尚早。在2017年,期待RN团队能做出更长足的进步,但不要太乐观,以目前的状态来看,想达到stable状态还是有着相当大的难度。

4.6 redux 与 mobx

redux 成功成为了 react 技术栈中的最重要成员之一。与vue一样,redux也是凭借着比其它Flux框架更简单易懂的API才能脱颖而出。不过已经很快有人开发厌烦它每写一个应用都要定义action、reducer、store以及一大堆函数式调用的繁琐做法了。

mobx也是基于ES5 setter,让开发者可以不用主动调用action函数就可以触发视图刷新,它只需要一个store对象以及几个decorator就能完成配置,确实比redux简单得多。

在数据到视图同步上,无论使用什么样的框架,都有一个至关重要的问题是需要开发者自己操心,那就是在众多数据变动的情形下,如何保证视图以最少的但合理的频率去刷新,以节省极其敏感的性能消耗。在redux或mobx上都会出现这个问题,而mobx尤甚。为了配合提升视图的性能,你依然需要引入action、transaction等高级概念。在控制流与视图分离的架构中,这是开发者无可避免的关注点,而对于angular、vue,框架会帮你做很多事情,开发者需要考虑的自然少了许多。

4.7 bootstrap 4

bootstrap 4处于alpha阶段已经非常久了,即使现在3.x已经停止了维护,它似乎受到了twitter公司业务不景气的影响,github上的issue还非常多。bootstrap是建设内部平台最佳的CSS框架,特别是对于那些对前端不甚了解的后端工程师。我们不清楚bootstrap还能坚持多久,如果twitter不得不放弃它,最好的归宿可能是把它交给第三方开源社区去维护。

五、工程化与架构

5.1 rollup 与 webpack 2

rollup是近一年兴起的又一打包工具,其最大卖点是可以对ES2015 Modules的模块直接打包,以及引入了Tree-Shaking算法。通过引入babel-loader,webpack一样可以对ES2015 Modules进行打包,于是rollup的亮点仅在于Tree-Shaking,这是一种能够去除冗余,减少代码体积的技术。通过分析AST(抽象语法树),rollup可以发现那些不会被使用的代码,并去除它。

不过Tree-Shaking即将不是rollup的专利了,webpack 2也将支持,并也原生支持ES6 Modules。这可以说是“旁门左道”对主流派系进行贡献的一个典型案例。

webpack是去年大热的打包工具,俨然已经成为了替代grunt/gulp的最新构建工具,但显然并不是这样。笔者一直认为webpack作为一个module bundler,做了太多与其无关的事情,从而表象上看来这就是一个工程构建工具。经典的构建需要有任务的概念,然后控制任务的执行顺序,这正是Ant、Grunt、Gulp做的事情。webpack不是这样,它最重要的概念是entry,一个或者多个,它必须是类JavaScript语言编写的磁盘文件,所有其它如CSS、HTML都是围绕着entry被处理的。估计你很难一眼从配置文件中看出webpack对当前项目进行了怎样的“构建”,不过似乎社区中并没有人提出过异议,一切都运行得很好。

题外话:如果使用webpack构建一个没有任何JavaScript代码的工程?

新的angular 2使用webpack 2编译效果更加,不过,已经提了一年的webpack 2,至今仍处于beta阶段,好在现在已经rc,相信离release不远了。

5.2 npm、jspm、bower与yarn

在模块管理器这里,npm依旧是王者,但要说明的是,npm的全称是node package mamager,主要用来管理运行在node上的模块,但现在却托管了大量只能运行在浏览器上的模块。造成这种现象的几个原因:

  1. webpack的大量使用,使得前端也可以并习惯于使用CommonJS类型的模块;
  2. 没有更合适的替代者,bower以前不是,以后更不会是

前端的模块化规范过去一直处于战国纷争的年代。在node上CommonJS没什么意见。在浏览器上,虽然现在有了ES2015 Modules,却缺少了模块加载器,未来可能是SystemJS,但现在仍处于草案阶段。无论哪种,都仍处于JavaScript语言层面,而完整的前端模块化还要包括CSS与HTML,以及一些二进制资源。目前最贴近的方案也就只能是JSX+CSS in JS的模式了,这在webpack环境下大行其道。这种现象甚至影响了angular 2、ember 2等框架的设计。从这点看来,jspm只是一个加了层包装的壳子,完全没有任何优势。

npm本身也存在着各种问题,这在实践中总会影响效率、安全以及一致性,Facebook果断地出品了yarn——npm的替代升级版,支持离线模式、严格的依赖版本管理等在工程中非常实用的特性。

至于前端模块化,JavaScript有CommonJS和ES2015 Modules就够了,但工程中的组件,可能还需要在不同的框架环境中重复被开发,它们依旧不兼容。未来的话,webcomponents可能是一个比较优越的方案。

5.3 同构

同构的设计在软件行业早就被提出,不过在web前端,还是在node.js、特别是react的出现后,才真正成为了可能,因为react内核的运行并不依赖于浏览器DOM环境。

react的同构是一个比较低成本的方案,只要注意代码的执行环境,前后端确实可以共享很大一部分代码,随之带来的一大收益是有效克服了SPA这种前端渲染的页面在首屏性能上的瓶颈,这是所有具备视图能力的框架angular、vue、react等的共性问题,而现在,它们都在一种程度上支持server render。

可以想到的做前后端同构面临的几个问题:

  1. 静态资源如何引入,CSS in JS模式需要考虑在node.js上的兼容性;
  2. 数据接口如何fetch,在浏览器上是Ajax,在node.js上是什么;
  3. 如何做路由同构,浏览器无刷新切换页面,新路由在服务端可用;
  4. 大量DOM渲染如何避免阻塞node.js的执行进程

目前github上star较多的同构框架包括vue的nuxt和react的next.js,以及数据存储全包的meteor。可以肯定的是,不论它们是否能部署在生产环境中,都不可能满足你的所有需求,适当的重新架构是必要的,在这个新的领域,没有太多的经验可以借鉴。

六、未来技术与职业培养

6.1 大数据方向

越来越多做toB业务的公司对前端的需求都是在数据可视化上,或者更通俗一些——报表。这个部分在从前通常都是前端工程师嗤之以鼻的方向,认为无聊、没技术。不过在移动端时代,特别是大数据时代,对此类技能的需求增多,技术的含金量也持续提升。根据“面向工资编程”的原则,一定会有大量工程师加入进来。

对这个方向的技术技能要求是canvas、webgl,但其实绝大多数需求都不需要你直接与底层API打交道,已经有大量第三方工具帮你做到了,不乏非常优秀的框架。如百度的echarts,国外的chart.jshighchartsd3.js等等,特别是d3.js,几乎是大数据前端方向的神器,非常值得学习。

话说回来,作为工程师,心存忧患意识,一定不能以学会这几款工具就满足,在实际的业务场景中,更多的需要你扩展框架,生产自己的组件,这需要你具备一定的数学、图形和opengl底层知识,可以说是非常大的技术壁垒和入门门槛。

6.2 webVR

今年可以说是VR技术爆发式的一年,市场上推出了多款VR游戏设备,而淘宝更是开发出了平民的*buy+*购物体验,等普及开来,几乎可以颠覆传统的网上购物方式。

VR的开发离不开对3D环境的构建,webVR标准还在草案阶段,A-Frame可以用来体验,另一个three.js框架是一个比较成熟的构建3D场景的工具,除了能在未来的VR应用中大显身手,同样也在构建极大丰富的3D交互移动端页面中显得必不可少,淘宝就是国内这方面的先驱。

6.3 webassembly

asm.js已发展成webassembly,由谷歌、微软、苹果和Mozilla四家共同推动,似乎是非常喜人乐见的事情,毕竟主要浏览器内核厂商都在这里了。不过合作的一大问题就是低效,今年终于有了可以演示的demo,允许编写C++代码来运行在浏览器上了,你需要下载一大堆依赖库,以及一次非常耗时的编译,不过好歹是个进步。

短时间内,我们都不太可能改变使用JavaScript编写前端代码的现状,Dart失败了,只能期望于未来的webassembly。有了它,前端在运行时效率、安全性都会上一个台阶,其它随之而来的问题,就只能等到那一天再说了。

6.4 webcomponents

webcomponent能带给我们什么呢?HTML Template、Shadow DOM、Custom Element和HTML Import,是的,非常完美的组件化系统。angular、react的组件化系统中,都是以Custom Element的方式组合HTML,但这都是假象,它们最终都会被编译成JavaScript才会执行。但webcomponents不一样,Custom Element原生就可以被浏览器解析,DOM元素本身的方法都可以自定义,而且元素内部的子元素、样式,由于Shadow DOM的存在,不会污染全局空间,真正成为了一个沙箱,组件化就应该是这个样子,外部只关心接口,不关心也不能操纵内部的实现。

当前的组件化,无不依赖于某一特定的框架环境,或者是angular,或者是react,想移植就需要翻盘推倒重来,也就是说他们是不兼容的。有了webcomponents,作为浏览器厂商共同遵循和支持的标准,这一现状将极有可能被改写。

未来的前端组件化分发将不会是npm那么简单,可能只是引用一个html文件,更有可能的是包含CSS、HTML、JavaScript和其它二进制资源的一个目录。

目前只有最新的Chrome完全支持webcomponents的所有特性,所以距离真正应用它还尚需时日。由于技术上的限制,webcomponents polyfill的能力都非常受限,Shadow DOM不可能实现,而其它三者则更多需要离线编译实现,可以参考vue 2的实现,非常类似于webcomponents。

6.5 关于微信小程序

微信小程序对于今年不得不说,却也无话可说。依托于庞大的用户量,微信官方出品了自有的一套开发技术栈,只能说给繁杂的前端开发又填了一个角色——微信前端工程师,此外,从技术上,笔者确实无语。

七、总结

最后还有几点需要说明。

7.1 工程化

首先,现在业界都在大谈前端工程化,人人学构建,个个会打包。鄙人认为,工程化的要点在于“平衡诸方案利弊,取各指标的加权收益最大化”。仅仅加入了项目构建是远远不够的,在实践中,我们经常需要考虑的方向大可以分为两种:一是研发效率,这直接应该响应业务需求的能力;二是运行时性能,这直接影响用户的使用体验,同时也是产品经理所关心的。这两点都直接影响了公司的收入和业绩。

具体到细节的问题上来,比如说:

  1. 静态资源如果组织和打包,对于具备众多页面的产品,考虑到不断的迭代更新,如何打包能让用户的代码下载量最少(性能)?不要说使用webpack打成一个包,也不要说编译commonchunk就万事大吉了,难道还需要不断地调整编译脚本(效率)?改错了怎么办?有测试方案么?
  2. 利用angular特别是react构建纯前端渲染页面,首屏性能如何保证(性能)?引入服务端同构渲染,好的,那么服务端由谁来编写?想来必是由前端工程师来编写,那么服务端的数据层架构是怎么样的?运维角度看,前端如何保证服务的稳定(效率)?
  3. 组件化方案如何制定(效率)?如果保证组件的分发和引用的便捷性?如何保证组件在用户端的即插即用(性能)?

对于工程师来说,首先需要量化每个指标的权重,然后对于备选方案,逐个计算加权值,取最大值者,这才是科学的技术选型方法论。

然而在业界,很少能看到针对工程化的更深入分享和讨论,大多停留在“哪个框架好”,“使用XXX实现XXX”的阶段,往往是某一特定方向的优与劣,很少有科学的全局观。甚至只看到了某一方案的优势,对其弊端和可持续性避而不谈。造成这种现状的原因是多方面的,一是技术上,工程师能力的原因并没有考虑得到,二是政治上,工程师需要快速实现某一目标,以取得可见的KPI收益,完成团队的绩效目标,但更多的可能是,国内绝大多数产品的复杂性都还不够高,根本无需考虑长久的可持续发展和大规模的团队合作对于技术方案的影响。

因此,你必须接受的现状是,无论你是否使用CSS预处理器、使用webpack还是grunt、使用react还是angular,使用RN还是Hybrid,对于产品极有可能都不是那么地敏感和重要,往往更取决于决策者的个人喜好。

7.2 角色定位

确实,近两年,web前端工程师开始不够老实,要么用node.js插手服务端开发,要么用RN插手客户端开发。如何看待这些行为呢?

鄙人以为,涉足服务端开发是没问题的,因为只涉及到渲染层面,还是属于“前端”的范畴的。况且,在实际的工程实践中,已经可以证明,优秀的前端研发体系确实离不开服务端的参与,想想Facebook的BigPipe。不过,这需要服务端良好的分层架构,数据与渲染完全解耦分离,后端工程师只负责业务数据的CRUD,并提供接口,前端工程师从接口中获取数据,并推送到浏览器上。数据解耦是比接口解耦更加优越的方案。因此现在只要你的服务端架构允许,node.js作为web服务已经比较成熟,前端负责服务端渲染是完全没有问题的。

前端涉足客户端开发也是合理的,毕竟都运行在用户端,也属于前端的范畴。抛开阿里系的weex鄙人不甚了解,NativeScript、RN都还缺乏大规模持续使用的先例,这是与涉足服务端领域的不同,客户端上的方案都还不够成熟,工具的限制阻碍了前端向客户端的转型,仍然需要时间的考验。不过时间可能不会很多,未来的web技术依托高性能硬件以及普及的webgl、webrtc、Payment API等能力,在性能和功能上都会挑战Native的地位。最差的情况,还可以基于Hybrid,利用Native适当扩展能力,这就是合作而非竞争关系了。

总之前端工程师的仍然在浏览器上,就这一点,范围就足够广使得没人有敢言自己真正“精通”前端。如果条件允许的话,特别是技术成熟之后,涉猎其它领域也是鼓励的。

7.3 写在最后

在各种研发角色中,前端注定是一个比较心累的一个。每一年的年末,我们都能看到几乎完全不一样的世界,这背后是无数前端人烧脑思考、激情迸发的结果。无论最终产品的流行与否,都推动着前端技术领域的高速更新换代。正是印证了那一句“唯有变化为不变”。作为业务线的研发工程师,我们的职责是甄选最佳组合方案,取得公司利益最大化。这个“最佳”的涉猎面非常广,取决于设计者的技术视野广度,也有关于决策者的管理经验,从来都不是一件简单的事。

未来的web前端开发体验一定是更丰富的,依托webcomponents的标准化组件体系,基于webassembly的高性能运行时代码,以及背靠HTTP/2协议的高速资源加载,前端工程师不必在性能上、兼容性上分散太多精力,从而可以专注于开发具备丰富式交互体验的下一代web APP,可能是VR,也可能是游戏。

在迎接2017的同时,我们仍然要做好心理准备,新的概念、新的框架和工具以及新的语法依旧会源源不断的生产出来,不完美的现状也依旧会持续。

由于水平有限,笔者在上述内容中难免有失偏颇,请多包涵。

由于团队架构调整,今年初夏到秋末可谓在公司无所事事,业余做了两件事:

1. 申请了两个公司专利

目前第一个的奖金马上到手,扣税后也就只能买两件普通价格的女人衣服。话说这年月想以专利来致富是不可能了,首先公司已经不在那个冲量的阶段了,现在对质量有些要求,你的交底书首先要经过部分专利负责人的审核,确定确实可能有一定价值,公司才会提交给专利代理撰写申请材料。据我一个在专利局相关部门的同学讲,国家对专利的审核也愈加严格了。

2. 向社区投了一篇文章

为infoQ写了篇首发稿件,基本没有读者反馈,归根到底,我认为还是里面的内容太过抽象,不当面解释,看文字是很难看懂的,即便有图例,也是很复杂的图。

之所以投到infoQ,其实也属无奈,在国内,你基本找不到所谓的“高质量技术分享社区”,无论csdnsegmentfault,还是掘金,都充斥着大量水帖、扫盲帖和问题帖,所谓的专栏也名不副实,整个网站俨然已经成了论坛。

阅读全文 »
0%