一个有趣的事实是,不论 margin 还是 _padding_,都可以设置百分比值:

1
2
3
4
.content {
margin: 5%;
padding: 10%;
}

直观上来讲,margin-left_、_margin-right_、_padding-left_、_padding-right 是相对于包含框的宽度,而 margin-top_、_margin-bottom_、_padding-top_、_padding-bottom 相对于包含框的高度。然而根据 CSS 规范,这8个值一般都是相对于包含框的宽度,究其原因还是排版的习惯需要所致。

阅读全文 »

纪念此猫

概念

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

0%