纪念此猫

概念

BigPipe 的原理是使用 HTTP/1.1chunk 能力分片多线程加载页面的不同部分,以降低白屏/首屏时间。由于 HTML 文档(document)是一篇纯文本,文本的内容直接决定了最终页面的展现形态和功能,因此虽然服务端可以采用多线程加载数据和输出页面片段,但在浏览器端这些片段必须保证是有序排列。于是,JavaScript 脚本担负起了这个职责,它将 HTML 片段字符串渲染成为 DOM。

阅读全文 »

CSSOM 指 _CSS Object Model_,即 _CSS对象模型_。CSSOM 是 JavaScript 操纵 CSS 的一系列 API 集合,它属是 DOM 和 HTML API 的附属。

其中视图模型(View Model)中定义了一系列接口,包括多个关于窗体、文档和元素的位置尺寸信息,特别容易混淆。

阅读全文 »

虽然 redux 的模型非常简单,但如何对其理解不深,在实际的业务研发中很容易迷失,比如会纠结该如何定义枚举的 Action Type

阅读全文 »

相信很多人接触 Redux 时都会被它奇怪的 API 搞得云里雾里。这里不再冗述 Flux 架构的思想,实现 Flux 的工具有很多,它们只是在实现这种编程模式,并不会有太复杂的逻辑。事实也是这样,Redux 的 API 非常少,但并不一定容易理解。

阅读全文 »

Webcomponents 草案包含四个特性:

具体每个特性的意义不再冗述,网上到处都有,但对四个特性之间的联系及应用普遍缺少更深入的解释。

关联

Webcomponents 的名字立即会让人想起组件化,有观点认为 Custom ElementsHTML Imports 是主要部分,TemplatesShadow DOM 是次要部分,毕竟,传统的组件系统就是由组件依赖以及组件内容构成的。甚至有人经常把 Custom ElementsHTML Imports 绑定在一起,认为 Custom Elements 都是 import 进来的。

以上观点肯定是错误的,可能由于 Webcomponents 还在草案之中,文档不全,造成误解也难怪。

先来看 Custom Elements,如何注册一个自定义元素?是这样么:

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Demo</title>
<link rel="import" href="wc-rank.html">
</head>
<body>
<wc-rank></wc-rank>
</body>
</html>

不是,如果是,就证明了 Custom Elements 都是 import 进来的观点了。

自定义元素需要通过 JavaScript 脚本来注册:

1
document.registerElement('x-rank', options)

使用没有注册过的自定义元素通常也不会有问题,不过它只能代表其后代元素的意义,本身并没有语义。一般地,我们会通过定义 options 参数来扩展 DOM 元素的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13

var RankElement = document.registerElement('x-rank', {
prototype: {
next: function () {

}
}
});

var $fooRank = document.querySelector('#foo-rank');

$fooRank.next();

在这种场景下,registerElement 就是必须的了。因此,在不支持 Custom Elements 的浏览器上,polyfill 是不可能完全实现的。

好了,现在我们实现的 Custom Elements 已经有了自定义方法,但还可能需要在内部添加一些固定的后代元素,比如,对于一个 Article 来讲,Header,Footer 就是固定的后代元素,而 Summary 则非。这时候,我们一般使用模板引擎,渲染后直接将 HTML 片段插入到 DOM 中进行解析和展现。Webcomponents 提供了原生的 template 元素,预先解析了这部分 DOM,但并不展现,需要时,取出其后代元素的集合(DocumentFragment):

1
2
3
4
5
6
7
8
9
10
11
12
<wc-rank id="rank"></wc-rank>
<template>
<header>yanni4night.com</header>
<h1>Hello</h1>
<footer>&copy;copyright yanni4night.com 2014~2016</footer>
</template>

<script>
var template = document.querySelector('template');
console.log(template.content instanceof DocumentFragment) //true
document.querySelector('#rank').appendChild(template.content);
</script>

如果自定义元素在创建时就已经拥有了固定的后代元素呢,该如何实现?

1
2
3
4
5
6
7
8
9
10
11
12
13
<wc-rank id="rank">
<header>yanni4night.com</header>
<footer>&copy;copyright yanni4night.com 2014~2016</footer>
</wc-rank>
<template>
<h1>Hello</h1>
</template>

