同构场景下的复杂前端构建
提到 Web 前端工程化,构建,或者称为“编译”,是其中最重要的构成部分。由于出道比较晚,我在毕业后的第一家公司就职时就不得不掌握一定的构建知识和技能。当时我们使用的构建工具,使用 python 语言编写,主要能对 JavaScript 进行 AMD 规范的合并和压缩,对 CSS 进行合并压缩,以及增添各种时间戳。由于工具没有扩展功能,因此构建流程的固定的,如有必要,需要对工具进行升级,柔韧性并不好,好在当时的业务复杂度也并不高。
利用 nightmare 做下线性能对比分析中,少不了要获取 PerformanceResourceTiming 数据:
1 | performance.getEntriesByType('resource'); |
CSSOM
指 _CSS Object Model_,即 _CSS对象模型_。CSSOM 是 JavaScript 操纵 CSS 的一系列 API 集合,它属是 DOM 和 HTML API 的附属。
其中视图模型(View Model)中定义了一系列接口,包括多个关于窗体、文档和元素的位置尺寸信息,特别容易混淆。
虽然 redux 的模型非常简单,但如何对其理解不深,在实际的业务研发中很容易迷失,比如会纠结该如何定义枚举的 Action Type
。
Webcomponents
草案包含四个特性:
具体每个特性的意义不再冗述,网上到处都有,但对四个特性之间的联系及应用普遍缺少更深入的解释。
Webcomponents
的名字立即会让人想起组件化,有观点认为 Custom Elements
与 HTML Imports
是主要部分,Templates
与 Shadow DOM
是次要部分,毕竟,传统的组件系统就是由组件依赖以及组件内容构成的。甚至有人经常把 Custom Elements
与 HTML Imports
绑定在一起,认为 Custom Elements
都是 import 进来的。
以上观点肯定是错误的,可能由于 Webcomponents
还在草案之中,文档不全,造成误解也难怪。
先来看 Custom Elements
,如何注册一个自定义元素?是这样么:
1 | <!DOCTYPE html> |
不是,如果是,就证明了 Custom Elements
都是 import 进来的观点了。
自定义元素需要通过 JavaScript 脚本来注册:
1 | document.registerElement('x-rank', options) |
使用没有注册过的自定义元素通常也不会有问题,不过它只能代表其后代元素的意义,本身并没有语义。一般地,我们会通过定义 options 参数来扩展 DOM 元素的方法:
1 |
|
在这种场景下,registerElement
就是必须的了。因此,在不支持 Custom Elements
的浏览器上,polyfill 是不可能完全实现的。
好了,现在我们实现的 Custom Elements
已经有了自定义方法,但还可能需要在内部添加一些固定的后代元素,比如,对于一个 Article 来讲,Header,Footer 就是固定的后代元素,而 Summary 则非。这时候,我们一般使用模板引擎,渲染后直接将 HTML 片段插入到 DOM 中进行解析和展现。Webcomponents
提供了原生的 template 元素,预先解析了这部分 DOM,但并不展现,需要时,取出其后代元素的集合(DocumentFragment):
1 | <wc-rank id="rank"></wc-rank> |
如果自定义元素在创建时就已经拥有了固定的后代元素呢,该如何实现?
1 | <wc-rank id="rank"> |
可见,template 数量增多后会使操作十分麻烦,Shadow DOM
可以解决这个问题:
1 | <wc-rank id="rank"> |
这样,便以优雅的方式达到了定义自定义元素以及高效重用的目的。现在,将 Custom Elements
的定义分离出去,维护在单独的文档中,这就是 HTML Imports
的用处之一。
1 | <!-- wc-rank.html --> |
1 | <!-- index.html --> |
如此,四个 Webcomponents
的特性全部有了用武之地:
(语义)->
Custom Elements
(内容)->
Templates
(效率)->
Shadow DOM
(重用)->
HTML Imports
它们都可以独立使用,但相互组合,更能实现优雅和高效的组件化。
由于目前仅 Chrome(Opera)实现了全部特性,因此 Polyfill 仍有存在一定的价值。下面几个项目都依赖了 webcomponentsjs,但在 API 上有所不同。
谷歌发起的项目。Polymer 使用 < dom-module > 标签来定义 Custom Elements
:
1 |
|
内部同时声明了 Templates
,并使用 shady DOM 来针对不支持 shadow DOM 的浏览器。HTML Imports
也被支持。
值得一提的是,Polymer 支持更复杂的 template,比如 mustache 语法及 dom-if_、_dom-repeat 指令,有点类似于 Angular 的 ng-if 和 _ng-repeat_。
X-tag 是微软支持的项目。X-tag 以纯 JavaScript 脚本声明 Custom Elements
:
1 | xtag.register('wc-rank', { |
X-tag 实现了 Custom elements
的生命周期回调,对 HTML Imports
、Templates
和 Shadow DOM
没有明显的支持。
Bosonic 旨在构建一套低级的 UI 元素:即拿即用。
1 | <element name="wc-rank"> |
Rosetta 是百度的一套 Webcomponents
解决方案,与上面三个项目最大的区别是在线下利用构建进行 polyfill,以提高运行时效率。
1 | <element name="r-slider"> |
API 与 Polymer 如出一辙。
换个角度,Webcomponents
目前在前端生产环境中使用还为时尚早,但其组织方式可以被服务端借鉴。试想,每个组件或者自定义元素都组织在私有的目录下:
将 Custom Elements
作为 组件名 和 __组件引用指令__,如:
1 | <!--wc-rank.html--> |
那么最终输出可以是:
1 | <div is="wc-rank"> |
该过程完全可以在服务端完成,已经成为了一种简单的模板引擎。同时,如果需要执行 document.createElement(‘wc-rank’) ,可以将 wc-rank.html 和 wc-rank-content.html 带到前端进行动态解析。
接着,css 和 js 可以按照传统的方式进行依赖搜索和 combo。这样便将 Webcomponents
应用于服务端,并沿用组件化的思想和 Webcomponents
的草案 API,不失为前端工程化的一种解决方案。
Incremental DOM 是谷歌公司在 2015年4月起发起的一种支持模板引擎的高效 DOM 操作技术。相比于 React.js
的 DOM Diff
算法和 Ember.js
的 Glimmer
引擎,Incremental DOM
没有使用 Virtual DOM
的概念,转而直接去操作 DOM,因此其特点就是节约内存消耗,这对于移动端来说可能存在积极的意义。
先来看 Incremental DOM
的 API 的样子:
1 | elementOpen('div', '', ['title', 'tip']); |
可以说 Incremental DOM
的 API 十分地原始和简陋。Google Developers 也提到,Incremental DOM
并非为开发者直接使用,而是用于模板引擎的底层实现。
现在来简单分析下 Incremental DOM
的实现原理。
由于操作 DOM 的代价相当高,因此 React.js
和 Glimmer
都有一套算法来计算最小的 DOM 操作量。在 Incremental DOM
内部,通过__遍历__原始 DOM 进行脏检查来计算这个值。
首先了解 Incremental DOM
的几个概念:
在 Incremental DOM
内部,维护了多个节点(HTMLElement)指针:
主要操作有 enterNode
、exitNode
、nextNode
。
假设当前指针指向为:
1 | <div id="content"> <!--currentParent--> <!--prevCurrentParent--> |
enterNode()
操作,即进入 .item3
内部,各指针变为:
1 | <div id="content"> <!--prevCurrentParent--> |
exitNode()
操作,离开 .item3
,各指针变为:
1 | <div id="content"> <!--currentParent--> |
nextNode()
操作,遍历至下一个节点,各指针变为:
1 | <div id="content"> <!--currentParent--> <!--prevCurrentParent--> |
明白了 Incremental DOM
的内部指针状态后,我们来看一个例子。
1 | <div id="content"> |
我们要修改 .child
元素的 title 值:
1 | patch(document.querySelector('#content'), function () { |
实际的 DOM 遍历和操作为:
setAttribute('title', 'Jim')
由于 JS 代码与 HTML 在结构上是一致的,因此当遍历到 .child
元素时,直接修改其元素,而其它元素由于结构属性都没有改变,因而没有额外的 DOM 操作。
如果我们要进一步修改 DOM 为:
1 | <div id="content"> |
Incremental DOM
的 API 操作为:
1 | patch(document.querySelector('#content'), function () { |
实际的 DOM 遍历和操作为:
removeAttribute('title')
createElement()
createText()
Incremental DOM
正是通过这种简单粗暴的方式来实现最小量的 DOM 操作。
由于直接在原始 DOM 上做脏检查,Incremental DOM
在性能上有所下降。
上图是各个框架在布局和绘画上的性能比较,可见 Incremental DOM
是垫底的。
Incremental DOM
用性能来换取内存优化:
上图是各个框架在 GC 上的性能,Incremental DOM
表现十分优越。
Incremental DOM
的特点使得它适用于内存敏感型而非性能敏感型的应用。同时,前面也提到,Incremental DOM
为模板引擎的底层所设计,不适合直接调用其 API。
已经应用了 Incremental DOM
的模板引擎有:
Incremental DOM
仍在发展中,相比 react.js
与 ember.js
而言并没有受到太多的关注,期待其对模板引擎在性能上的促进和发展。