转载

WebComponents与Polymer

前端组件化这件事情不知道耗费了多少工程师的心血,在WebComponents之前,HTML自身并不支持模块化或组件化,但组件化的需求是客观存在的。在此之前已经有无数的解决方案。各种模板有handlebars,jade,ejs等,css解决方案bootstrap,还有前端框架比如react和angular都在解决组件化的问题。但是所有的这些方案创建的组件都不能用在其他方案中,简直是蛋疼。

从近几年一些技术发展可以得到一个趋势:一些重要的问题一定会被浏览器原生支持,而一旦浏览器原生支持,与之功能重合的工具和框架就会死去:

  • canvas,video来了Flash就死了
  • document.querySelector来了JQuery就死了(只是死的比较慢而已)
  • 原生的 <input type="date"> 来了JQuery插件datapicker就死了

那么组件化这个问题这么蛋疼,会不会被原生解决?当然,一个原生的通用组件化标准正在制定中!

WebComponents 标准

WebComponents 是一组w3c标准的集合,Google 主推。它的思想是让浏览器原生支持自定义标签,让HTML可以像模板引擎一样引入另一个html作为组件。它并不是一个框架,而是希望用户自定义的组件可以无缝的使用在其他任何框架中,WebComponents也没有绑死到任何一个框架。想象WebComponent的时代我们可以做这些事情:

  • 抛弃模板,预编译,回归本源,使用css写css,使用javascript写javascript。
  • 先写组件,再用组件拼页面。
  • 只用一个标签就可以引入任何一个组件
  • 组件分享,通过现有的bower,npm可以拿到任何共享的组件
  • 在Angular,React中使用通用的组件

然而当WebComponents到来,谁会死去?(我没有说React)

WebComponents 是一套标准集合,包含以下几个标准:

  • Shadow Dom
  • Custom Elements
  • Html Import
  • Template

其中大多数spec的状态都是Editor's Draft,意味着还没有普及大众,还有可能修改。只有Template标准是WD,正在被执行的标准。

既然是这样还说个蛋?都不支持就是还不能用啊!

暂且别急,应用的事下文会提到。我们先分析标准。

Shadow DOM

http://w3c.github.io/webcomponents/spec/shadow/

Shadow DOM 标准存在的意义是封装DOM,其实Shadow DOM在我们身边都存在很长时间了。

假如你使用Chrome或其他支持Shadow DOM的浏览器,请看以下密码输入标签:

如果看不到可以使用任何一个密码输入标签,在里面输入密码,是不可见的。通过审查元素也看不到!那么问题来了,浏览器把数据存到了哪里呢? 审查元素:

WebComponents与Polymer

马上让它现形! F12 调出开发者工具,点击右边的齿轮(设置)之后,在中间有个选项: Show user agent shadow DOM 把它勾上后再审查元素看看

WebComponents与Polymer

惊喜的发现 <input> 标签并非世界尽头,就好像发现原子里面还有结构一样。基本元素内部的结构就是Shadow DOM。input标签内部居然有个div,在这个div的content里保存的才是真正的内容。其实不止input标签,还有其他一些标签也一样。

考虑为何Chrome使用Shadow DOM。原因只有1个:一颗DOM树已经不够用了。一方面希望开发者只关注业务,另一方面页面变得越来越复杂,不得不展现更多内容。Shadow DOM对于组件化的意义非常重要,它为上层展现一个简单的界面,隐藏丰富的细节。比如input 标签,在上层看来就是一个不可分的元素,没有人会在乎数据在标签内部是怎么传输的,事件是怎么传播的,完美的达到了封装的效果,这中封装的元素在react ,angular中使用自然是没有问题的,可以做通用的组件。

上文对于Shadow Dom有了感性的认识,下面我们深入到细节: 内容全部来自于 http://w3c.github.io/webcomponents/spec/shadow/ 和自己的理解。

模型

最大的一个变化是:DOM 树形模型的扩展,以前是元素的树,现在是树的树(Tree Of Tree):

WebComponents与Polymer

一张图展示了几个概念:

  • shadow host:shadow DOM 的父级元素
  • shadow root:shadow DOM 的根元素

对应上面的 <input> 的例子可以很直观的理解: input 标签是 shadow host,而shadow root 就写在那里。

