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

当小组决定让我写这个主题时,我是拒绝的,因为以我的资历,还远远不能积累到足够的素材、故事和教训来拼凑一篇像样的回忆录。因此,这里算是我的这个主题的个人认识吧。

经常听到这样的言语:

现在业界对前端工程师这个角色的看法是有误解的,事实上前端的工作并没有那样简单。

我的个人观点是:__业界没有误解,前端的工作就是那样简单__。

最近在特别关注UI设计的社会培训课程,几乎每家培训机构在UI课程中都会加入一项”Web前端开发”,期望能在几天的培训课程后,能够让UI设计学员掌握一定的编码技巧,有能力承担简单网页的制作、JS特效等开发。可见前端入门水平之低。

让我们看拉勾网上一个招聘职位的 Job Description(JD):

1. 3年以上前端开发经验;
2. 熟练使用html和css制作符合W3C标准的页面,注重页面性能、语义化,能从整个产品的角度去考虑代码结构;
3. 熟悉JS,使用过JQ等前端框架,了解基本的语法
4. 熟悉AngularJS或同等框架,能做基于API接口的前端产品
5. 喜欢前端技术并且善于钻研,喜欢与他人分享自己的成果,乐于跟同行讨论问题

这是一个对有着三年工作经验的前端工程师的期望,

  • 熟练使用html和css制作符合W3C标准的页面,这就是前端工程师的基本工作内容;”注重页面性能、语义化,能从整个产品的角度去考虑代码结构”,ok,这有一点提升的要求了,不仅仅要求你制作的页面能用,还要求一定的优化。不过这一点属于extra内容,对于一家创业团队来说,现在重点是怎么迅速将投资人的钱转换为看得见的产品,起码有个交代,至于优化这些工作,你加班来做吧,反正老板暂时是不怎么care的;
  • 熟悉JS,使用过JQ等前端框架,了解基本的语法,开什么玩笑,前端工程师如果不熟悉JS还能叫前端工程师吗,什么叫了解基本的语法,以现在这个年代的页面复杂性,恐怕需要了解全部语法而不是仅仅基本语法。不够了解全部语法也不是什么难事,相信99%的工程师都能做到,剩下那1%可以刨出工程师范畴之外了。JQ更不必说了,即使不是前端工程师,都有一大批人听说并且有能力使用;
  • 熟悉AngularJS或同等框架,这要求又高了,AngularJS可不是所有前端工程师都听说过的,更不用说熟悉了,况且它那么复杂。这里有一个很有意思的点,”熟悉JS”和”熟悉AngularJS”,我理解这是一种包含的关系 ,熟悉AngularJS”必然”熟悉JS”,反之则不一定,因此,JD中的”熟悉JS”可以认为是废话;
  • 喜欢前端技术并且善于钻研,这是套话了,暂且不表

看来三年的前端工作经验也无非如此,只需要掌握基本的职业技能,再花点时间稍微扩展一下知识就能拿到14k~18k的月薪了,前端就是这么一个low的职业,不需要你拥有高学历,也不需要你有能力解决XXX算法之类的烧脑问题。因此,无数非计算机专业的、计算机专业里常挂科的、野鸡大学的、培训机构的都来分前端这块面包,造就了大量的前端工程师。好在近两年互联网移动化转型,产生了大量移动web前端职位,前景似乎一片光明。

随着业务的发展,我们贴吧这样的前端团队也需要及时补充血液,在过去几个月里,面试了的很大一部分候选人。其实对于百度这样公司的人才甄选标准,能够进入面试环节,简历必然也是相当可以的。而且对于有工作经验的社招候选人,其编码能力也不会差到哪去,支持团队的日常业务开发任务问题不会很大。为什么大部分候选人会被毙掉呢?因为我们要的是工程师,而他们更像是网页制作

这是一个很大的差异,你可以理解为战略和战术的区别:网页制作能够解决单个Case(一般是一个HTML网页)的开发就ok,而工程师则要负责一套包含HTML页面的整体解决方案。咦?前端的工作有这么复杂么?有的。什么时候有的?我不太清楚,最近几年吧。

我们看一些前端的工作都复杂在哪里。首先,当我们设想一个前端工程的时候,会考虑:

  • 模板与数据:应使用后端模板渲染还是前端模板,后端模板对首屏性能友好,但需考虑前后端开发解耦;前端模板需考虑框架的维护性、升级;
  • 静态资源:是否需要使用各种预处理器,如何打包;
  • 组件化:是否需要组件化,使用何种组件化规范,这涉及到团队多人合作问题

上面每一个主题都可以拆成N多个小主题,涉及到的问题成百上千。其实前端工程中的真正难点还不是这些,而是备选方案五花八门,没有统一标准。相对于其它领域,前端的技术发展太快,没有一种技术能够流行到成为主流。比如前些年的JS模块化规范 Require.js 现在已基本不被推荐,反而基于 BabelES6 Modules 越来越流行。能够利用自己的实践经验来有效规划合适的技术架构的人,已经成为了一种新的职业:前端架构师。