<script>
var template = document.querySelector('template');
console.log(template.content instanceof DocumentFragment) //true
document.querySelector('#rank').insertBefore(template.content, document.querySelector('#rank').lastChild);
</script>

可见,template 数量增多后会使操作十分麻烦,Shadow DOM 可以解决这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<wc-rank id="rank">
<h1>Hello</h1>
</wc-rank>
<template>
<header>yanni4night.com</header>
<content select="h1"></content>
<footer>&copy;copyright yanni4night.com 2014~2016</footer>
</template>

<script>
var proto = Object.create(HTMLElement.prototype);
proto.createdCallback = function() {
var root = this.createShadowRoot();
var template = document.querySelector('template');
var clone = document.importNode(template.content, true);
root.appendChild(clone);
};
document.registerElement('wc-rank', {
prototype: proto
});
</script>

这样,便以优雅的方式达到了定义自定义元素以及高效重用的目的。现在,将 Custom Elements 的定义分离出去,维护在单独的文档中,这就是 HTML Imports 的用处之一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!-- wc-rank.html -->
<template>
<header>yanni4night.com</header>
<content select="h1"></content>
<footer>&copy;copyright yanni4night.com 2014~2016</footer>
</template>

<script>
var proto = Object.create(HTMLElement.prototype);
proto.createdCallback = function() {
var root = this.createShadowRoot();
var template = document.querySelector('template');
var clone = document.importNode(template.content, true);
root.appendChild(clone);
};
document.registerElement('wc-rank', {
prototype: proto
});
</script>
1
2
3
4
5
<!-- index.html -->
<link rel="import" href="wc-rank.html">
<wc-rank>
<h1>Hello</h1>
</wc-rank>

如此,四个 Webcomponents 的特性全部有了用武之地:

(语义)->
Custom Elements
(内容)->
Templates
(效率)->
Shadow DOM
(重用)->
HTML Imports

它们都可以独立使用,但相互组合,更能实现优雅和高效的组件化。

Polyfill

由于目前仅 Chrome(Opera)实现了全部特性,因此 Polyfill 仍有存在一定的价值。下面几个项目都依赖了 webcomponentsjs,但在 API 上有所不同。

Polymer

谷歌发起的项目。Polymer 使用 < dom-module > 标签来定义 Custom Elements

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

<!--Custom element defination-->
<dom-module id="wc-rank">

<template>
<p>I'm a DOM element. This is my local DOM!</p>
<content select="footer"></content>
</template>

<script>
Polymer({
is: "wc-rank"
});
</script>

</dom-module>

<!--Custom element usage-->
<wc-rank>
<footer>&copy;copyright</footer>
</wc-rank>

内部同时声明了 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
2
3
4
5
6
xtag.register('wc-rank', {
content: function(){/*
‹h2›My name is rank/h2›
‹span›I work for a mad scientist‹/span›
*/}
});

X-tag 实现了 Custom elements 的生命周期回调,对 HTML ImportsTemplatesShadow DOM 没有明显的支持。

Bosonic

Bosonic 旨在构建一套低级的 UI 元素:即拿即用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<element name="wc-rank">
<template>
<span>I'm Shadow DOM</span>
</template>
<script>
Bosonic.register({
tip: function() {
var span = this.shadowRoot.querySelector('span');
span.textContent = 'Hello, world!';
}
});
</script>
</element>

<wc-rank></wc-rank>
<script>
document.querySelector('wc-rank').tip();
</script>

Rosetta

Rosetta 是百度的一套 Webcomponents 解决方案,与上面三个项目最大的区别是在线下利用构建进行 polyfill,以提高运行时效率。

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
<element name="r-slider">
<style>
</style>
<template>
<div>
{attrs.text}
</div>
<content select='.aaa'>
</content>
</template>
<script type="text/javascript">
Rosetta({
is: 'r-slider',
properties: {
list: {
type: Array,
value:[
{
title: '111'
src: 'xxx'
},
{
title: '222'
src: 'zzz'
}
]
},
text: {
type: String,
value: '测试'
}
}
});
</script>
</element>

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

Custom Elements 作为 组件名 和 __组件引用指令__,如:

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
<!--wc-rank.html-->
<wc-rank>
<template>
<h1>This is a rank</h1>
<wc-rank-content></wc-rank-content>
<aside>Aside of rank</aside>
</template>
</wc-rank>