Shadow Dom 是四个规范中最难搞的一个,一方面你要在大部分API中隐藏Shadow DOM,另一方面你希望渲染的时候尽量简单,把DOM还当一整棵树来处理,把shadow DOM在渲染的时候当成DOM 树的一部分,这就是 Composed Tree。

WebComponents与Polymer

显然,把Tree Of Tree 变成一棵树需要一定的转化算法。 不属于此文的范畴,我们只需要知道转化之前是什么样子,转化之后是什么样子就行。其实也没什么,就是把shadow DOM挂在父级标签下面就好了。

确实,到目前为止还是很简单的,但马上一个怪胎出现了--slot!

什么鬼?

一个图来说明slot在compose过程中的怪异行为

WebComponents与Polymer

slot 在compose后并不存在,而是原本的位置上抢了shadow host 下的子元素作为自己的替身。其实slot本身才是替身,可以理解为占位符。

好了,理解了slot是占位符的本质,我们又问,这鬼是做什么用的?

为了host 和 shadow DOM之间传递数据,也为了方便维护,如果一个数据在host中和shadow中都存在,那么只维护宿主,在shadow中使用slot就行。就到这里,不再展开。

当前很多支持Shadow DOM的浏览器使用的是 content 标签,而不是 slot ,毕竟标准还没有定稿。

事件机制

Shadow DOM的出现让事件的机制更加复杂。

一方面由于封装,一些事件不会传播到Shadow DOM之外。

另一方面,其他事件传播的路径遵循原始的DOM,而不是composed dom。也就是说shadow root和slot 也会参与到事件路径中。

还有一点需要注意的:封装的思想在事件中依然存在。在上层DOM看来的事件的target不能包含底层DOM的细节。所以,在不同层级的树中看到同一事件的target是不一致的。这就叫做 event retargeting

再举 <input> 的例子:在全局的dom中看到的click事件的target是 <input> ,而这个事件的起源可能是shadow DOM中的 <div id=editor> ,我们都知道事件向上'冒泡'传播,在shadow DOM 之内,这个事件的target是 <div> ,而事件传播出Shadow DOM,事件的target 变成了 <input> !

再加上还有些标签有 relatedTaget 就更复杂了,不细说

样式的封装

值得注意的是,Shadow DOM的封装不仅对html起作用,对于CSS同样起作用,在Shadow DOM内定义的CSS作用于只在这个Shadow DOM之内,而不会影响其他DOM。

API扩展

Shadow DOM的出现一定影响了某些接口(当然)。

包括一个新元素 ShadowRoot 扩展了Element,增加了 attachShadow 方法,用于在一个元素下面增加一个shadow DOM等。

例子

下文一并处理吧!

Custom Elements

http://w3c.github.io/webcomponents/spec/custom/ 已经有了Shadow DOM这样的利器,你不会认为就做个 input 这么简单吧!要想使用这把大杀器,还需要自定义元素(自定义标签)这个角色。

这个标准的目标是:让开发者自己定义功能完整的DOM元素。自定义元素的本质是让开发者教浏览器新的元素定义。因此这个标准的重点是提供一系列接口,让开发者教浏览器新的元素行为如何。

首先浏览器规范从一开始就允许非标准标签的出现,只不过这些标签的类别被当作 HTMLUnknownElement 。这一点很重要,否则如果浏览器遇到不认识的标签就报错,就无法做自定义标签来,AngularJS这类框架也就不会出现。

那么自定义标签都能干毛?

首先看,既然可以教浏览器新的标签,那么浏览器就一定要有一个数据结构保存已知标签的定义,这个东西就叫 registery 就翻译成注册器吧。一个document只能有1个文档。

一个DOM元素一定要有一个接口名比如 html , body , div 等,而自定义元素的接口名必须是两部分:命名空间和本地命名,两者必须以 - 分隔(好像一定是这个样子,没有为什么)

比如 <wilddog-list></wilddog-list> 命名空间是 wilddog 而本地命名是 list

教浏览器认识新标签只告诉一个名字显然是不够的。要教会它行为。有两种方式

  • 从头开始,做一个完全不同的元素
  • 扩展一个已经存在的元素

那么这个接口被设计成来这个样子

partial interface Document {       Function registerElement(DOMString type, optional ElementRegistrationOptions options); };  dictionary ElementRegistrationOptions {        object? prototype = null;      DOMString? extends = null; }; 

现在浏览器的支持情况以及polyfill

TBD

polymer是个啥

TBD

与React对比

TBD

正文到此结束
Loading...