Web Component

前言

组件,是数据和方法的一个封装,其定义了一个可重用的软件元素的功能,展示和使用,通常表现为一个或一组可重用的元素。
组件的特性通常可以总结为以下几点:

  • 可拓展性:既然组件是针对某一特定功能或需求开发的,那它就必须易于开发和拓展;
  • 封装性:组件作为一个独立整体供使用,应该是对内修改,对外封闭,只供使用,而不对使用环境产生副作用;
  • 易用性:组件的目的是产生可重用的独立部件,那就必须提供一种简单快捷的方式供使用。

组件化,给前端开发带来了极大的效率提升,是近几年以来web开发发展的趋势,各种组件化的用户界面库,框架也层出不穷,如,React,Vue等,这些框架关于组件化都有各自的实现,推崇理念,与编程规范,各大框架的支持者之间的争论也是向来不断,而若想在不同框架间切换,成本还是挺高的,因为毕竟谁都希望自己能占主流,占据绝对优势地位,就像当前IE与网景浏览器之争,延续到现在,各类浏览器标准兼容差异万千,近年来w3c不断在为web标准规范做努力,Web Components就是推出的关于组件化的一个标准,希望它能将组件化更好的带进web开发,同时尽量保证标准规范,开发者可以更好的关注于开发,而不是框架选择与争论之上。

简介

Web Components它本身不是一个规范,他是由W3C提出的另外4个规范的合集,使开发者可以自由创建在web应用或文档可重用的元素或部件,这四个规范是:

  • Custom Elements(草案阶段): 定义新HTML元素的一系列API
  • Shadow Dom(草案阶段):组合对DOM和样式的封装
  • HTML Template(html5): HTML内的DOM模板,在<template>元素内声明
  • HTML Imports(草案阶段): 定义在文档中导入其他HTML文档的方式

如上,这四个规范除了template已经成为了HTML5的规范,其他3个还是处于草案阶段的,所以浏览器的支持情况比较差也是可以理解的了。接下来这四个规范一个个聊一聊。

Custom Elements

自定义元素支持开发者定义一类新HTML元素,声明其行为和样式,比较好的实现了组件开发的可拓展性。

浏览器对待自定义元素,就像对待标准元素一样,只是没有默认的样式和行为。这种处理方式是写入 HTML5 标准的。

1
2
"User agents must treat elements and attributes that they do not understand as semantically neutral; leaving them in
the DOM (for DOM processors), and styling them according to CSS (for CSS processors), but not inferring any meaning from them."

上面这段话的意思是,浏览器必须将自定义元素保留在 DOM 之中,但不会任何语义。除此之外,自定义元素与标准元素都一致。

事实上,浏览器提供了一个HTMLUnknownElement对象,所有自定义元素都是该对象的实例。

1
2
3
4
var tabs = document.createElement('tabs');

tabs instanceof HTMLUnknownElement // true
tabs instanceof HTMLElement // true

上面代码中,tabs是一个自定义元素,同时继承了HTMLUnknownElement和HTMLElement接口。

Custom Elements提供了一些生命周期让我们组件可以在初始化的过程中就给自己绑定上方法:

  • createdCallback:元素首次被插入文档DOM时触发
  • attachedCallback:元素从文档DOM中删除时触发
  • detachedCallback:元素被移动到新的文档时触发
  • attributeChangedCallback(attrName, oldVal, newVal):元素增加、删除、修改自身属性时触发

自定义元素分两类:

  • 自定义标签元素(Autonomous custom elements):完全独立于原始HTML元素标签的新标签元素,其所有行为需要开发者定义;
  • 自定义内置元素(Customized built-in):基于HTML原始元素标签的自定义元素,以便于使用原始元素的特性,开发者只需要定义拓展行为;

自定义标签元素

现在我们想要定义一个这样的元素:

1
<flag-icon country="cn"></flag-icon>

通过给属性country赋值来显示对应的国旗。

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
class FlagIcon extends HTMLElement {
constructor() {
super();
this._countryCode = null;
}

static get observedAttributes() { return ["country"]; }

attributeChangedCallback(name, oldValue, newValue) {
// name will always be "country" due to observedAttributes
this._countryCode = newValue;
this._updateRendering();
}

connectedCallback() {
this._updateRendering();
}

get country() {
return this._countryCode;
}

set country(v) {
this.setAttribute("country", v);
}

_updateRendering() {
//...
}
}

//全局注册该元素
customElements.define("flag-icon", FlagIcon);

注册后,也通过js创建该元素

1
2
3
const flagIcon = document.createElement("flag-icon");
flagIcon.country = "cn";
document.body.appendChild(flagIcon);