<!--wc-rank-content.html-->
<wc-rank-content>
<template>
<content select="ul"></content>
</template>
</wc-rank-content>

<!--index.html-->
<wc-rank>
<wc-rank-content>
<ul>
<li>First</li>
<li>Second</li>
</ul>
</wc-rank-content>
</wc-rank>

那么最终输出可以是:

1
2
3
4
5
6
7
8
9
10
<div is="wc-rank">
<h1>This is a rank</h1>
<div is="wc-rank">
<ul>
<li>First</li>
<li>Second</li>
</ul>
</div>
<aside>Aside of rank</aside>
</div>

该过程完全可以在服务端完成,已经成为了一种简单的模板引擎。同时,如果需要执行 document.createElement(‘wc-rank’) ,可以将 wc-rank.html 和 wc-rank-content.html 带到前端进行动态解析。

接着,css 和 js 可以按照传统的方式进行依赖搜索和 combo。这样便将 Webcomponents 应用于服务端,并沿用组件化的思想和 Webcomponents 的草案 API,不失为前端工程化的一种解决方案。

参考

简介

Incremental DOM 是谷歌公司在 2015年4月起发起的一种支持模板引擎的高效 DOM 操作技术。相比于 React.jsDOM Diff 算法和 Ember.jsGlimmer 引擎,Incremental DOM 没有使用 Virtual DOM 的概念,转而直接去操作 DOM,因此其特点就是节约内存消耗,这对于移动端来说可能存在积极的意义。

先来看 Incremental DOM 的 API 的样子:

1
2
3
elementOpen('div', '', ['title', 'tip']);
text('Hello World!');
elementClose('div');

可以说 Incremental DOM 的 API 十分地原始和简陋。Google Developers 也提到,Incremental DOM 并非为开发者直接使用,而是用于模板引擎的底层实现。

原理

现在来简单分析下 Incremental DOM 的实现原理。

由于操作 DOM 的代价相当高,因此 React.jsGlimmer 都有一套算法来计算最小的 DOM 操作量。在 Incremental DOM 内部,通过__遍历__原始 DOM 进行脏检查来计算这个值。

首先了解 Incremental DOM 的几个概念:

节点指针

Incremental DOM 内部,维护了多个节点(HTMLElement)指针:

  • currentNode:当前节点;
  • currentParent:当前节点的父节点;
  • previousNode:当前节点的前置兄弟节点;
  • prevCurrentNode:之前遍历的最后一个节点;
  • prevCurrentParent:之前遍历的最后一个节点的父节点;
  • prevPreviousNode:之前遍历的最后一个节点的前置兄弟节点

节点遍历

主要操作有 enterNodeexitNodenextNode

假设当前指针指向为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div id="content"> <!--currentParent--> <!--prevCurrentParent-->
<div class="item1"> <!--prevPreviousNode-->
</div>
<div class="item2"> <!--previousNode--> <!--prevCurrentNode-->
</div>
<div class="item3"> <!--currentNode-->
<div title="tip">
Hello World!
</div>
</div>
<div class="item4">
</div>
<div class="item5">
</div>
</div>

enterNode() 操作,即进入 .item3 内部,各指针变为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div id="content">  <!--prevCurrentParent-->
<div class="item1">
</div>
<div class="item2"> <!--prevPreviousNode-->
</div>
<div class="item3"> <!--prevCurrentNode--> <!--currentParent-->
<!--previousNode-->
<div title="tip"> <!--currentNode-->
Hello World!
</div>
</div>
<div class="item4">
</div>
<div class="item5">
</div>
</div>

exitNode() 操作,离开 .item3,各指针变为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div id="content"> <!--currentParent-->
<div class="item1">
</div>
<div class="item2">
</div>
<div class="item3"> <!--prevCurrentParent--> <!--previousNode-->
<!--prevPreviousNode-->
<div title="tip"> <!--prevCurrentNode-->
Hello World!
</div>
</div>
<div class="item4"> <!--currentNode-->
</div>
<div class="item5">
</div>
</div>

nextNode() 操作,遍历至下一个节点,各指针变为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<div id="content"> <!--currentParent--> <!--prevCurrentParent-->
<div class="item1">
</div>
<div class="item2">
</div>
<div class="item3"> <!--prevPreviousNode-->
<div title="tip">
Hello World!
</div>
</div>
<div class="item4"> <!--prevCurrentNode--> <!--previousNode-->
</div>
<div class="item5"> <!--currentNode-->
</div>
</div>