既然前端领域也如此善变和复杂,那么一个前端工程师应该如何学习和成长才能成为一名前端架构师呢?

  1. __精通HTML/CSS/JS__,这是基本要求,不再冗述;
  2. 之前对一个对前端工程师的基本要求,也是能体现前端工程师存在价值的一点就是能够__解决各种浏览器兼容问题__。在2015~2016之交的今天,这一点似乎并不那么重要,因为越来越多的公司抛弃了IE6&IE7,并且致力于移动端web开发,今天的浏览器兼容问题已经不同于从前;
  3. __实现UE设计的各种交互效果__也经常出现在各个JD中,这里的”交互效果”主要指特效。曾几何时,JS的主要应用场景就是写各种交互特效,写特效也是前端工程师的必备技能之一,能够写出炫目的特效往往成为炫耀的资本。在今天,Web的风格与N年前大不相同,夸张的动效不再是为了吸引用户的眼球,反而被设计者极力避免使用,以免影响用户体验。另一方面,在移动设备上,动画已经开始使用CSS3实现而非JS实现。总之,JS特效这一环节在当前已没那么重要,在简历上标记『会使用JS实现各种效果』已经没有什么权重;
  4. __精通原生JavaScript__,脱离jQuery等框架进行编程。一个框架没有绝对意义上的好不好,只有针对应用场景的适合不适合。通过编写原生JS代码,不但有机会了解W3C/ECMAScript/Whatwg等标准规范,还对你以后可能参与的JS框架的开发有益处;
  5. __熟悉常用的JS框架__,阅读优秀的开源框架,你能看到更多的编码技巧、更优秀的设计理念,对你以后的程序设计有着绝对有益的影响。特别地,职位面试中几乎都会有关注各种框架的问题;
  6. __熟悉移动端开发__,近两年移动设备的普及,特别是微信等传播平台的出现,使得前端越来越偏重向移动端,业界甚至产生了多个移动页面快速研发解决方案,如易企秀MAKA微页兔展等等,可见移动端开发的重要性;
  7. __熟悉nodejs、php等非脚本语言__,未来全栈是一个很重要的方向,特别是nodejs成熟后,纯js平台的全栈对于小型团队来说尤为重要;
  8. __了解前端工程化__,工程化能力才能真正一个工程师而非网页制作的价值,它需要对前端开发中的各个环节都有清晰的认知。如果没有工程化思考,复杂的前端项目将无从谈起,只会停留在网页制作的水平。

以上几点是我对一名前端工程师的能力的基本认识,这与几年前相比几乎是截然不同的。之前很重要的技能在今天看来不值一提,今天必备的技能在之前看来也是遥不可及,可见随着时代变迁、技术的发展,对工程师的定义也会发生翻天覆地的变化。

那么成为一名合格的前端工程师之后,如何变为一名前端架构师呢?这里我认为有两点特征就够了,这两点特征也是大多数人的瓶颈,阻挡着大部分候选人进入百度,也阻碍着大部分”网页制作”变成”前端工程师”。这两点是:

  • 学习能力
  • 总结能力

在任何行业任何工种下,学习都是一个永恒的话题,特别对于web前端这样高速发展的领域。只有通过不断学习,才能有机会扩展视界,沉淀出足够的技术方案。同时,也只有总结,才能归纳出最合适的方案。这两点缺一不可,前端提到的几点要求只是现状,如何不满足,但拥有足够的学习和总结能力,这些就都会很快解决。因此,学习能力和总结能力才是工程师成长中最需要掌握的技能。

现在我们回到前面,为什么我说”前端的工作就是这么简单”,因为绝大多数业务的需求就是比较简单,只要开发一个可以看得见的网页就好了,这是普通网页制作人员就能Hold住的工作内容。这也说明了市场上绝大多数所谓”前端工程师”都是”网页制作”。一旦业务升级、团队规模扩大,”网页制作”的工作就会立即将整个团队的业务节奏推向万劫不复的深渊。一般地,在这种情况下至少会有一个人发现了问题,并开始解决,从此走向了”前端工程师”之路。

因此,在你的成长过程中,遇到困难和问题并非一定都是坏事,往往越糟糕的环境就越能激发你的创造天赋。在这一点上,一个有一定发展但又不足够成熟的公司往往更适合你的初期发展,它会培养你的学习和总结能力。我个人认为在中国,BAT这类公司并不合适,反而像豆瓣、知乎这类中小型公司,效率相对更高,会提供给你更大的发展空间。当你发现自己在这类公司中已经解决了很多问题,积累了足够的经验后,再去大型公司,比较一下他们的工作,会发现你之前的工作方式是多么低级和不规范,从而产生敬畏之心。当你怀着一份敬畏之心来对待这份工作,你会有着不一样的看法:前端领域深似海。