自定义内置元素

继承自已有元素,拥有已有元素的所有特性。

比如我们自定义一个按钮,集成普通按钮所有的特性,但是当点击的时候会有一个动效,就可以这么做 ——

1
2
3
4
5
6
7
8
9
class PlasticButton extends HTMLButtonElement {
constructor() {
super();

this.addEventListener("click", () => {
// 动效逻辑
});
}
}

不同的是,注册时要加上一个参数

1
customElements.define("plastic-button", PlasticButton, { extends: "button" });

使用时也稍有不同

1
<button is="plastic-button">点我!</button>

通过js创建元素,则是这样

1
2
3
const plasticButton = document.createElement("button", { is: "plastic-button" });
plasticButton.textContent = "点我!";
document.body.appendChild(flagIcon);

Shadow Dom

他的作用是:管理多DOM树的层级关系,更好的合成DOM。他的中心思想是封装一个完全独立于文档流的子DOM树。他完美的做到了css的封装。当然还有文档内容的封装。以及通过重定向事件做了事件层面的封装。当然封装是他提供的能力,作为使用者的我们其实很关心的是主文档与Shadow Dom的交互,这个在下面会提到。

创建Shadow Dom

使用的第一步是创建,Shadow Dom的创建得基于一个文档中已经存在的一个元素(HTML内置元素或自定义元素),也就是宿主元素。

1
2
3
var frag = document.createElement('div');
var shadowRoot = frag.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<p>Shadow DOM Content</p>';

上文使用attachShadow()方法创建的元素就是一个影子DOM,而其子内容就构成一棵影子树(shadow tree),而和影子DOM绑定,也就是包含该树的文档内元素通常称为影子主体(shadow host)。

宿主元素的内容是不会被渲染的,我们可以通过slot来将内容映射到shadow dom中来显示,还可以通过设置name的属性来决定那一块被映射。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- 页面 -->
<div class="menus">
<slot></slot>
<slot name="top"></slot>
<slot name="right"></slot>
</div>

<!-- shadow dom -->
<h2>Menus</h2>
<ul slot="top">
<li>Home</li>
<li>About</li>
</ul>
<ul slot="right">
<li>Home</li>
<li>Top</li>
</ul>

渲染结果:

1
2
3
4
5
6
7
8
9
10
11
<div class="menus">
<h2>Menus</h2>
<ul>
<li>Home</li>
<li>About</li>
</ul>
<ul>
<li>Home</li>
<li>Top</li>
</ul>
</div>

样式的影响

Shadow Dom的样式被完全封装,内部的样式对外部完全没有影响。文档流中的样式也对内部没有影响。这一点其实很重要。因为只有这样,才能保证组件的无伤。但是我们使用过程中肯定也会想要有时对shadow dom中的样式进行一定的改写的。Shadow Dom提供了这样的接口。

1.组件->影响文档流

组件内部只能使用:host来改变宿主元素的样式,页面的其他内容也是无法影响的

1
2
3
4
5
6
:host(x-foo:host) {
/* 当宿主是 <x-foo> 元素时生效。 */
}
:host(div) { {
/* 当宿主或宿主的祖先元素是<div> 元素时生效。 */
}

2.文档流->组件

文档流可以通过::shadow或者/deep/来影响组件的样式。如果想要修整content元素的样式,使用::content。chrome自己使用了<<和<<<

1
2
3
4
5
6
7
8
9
10
::shadow和/deep/
<style>
#host ::shadow span {
color: red;
}
#host /deep/ span {
color: red;
}
</style>
/*为了与content一起使用的话::content*/

因为CSS目前还是全局作用域的,Shadow Dom的CSS封装很好的解决了现在CSS的一个大问题.

事件的封装

Shadow Dom对于事件通过在冒泡阶段target的重定向来封装事件,然后一些可能对页面造成影响的事件,Shadow Dom就会影藏掉这些事件,也就是在冒泡到主页面的过程中被挡住了。

图片

就像图中所示,普通点击时,target会是我们真正点击的元素,而Shadow Dom则会将事件的target重定向到宿主元素身上,主要是为了保证组件内部的封装。多层宿主的时候,每层都会重定向到自己的宿主身上。

还有一些事件不会冒泡到主文档流:abort, error, select, change, load, loadedmetadata, reset, resize,scroll and selectstart。

Shadom Dom与Virtual Dom的比较

Shadow Dom是W3c的规范,它主要被我们用来处理Dom树之间的关系,他的主要思想是封装。它本身还是Dom。