DOM 操作

明白了 Incremental DOM 的内部指针状态后,我们来看一个例子。

1
2
3
4
5
<div id="content">
<div class="parent">
<div class="child" title="James">Hello World!</div>
</div>
</div>

我们要修改 .child 元素的 title 值:

1
2
3
4
5
6
7
patch(document.querySelector('#content'), function () {
elementOpen('div', '', null);
elementOpen('div', '', ['title', 'Jim']);
text('Hello World!');
elementClose('div');
elementClose('div');
});

实际的 DOM 遍历和操作为:

  1. enterNode(#content)
  2. enterNode(.parent)
  3. enterNode(.child)
  4. setAttribute('title', 'Jim')
  5. nextNode()
  6. exitNode(.child)
  7. exitNode(.parent)
  8. exitNode(#content)

由于 JS 代码与 HTML 在结构上是一致的,因此当遍历到 .child 元素时,直接修改其元素,而其它元素由于结构属性都没有改变,因而没有额外的 DOM 操作。

如果我们要进一步修改 DOM 为:

1
2
3
4
5
6
<div id="content">
<div class="parent">
<div class="child">Hello World!</div>
<div class="sibling">Hello World!</div>
</div>
</div>

Incremental DOM 的 API 操作为:

1
2
3
4
5
6
7
8
9
10
patch(document.querySelector('#content'), function () {
elementOpen('div', '', null);
elementOpen('div', '', null);
text('Hello World!');
elementClose('div');
elementOpen('div', '', null);
text('Hello World!');
elementClose('div');
elementClose('div');
});

实际的 DOM 遍历和操作为:

  1. enterNode(#content)
  2. enterNode(.parent)
  3. enterNode(.child)
  4. removeAttribute('title')
  5. nextNode()
  6. exitNode(.child)
  7. enterNode(.sibling)
  8. createElement()
  9. createText()
  10. nextNode()
  11. exitNode(.sibling)
  12. exitNode(.parent)
  13. 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.jsember.js 而言并没有受到太多的关注,期待其对模板引擎在性能上的促进和发展。

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 的这种新的依赖管理方式可能造成依赖模块的目录不一致,除非所有依赖模块都删除并重新安装。但是,这种不一致理论上是无害的,每个模块都能找到所有符合要求的依赖。

开发过移动端页面的同学一定听过 dprscalerem 三个概念。最起码,也会用过 scale,如

因为如果你不设置这一行,几乎所有的移动端浏览器都会把宽度设置为 980px,页面上的文字变得太小而难以阅读。

那么,这三者究竟有着怎样的关系呢?

首先从需求讲起。

移动端设备的屏幕尺寸千差万别,即便设计师能够提供每一种尺寸下的 UE 图,工程师也无法做到针对每种场景的适配。一般地,作为近似,在技术上可以使用媒体查询(media query)的方式将屏幕尺寸划分为几个等级,不同等级下使用不用的CSS样式。

但这显然不够精确,在不同设备上很难做到体验一致,不但代码难以维护,同时存在着被设计师吐槽的风险。

如何在不同的屏幕上完美还原设计图,同时兼顾有限的人力与时间?

由此我们可以提出需求:

  1. 设计师只输出一套基准尺寸的 UE 图;
  2. 通过页面缩放还原 UE 图的设计比例

这有两种方案:

  1. 使用 rem 为单位设置各个元素的尺寸;
  2. 设定一个固定的页面宽度,所有元素可以使用 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_remue_fsfoo_wue_w 皆为已知,而 foo_fs 可给定一个具体值,相当于已知。

1
2
3
.px2rem(@px){
@px * (foo_w) / (foo_fs * ue_w) rem
}

例如,以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
2
3
.px2rem(@px){
@px * 0.016 rem
}

可见,实现与 UE 图等比例的效果,只要定一个基准的 ue_w 和一个基准的 ue_fs,并任取一个当前设备的 foo_fs 就可以了,跟什么 dprscale 根本没有关系。

事实

那么如何根据屏幕宽度取一个合适的 foo_fs 呢?

再来看上面的(3)式,为了更精确的还原UE图,我们一定希望 foo_rempsd_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 / dprfoo_fsscreen.width * dpr 的约数。

CSS3 中新增了 vwvh 两个单位,分别代表可见区域宽高的百分之一,目前浏览器支持程度还不好:

caniuse

为了向后兼容,我们取 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
2
3
4
5
var dpr = window.devicePixelRatio;
var meta = '<meta name="viewport" content="width=' + baseW + ", initial-scale=" + scale + ", maximum-scale=" + scale + ", minimum-scale=" + scale + ', user-scalable=no"/>';
document.write(meta);
var style = '<style tyle="text/css">html{font-size:' + (screen * dpr / 10) + 'px !important}</style>';
document.write(style);

第二种方案,scale

相比于第一种,第二种方案显得简单粗暴:

设置一个基准的尺寸,页面上所有元素都按照此基准布局。然后将页面缩放到设备的真实尺寸上去。

比如设定基准为 400px,而真实设备尺寸为 500px,则 scale 必须为 500 / 400 = 1.25。核心代码参考:

1
2
3
4
var baseW = 400
var scale = screen.width / baseW;
var meta = '<meta name="viewport" content="width=' + baseW + ", initial-scale=" + scale + ", maximum-scale=" + scale + ", minimum-scale=" + scale + ', user-scalable=no"/>';
document.write(meta);

同时还必须要设置HTML的宽度:

1
2
3
html {
width: 400px !important
}

不必再去计算 foo_remfoo_fs 等参数。由于 scale 并非等于 1 / dpr,因此1物理像素也就没法实现了。同时,vwvh 的兼容也没有体现。好在它不需要转换 px 为 rem。

对比

比较上述两种方案:

方案 等比布局 1物理像素 兼容vw/vh 绝对定位 UE尺寸 高清图
第一种 LESS
第二种 有误差

因此两种方案的使用场景是:

  1. 如果必须实现1物理像素,或者需要精确的高清图片,或者对 vw/vh 向后兼容,则使用方案一,代表有淘宝,,缺点是需要单位换算,也存在一定的误差;
  2. 预处理,或者需要精确的像素控制,则使用方案二开发会比较方便,代表有百度H5,缺点是不能实现1物理像素,也不能实现精确的高清图片

回归

上述两个方案比较让人不爽的是都使用了 JavaScript 脚本来动态设置 scale 的大小。在苹果公司的原始设计中, viewport 是这样使用的么?

参见 Safari HTML Referenceviewport 允许开发者设置 width 来调整适配的目标设备宽度,但最终document.documentElement.clientWidth的值为

document.documentElement.clientWidth === Math.max(screen.width / scale, width)

在前面两个方案中,width 等于 screen.widthscale 小于1,因此

document.documentElement.clientWidth === screen.width / scale === screen.width * dpr

如果 scale 写死为1,自定义的 width 不能小于 screen.width。但一旦 width 大于 screen.width,就会出现滚动条,这时,JavaScript 动态计算的 scale 上场了。

因此,按照苹果公司的设计初衷,没办法不使用 JavaScript 实现完美的 UE 还原。使用一份 UE 图,无法做到多个屏幕尺寸上呈现一致的效果。

横屏

移动端开发经常缺失的一个环节是,设计师很少提供横屏版的 UE。当手机屏幕横过来怎么办?

可以监听 resizepageshow 等事件,事件触发后,重新计算 scalefoo_fs 等值。这样能保证页面元素的比例仍与 UE 相当。

有没有问题?

水平方向上好像没什么问题。垂直方向呢?

当整体页面按比例放大后,页面高度必然也会等比放大,而在横屏模式下,屏幕垂直高度又很小,从而导致大部分内容都被推出了首屏,体验和视觉上效果都不好。

因此,元素的高度一般不是用 rem 而使用 px,除非元素尺寸与屏幕尺寸强相关。

这是不是意味着所谓的完美还原是不切实际的?对于那种划页 H5, rem 的高度仍然试用,但对于普通的文本内容则不合适了。

依据具体需求采取不同的方案。

总结

对于普通的需求,width=device-widthscale=1 就够了,虽然不能在不同的设备上展示同样的效果,但是够用。

对展现要求稍高,则使用 JavaScript 动态计算 scalefoo_fs

0%