当我本人看穿这一切后,忽然觉得,前端如此,哪个领域又何尝不是?一味地追求极致只会让自己陷入我执当中。技术界多有高人精通,却有几人真正明白这些都是对于不完美的妥协?生活亦如此,每个人都称热爱生活,谁又会在意其背后的心酸、苦楚与无奈?

一切技术都是浮云,美好生活才是真谛。当你做这份工作时,失败和妥协只是你成长过程中不断被踢开的绊脚石,热爱和坚持才是那永不枯竭的推动力。不要过份追求成长,当你的经验教训积累足够,成长就会像那初春解冻的冰川,势如破竹。

cat

介绍一下你自己和所做的工作。

我叫 yinyong,2013年硕士研究生毕业开始在宇宙中心五道口的搜狐网络大厦工作直到去年,目前在百度。2008年南方大雪的时候 刚好 选择了也留在西安过年,一起的还有许多不能回家的小伙伴。因为无聊,拿起了别人从图书馆借来的却没有阅读的《Java程序设计》,从此一发不可收拾踏上了编程之路。不过,毕业时做的WEB前端工程师的角色,目前扔在坚持但主要做前端技术架构方向的研究。

我是工程师,不是程序员。

你都在使用哪些硬件?

上学时喜爱手机,曾经手握7部各种操作系统的手机(iOS除外,因为买不起)。不过后来兴趣越来越淡了,在折腾了几年的Android后,年纪大了心也累了,目前手握一部iPhone6,娱乐工作都能应付,而且充电速度很满意。家里的iPad Air基本上很少碰了,之前还用它看看美剧。

工作上我一直使用自己购买的Mac Air 2012,后来公司发了台Mac Pro 2014,发现除了续航外,体验等方面都不如自己的Air。在公司还放着一台神舟K480N-i7D3,买来主要看上了其CPU、显卡并且价格不高,在更换了SSD并加了内存后,主要用做玩游戏。不过现在也很少玩了,电脑放在那基本算是备用机。

此外,我基本没有其它硬件设备,特别对机械键盘我是嗤之以鼻的,因为它会让我觉得自己是手指体力劳动者而非脑力劳动者。

软件呢?

在文本编辑上,我一直使用 Sublime Text,最爱它的多行编辑功能,同时其插件使用Python编写也比较容易,自己也写过一个

其它软件:

  • 网易云音乐,Mac版+iOS版,开通了畅听流量包
  • Bandizip X 2,用于文件解压
  • Pages/Keynote,日常办公文档
  • iTerm 2

总之很少有特别的软件能反应出我的实际工作内容,甚至是便签我也一直使用mac自带的。

你最理想的工作环境是什么?

喜欢工位的一侧靠着窗户并且有很宽广的视野,同时希望后面不要有人可以随时窥视我的显示器。

你平时获得工作灵感的方式有哪些?

因为现在很少再专注于一个具体技术问题,而更多的是大方案上的思考,因此更多的是 Google 各种国外 blog来攒取灵感。但偶尔也会去Github上搜寻有意思的项目。

推荐一件生活中的利器给大家。

一款iOS应用Days Matter,我用它来统计我自出生以来的天数。


本文参与了「利器社群计划」,发现更多创造者和他们的工具:http://liqi.io/community/

序号 时间 位置
1 2014.12.15 Call
2 2015.2.14 HuaiRou
3 2015.3.14 HuaiRou
4 2015.3.16 WangJing
5 2015.3.22 HuaiRou
6 2015.3.25 WangJing
7 2015.3.26 WangJing
8 2015.3.29 WangJingxi
9 2015.4.2 WangJing
10 2015.4.4 WangJingxi
11 2015.4.6 WangJingxi
12 2015.4.10 XiErqi
13 2015.4.11 WangJingxi
14 2015.4.18 WuDaokou
15 2015.4.19 HuaiRou
16 2015.4.22 WangJingxi
17 2015.4.23 WangJingxi
18 2015.4.25 HuiLongguan
19 2015.4.30~5.1 HuiLongguan/LongZe
20 2015.5.2~5.3 YanQing/LongZe
21 2015.5.6 WangJingxi
22 2015.5.8 WangJingxi
23 2015.5.9 ShangDi
24 2015.5.13 WangJingxi
25 2015.5.15~5.17 WangJingxi/HuiLongguan/LongZe
26 2015.5.21 WangJingxi
27 2015.5.22~5.23 HuiLongguan/LongZe
28 2015.5.25 WangJingxi
29 2015.5.27 WangJing
30 2015.5.30 WangJingxi/LongZe
31 2015.6.2 WangJingxi
32 2015.6.5~6.7 HuiLongguan/LongZe/HuaiRou
33 2015.6.13~6.14 YanQing/LongZe
34 2015.6.16 WangJingxi
35 2015.6.17 LongZe
36 2015.6.23~2016.2.2 LongZe/WangJing
37 2016.2.11~2016.3.22 WangJing
38 2016.2.25~ WangJing
0%