我们平常操作Dom的时候很多时候刷新操作就是将一块HTML替换,我们的操作会触发大量的repaint和reflow,这些操作都是很耗浏览器性能的。Virtual Dom是将这些操作打包,并且通过一些Diff算法来得出如何通过最简单的方式改变成我们想要的模样。它本身是Dom的一层抽象,不是真实的Dom。

Template

通过上面的Shadow Dom和Custom Elements,其实我们已经实现了组件的自定义以及封装。不过我们的模板最后一直使用的字符串。最后通过innerHtml的方式插入。包括现在其实JS模版其实全是这么实现的。这样子的坏处在于当我们多次使用一个模板的时候(比如刷新操作),每次都得把一段字符串转化为DOM结构,这其实是很费浏览器的性能的。

HTML模板定义了使用

  • 标签内的图片,等媒体资源不会被加载;
  • 标签不会出现在DOM树,审查元素看不到;

但是他又不是仅仅作为字符串存在。他是被解析成了Document Fragment。这样每次重用的时候就不会有解析为Dom这种浪费性能的操作。

Html Imports

现在我们需要的就是将这个组件打包出去。那么如何在HTML文档中引入另一个web文档或web组件呢?像JSP或PHP语言都对HTML语法进行了拓展,我们可以使用诸如标签直接引入另一个文档,然而在这之前,原生HTML
规范并不支持直接引入另一文档,通常都得通过ajax请求另一文档内容,然后通过JavaScript使用DOM API将内容插入,对于组件化开发和使用,这样显然不是我们期望的结果,这与组件的易用性是背离的,所以,HTML imports定义了如何在文档内引入和重用另一文档。

在文档内直接引入外链资源的文档或web组件,语法如下,使用标签:

<link rel="import" href="components.html">

假如在components.html中定义了got-top自定义元素,则在本文档内可以直接使用:

<go-top>GoTop</go-top>

如上,仅仅将link标签的rel属性设置成import即可,另外值得注意的是:为了避免重复执行引入文档内的脚本,对于已加载文档,import方式将跳过其加载和执行过程。

Web Components的兼容性

兼容性

如图,chrome算是很激进了,安卓也得是比较新的版本。safari,IE,FF的支持都很差

兼容性

这张图的意思的FF已经把Custom Elements和Shadow Dom立了development flag,将会去实现他。而Html Imports暂时hold on。这个和Safari暂时hold住了Custom Elements和Html Imports的原因一样。他们都觉得这个和ES6的modules解决的是同一个问题。他们在等待ES6的modules的实施效果。而最新的IE的申明则是,这三个规范都在思考中,应该是都会去实现。

Web Components的polyfill

也就是说这些个规范我们想单纯的使用时没有办法的。但是他是有组织提供了polyfill的。这个polyfill还是有很大的问题的,IE只能支持到IE11,而且shadow dom的CSS封装没有官方的支持也是没法完美实现的。

相关的框架

他的相关框架有Polymer,X-Tag,SKATEJS,Bosonic,这四个框架大部分都是对他的API的友好封装。Polymer是google出品,目前也是有15000的star的,比较火,除了封装了API,他还像Angular一样做了一层数据的双向绑定。但是这几个框架都是使用的上面的Polyfill,所以上面提高的问题他们也都有。

Web Components与React

这里我想比较一下这两者。因为React官网文档专门有一篇解释他们两者解决的是不同的问题。

Web Components个人感觉是HTML提出的模块化,他的目的是复用web组件,主要思想是封装。

React是为了搭建交互式UI,主要是针对不同的状态显示不同的View,处理的是view与data同步。

React官网文档也有实例如何在React中使用Web Components。其实就是在ComponentDidMount的时候初始化一下Web Components,很简单的使用。

Web components 与 Vuejs

这里还想提一下Vuejs,因为Vuejs自己也实现了CSS的模块化的,几乎是实现了一套Web components。他的组件的创建,注册,继承,生命周期都和Web components很像。看作者自己与其他框架比较的时候也说了,Vuejs和Polymer的区别就在于Vuejs不依赖于Web components,不需要polyfill。

总结

我觉得 Web Components作为浏览器底层特性不应该拿出来和React, vue 这类应用层框架相比较. Web Components 的方向以及提供的价值都不会跟 应用框架一致. 而 Web Components 作为未来的 Web 组件标准 , 它在任何生态中都可以运行良好. 我倒是更加期待应用层去基于 Web Components 去做更多的实现, 让组件超越框架存在, 可以在不同技术栈中使用.

总体来说,Web component他是w3c标准,基本会是组件技术的最终方向,但是需要大量的时间来让来让浏览器支持。