Chrome bug 之 JSON 序列化 PerformanceResourceTiming 对象
利用 nightmare 做下线性能对比分析中,少不了要获取 PerformanceResourceTiming 数据:
1 | performance.getEntriesByType('resource'); |
CSSOM View Module 中的尺寸与位置属性
CSSOM
指 _CSS Object Model_,即 _CSS对象模型_。CSSOM 是 JavaScript 操纵 CSS 的一系列 API 集合,它属是 DOM 和 HTML API 的附属。
其中视图模型(View Model)中定义了一系列接口,包括多个关于窗体、文档和元素的位置尺寸信息,特别容易混淆。
如何定义 redux 的 action
虽然 redux 的模型非常简单,但如何对其理解不深,在实际的业务研发中很容易迷失,比如会纠结该如何定义枚举的 Action Type
。
Webcomponents
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
它们都可以独立使用,但相互组合,更能实现优雅和高效的组件化。
Polyfill
由于目前仅 Chrome(Opera)实现了全部特性,因此 Polyfill 仍有存在一定的价值。下面几个项目都依赖了 webcomponentsjs,但在 API 上有所不同。
Polymer
谷歌发起的项目。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 是微软支持的项目。X-tag 以纯 JavaScript 脚本声明 Custom Elements
:
1 | xtag.register('wc-rank', { |
X-tag 实现了 Custom elements
的生命周期回调,对 HTML Imports
、Templates
和 Shadow DOM
没有明显的支持。
Bosonic
Bosonic 旨在构建一套低级的 UI 元素:即拿即用。
1 | <element name="wc-rank"> |
Rosetta
Rosetta 是百度的一套 Webcomponents
解决方案,与上面三个项目最大的区别是在线下利用构建进行 polyfill,以提高运行时效率。
1 | <element name="r-slider"> |
API 与 Polymer 如出一辙。
扩展
换个角度,Webcomponents
目前在前端生产环境中使用还为时尚早,但其组织方式可以被服务端借鉴。试想,每个组件或者自定义元素都组织在私有的目录下:
- components
- wc-rank
- wc-rank.html
- wc-rank.js
- wc-rank.less
- wc-rank-content
- wc-rank-content.html
- wc-rank-content.js
- wc-rank-content.less
- wc-rank
将 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
简介
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)指针:
- currentNode:当前节点;
- currentParent:当前节点的父节点;
- previousNode:当前节点的前置兄弟节点;
- prevCurrentNode:之前遍历的最后一个节点;
- prevCurrentParent:之前遍历的最后一个节点的父节点;
- prevPreviousNode:之前遍历的最后一个节点的前置兄弟节点
节点遍历
主要操作有 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--> |
DOM 操作
明白了 Incremental DOM
的内部指针状态后,我们来看一个例子。
1 | <div id="content"> |
我们要修改 .child
元素的 title 值:
1 | patch(document.querySelector('#content'), function () { |
实际的 DOM 遍历和操作为:
- enterNode(#content)
- enterNode(.parent)
- enterNode(.child)
setAttribute('title', 'Jim')
- nextNode()
- exitNode(.child)
- exitNode(.parent)
- exitNode(#content)
由于 JS 代码与 HTML 在结构上是一致的,因此当遍历到 .child
元素时,直接修改其元素,而其它元素由于结构属性都没有改变,因而没有额外的 DOM 操作。
如果我们要进一步修改 DOM 为:
1 | <div id="content"> |
Incremental DOM
的 API 操作为:
1 | patch(document.querySelector('#content'), function () { |
实际的 DOM 遍历和操作为:
- enterNode(#content)
- enterNode(.parent)
- enterNode(.child)
removeAttribute('title')
- nextNode()
- exitNode(.child)
- enterNode(.sibling)
createElement()
createText()
- nextNode()
- exitNode(.sibling)
- exitNode(.parent)
- exitNode(#content)
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
而言并没有受到太多的关注,期待其对模板引擎在性能上的促进和发展。
npm3 的依赖管理方案
npm3 于2015年6月发布,它与 npm2 很大的一点不同是__依赖管理方案__的升级。
为了管理同一个模块的不同版本,npm2 采用严格树形嵌套的形式组织依赖模块的目录,而 npm3 则尽量_扁平化_,将依赖模块提升至顶层目录:
对于 B 模块的两个版本,v1 版本会被放置于顶层,而 v2 版本则因为冲突关系仍放置于 C 模块下面。
这样的布置有什么好处?共用。加入再有一个模块 D 依赖与 Bv1,则不必再安装,直接使用顶层的 Bv1即可。
那如果 A 和 D 模块都依赖 Bv2 呢?npm3 不会将 Bv2 移至顶层,而是将 Bv2 仍挂在 C 和 D 下面:
似乎 npm3 并没有那么优化到最好,Bv2 模块没有被复用。要想实现这种最优的组织,需要手动执行命令:
$npm dedupe
那么,npm3 如何决定 B 模块的哪一个版本在顶层呢?答案是按照自然顺序的先后,最先安装的版本会放置于顶层。
理论上,自然顺序在任何情况下都是一定的,因此依赖模块的最终组织形式也是一定的。但这个前提是安装依赖之前 node_modules 是空的。一旦 node_modules 内已经有依赖模块,最终的组织形式就会受到影响。
假如我们现在有这样的一个应用:
修改 A 模块,使之依赖 Bv2:
现在,发布应用,在另一台机器上重新部署应用:
可见这两种环境下,依赖的目录组织形式不是一致的。
因此,npm3 的这种新的依赖管理方式可能造成依赖模块的目录不一致,除非所有依赖模块都删除并重新安装。但是,这种不一致理论上是无害的,每个模块都能找到所有符合要求的依赖。
数学方法解释 dpr/scale/rem 三者的关系
开发过移动端页面的同学一定听过 dpr
、scale
、rem
三个概念。最起码,也会用过 scale
,如
因为如果你不设置这一行,几乎所有的移动端浏览器都会把宽度设置为 980px
,页面上的文字变得太小而难以阅读。
那么,这三者究竟有着怎样的关系呢?
首先从需求讲起。
移动端设备的屏幕尺寸千差万别,即便设计师能够提供每一种尺寸下的 UE 图,工程师也无法做到针对每种场景的适配。一般地,作为近似,在技术上可以使用媒体查询(media query)的方式将屏幕尺寸划分为几个等级,不同等级下使用不用的CSS样式。
但这显然不够精确,在不同设备上很难做到体验一致,不但代码难以维护,同时存在着被设计师吐槽的风险。
如何在不同的屏幕上完美还原设计图,同时兼顾有限的人力与时间?
由此我们可以提出需求:
- 设计师只输出一套基准尺寸的 UE 图;
- 通过页面缩放还原 UE 图的设计比例
这有两种方案:
- 使用 rem 为单位设置各个元素的尺寸;
- 设定一个固定的页面宽度,所有元素可以使用 px 设定尺寸,然后缩放整个页面
我们分开来讲讲这两种方案。
第一种方案,rem
我们经常看到业界的大致方案是:
将
scale
设置为1 / dpr
,<html>
的font-size
计算为screen.width * dpr / >10
,然后在以less
将UE图上得到的尺寸透明转换为对应的rem
值。
由于 rem
是比例值,因此能做到最终每个元素的尺寸相对于 UE 图的比例都是一致的。
那么,问题是,上面的公式是怎么得到的?
分析
我们来用最基本的数据算式推导一下。
设基准 UE 的图宽为 ue_w
,<html>
的 font-size
值为 ue_fs px
。
在 PSD 上量得一个元素的宽度为 psd_w px
,等于 psd_rem
,即:
psd_rem * ue_fs = psd_w -----------(1)
在一个宽度为 foo_w
的设备上,该元素应该给定的宽度为 x_w px
。
根据 rem
单位的意义可知:
foo_rem * foo_fs / foo_w = psd_rem * ue_fs / ue_w -----------(2)
即:
foo_rem = psd_rem * (ue_fs / foo_fs) * (foo_w / ue_w) -----------(3)
其中 psd_rem
、ue_fs
、foo_w
、ue_w
皆为已知,而 foo_fs
可给定一个具体值,相当于已知。
1 | .px2rem(@px){ |
例如,以iPhone6的尺寸为基准,即:
ue_fs = 75px(任取值)
ue_w = 375px
一个宽度为屏幕宽度一般的元素,即:
psd_rem = 375px / 2 / 75px = 2.5rem
在一台 iPhone6 plus 上,则:
foo_w = 414px
foo_fs = 69px(任取值)
代入(3),得
foo_rem = 2.5 * (75 / 69) * (414 / 375) = 3rem = 3 * 69px = 414px / 2
刚好也为屏幕的一半。因此,上面的 LESS
实际内容是:
1 | .px2rem(@px){ |
可见,实现与 UE 图等比例的效果,只要定一个基准的 ue_w
和一个基准的 ue_fs
,并任取一个当前设备的 foo_fs
就可以了,跟什么 dpr
、scale
根本没有关系。
事实
那么如何根据屏幕宽度取一个合适的 foo_fs
呢?
再来看上面的(3)式,为了更精确的还原UE图,我们一定希望 foo_rem
和 psd_rem
都是有限小数,那么:
(ue_fs / foo_fs) * (foo_w / ue_w)
就也一定是有限小数。分解:
ue_fs / ue_w
和:
foo_fs / foo_w
最好都是有限小数。因此,只要取屏幕宽度的___约数___做 foo_fs
就可以了,如 iPhone6 上的 75px,iPhone6 plus 上的 69px 等等。
我们知道,在 dpr
大于1的设备上,是画不出来真正 1px 的,除非将 scale
设置成 1 / dpr
。这样,foo_w
也会成倍增加:
foo_w = screen.width * dpr
因此为了支持1物理像素,scale
必须设置成 1 / dpr
,foo_fs
取 screen.width * dpr
的约数。
CSS3
中新增了 vw
和 vh
两个单位,分别代表可见区域宽高的百分之一,目前浏览器支持程度还不好:
为了向后兼容,我们取 foo_w
的十分之一(百分之一会出小数)作为 foo_fs
:
foo_fs = foo_w / 10 = screen.width * dpr / 10 -----------(4)
这样,dpr
为2时,一个宽度为屏幕一半的元素尺寸为 5rem
,或 50vw
,仅数量级不同。
这就是为什么业界以(4)式计算 foo_fs
的缘由了。
rem
的方案就讲到这里,已经用数学算式推导出了 foo_fs
的计算公式(4)。
核心代码参考:
1 | var dpr = window.devicePixelRatio; |
第二种方案,scale
相比于第一种,第二种方案显得简单粗暴:
设置一个基准的尺寸,页面上所有元素都按照此基准布局。然后将页面缩放到设备的真实尺寸上去。
比如设定基准为 400px,而真实设备尺寸为 500px,则 scale
必须为 500 / 400 = 1.25
。核心代码参考:
1 | var baseW = 400 |
同时还必须要设置HTML的宽度:
1 | html { |
不必再去计算 foo_rem
、foo_fs
等参数。由于 scale
并非等于 1 / dpr
,因此1物理像素也就没法实现了。同时,vw
、vh
的兼容也没有体现。好在它不需要转换 px 为 rem。
对比
比较上述两种方案:
方案 | 等比布局 | 1物理像素 | 兼容vw/vh | 绝对定位 | UE尺寸 | 高清图 |
---|---|---|---|---|---|---|
第一种 | ✔ | ✔ | ✔ | ✔ | LESS | ✔ |
第二种 | ✔ | ✘ | ✘ | ✔ | ✔ | 有误差 |
因此两种方案的使用场景是:
- 如果必须实现1物理像素,或者需要精确的高清图片,或者对
vw/vh
向后兼容,则使用方案一,代表有淘宝,,缺点是需要单位换算,也存在一定的误差; - 预处理,或者需要精确的像素控制,则使用方案二开发会比较方便,代表有百度H5,缺点是不能实现1物理像素,也不能实现精确的高清图片
回归
上述两个方案比较让人不爽的是都使用了 JavaScript 脚本来动态设置 scale
的大小。在苹果公司的原始设计中, viewport
是这样使用的么?
参见 Safari HTML Reference,viewport
允许开发者设置 width 来调整适配的目标设备宽度,但最终document.documentElement.clientWidth
的值为
document.documentElement.clientWidth === Math.max(screen.width / scale, width)
在前面两个方案中,width 等于 screen.width
,scale
小于1,因此
document.documentElement.clientWidth === screen.width / scale === screen.width * dpr
如果 scale
写死为1,自定义的 width 不能小于 screen.width
。但一旦 width 大于 screen.width
,就会出现滚动条,这时,JavaScript 动态计算的 scale
上场了。
因此,按照苹果公司的设计初衷,没办法不使用 JavaScript 实现完美的 UE 还原。使用一份 UE 图,无法做到多个屏幕尺寸上呈现一致的效果。
横屏
移动端开发经常缺失的一个环节是,设计师很少提供横屏版的 UE。当手机屏幕横过来怎么办?
可以监听 resize
、pageshow
等事件,事件触发后,重新计算 scale
、foo_fs
等值。这样能保证页面元素的比例仍与 UE 相当。
有没有问题?
水平方向上好像没什么问题。垂直方向呢?
当整体页面按比例放大后,页面高度必然也会等比放大,而在横屏模式下,屏幕垂直高度又很小,从而导致大部分内容都被推出了首屏,体验和视觉上效果都不好。
因此,元素的高度一般不是用 rem 而使用 px,除非元素尺寸与屏幕尺寸强相关。
这是不是意味着所谓的完美还原是不切实际的?对于那种划页 H5, rem 的高度仍然试用,但对于普通的文本内容则不合适了。
依据具体需求采取不同的方案。
总结
对于普通的需求,width=device-width
和 scale=1
就够了,虽然不能在不同的设备上展示同样的效果,但是够用。
对展现要求稍高,则使用 JavaScript 动态计算 scale
和 foo_fs
。