Web Components 是个什么样的东西

我在掘金发这个文章的时候,已经有人转载了我在 segmentfault 先发的版本:segmentfault.com/a/119000000… ,为了防止误会,在这里简单说明一下。javascript

前端组件化这个主题相关的内容已经火了好久好久,angular 刚出来时的 Directive 到 angular2 的 components,还有 React 的 components 等等,无一不是前端组件化的一种实现和探索,可是提上议程的 Web Components 标准是个怎样的东西,相关的一些框架或者类库,如 React,Angular2,甚至是 x-tag,polymer 如今实现的组件化的东西和 Web Components 标准差异在哪里?我花时间努力地把现有的 W3C Web Components 文档看了下,而后坚强地写下这些记录。css

首先咱们须要知道,Web Components 包括了四个部分:html

这四部分有机地组合在一块儿,才是 Web Components。前端

能够用自定义的标签来引入组件是前端组件化的基础,在页面引用 HTML 文件和 HTML 模板是用于支撑编写组件视图和组件资源管理,而 Shadow DOM 则是隔离组件间代码的冲突和影响。vue

下边分别是每一部分的笔记内容。java

Custom Elements

概述

Custom Elements 顾名思义,是提供一种方式让开发者能够自定义 HTML 元素,包括特定的组成,样式和行为。支持 Web Components 标准的浏览器会提供一系列 API 给开发者用于建立自定义的元素,或者扩展示有元素。node

这一项标准的草案还处于不稳定的状态,时有更新,API 还会有所变化,下边的笔记以 Cutsom Elements 2016.02.26 这个版本为准,由于在最新的 chrome 浏览器已是能够工做的了,这样可使用 demo 来作尝试,最后我会再简单写一下最新文档和这个的区别。react

registerElement

首先,咱们能够尝试在 chrome 控制台输入 HTMLInputElement,能够看到是有这么一个东西的,这个理解为 input DOM 元素实例化时的构造函数,基础的是 HTMLElementgit

Web Components 标准提出提供这么一个接口:github

document.registerElement('x-foo', {
  prototype: Object.create(HTMLElement.prototype, {
    createdCallback: {      
      value: function() { ... }
    },
    ...
  })
})复制代码

你可使用 document.registerElement 来注册一个标签,标准中为了提供 namesapce 的支持,防止冲突,规定标签类型(也能够理解为名字)须要使用 - 链接。同时,不能是如下这一些:

  • annotation-xml
  • color-profile
  • font-face
  • font-face-src
  • font-face-uri
  • font-face-format
  • font-face-name
  • missing-glyph

第二个参数是标签相关的配置,主要是提供一个 prototype,这个原型对象是以 HTMLElement 等的原型为基础建立的对象。而后你即可以在 HTML 中去使用自定义的标签。如:

<div>
  <x-foo></x-foo>
</div>复制代码

是否是嗅到了 React 的味道?好吧,React 说它本身主要不是作这个事情的。

生命周期和回调

在这个 API 的基础上,Web Components 标准提供了一系列控制自定义元素的方法。咱们来一一看下:

一个自定义元素会经历如下这些生命周期:

  • 注册前建立
  • 注册自定义元素定义
  • 在注册后建立元素实例
  • 元素插入到 document 中
  • 元素从 document 中移除
  • 元素的属性变化时

这个是很重要的内容,开发者能够在注册新的自定义元素时指定对应的生命周期回调来为自定义元素添加各类自定义的行为,这些生命周期回调包括了:

  • createdCallback 自定义元素注册后,在实例化以后会调用,一般多用于作元素的初始化,如插入子元素,绑定事件等。

  • attachedCallback 元素插入到 document 时触发。

  • detachedCallback 元素从 document 中移除时触发,可能会用于作相似 destroy 之类的事情。

  • attributeChangedCallback 元素属性变化时触发,能够用于从外到内的通讯。外部经过修改元素的属性来让内部获取相关的数据而且执行对应的操做。 这个回调在不一样状况下有对应不一样的参数:

    • 设置属性时,参数列表是:属性名称,null,值,命名空间
    • 修改属性时,参数列表是:属性名称,旧值,新值,命名空间
    • 删除属性时,参数列表是:属性名称,旧值,null,命名空间

好了,就上边了解到的基础上,假设咱们要建立一个自定义的 button-hello 按钮,点击时会 alert('hello world'),代码以下:

document.registerElement('button-hello', {
  prototype: Object.create(HTMLButtonElement.prototype, {
    createdCallback: {
      value: function createdCallback() {
        this.innerHTML = '<button>hello world</button>'
        this.addEventListener('click', () => {
          alert('hello world')
        })
      }
    }
  })
})复制代码

要留意上述代码执行以后才能使用 <button-hello></button-hello>

扩展原有元素

其实,若是咱们须要一个按钮,彻底不须要从新自定义一个元素,Web Components 标准提供了一种扩展示有标签的方式,把上边的代码调整一下:

document.registerElement('button-hello', {
  prototype: Object.create(HTMLButtonElement.prototype, {
    createdCallback: {
      value: function createdCallback() {
        this.addEventListener('click', () => {
          alert('hello world')
        })
      }
    }
  }),
  extends: 'button'
})复制代码

而后在 HTML 中要这么使用:

<button is="button-hello">hello world</button>复制代码

使用 is 属性来声明一个扩展的类型,看起来也蛮酷的。生命周期和自定义元素标签的保持一致。

当咱们须要多个标签组合成新的元素时,咱们可使用自定义的元素标签,可是若是只是须要在原有的 HTML 标签上进行扩展的话,使用 is 的这种元素扩展的方式就好。

原有的 createElementcreateElementNS,在 Web Components 标准中也扩展成为支持元素扩展,例如要建立一个 button-hello

const hello = document.createElement('button', 'button-hello')复制代码

标准文档中还有不少细节上的内容,例如接口的参数说明和要求,回调队列的实现要求等,这些更可能是对于实现这个标准的浏览器开发者的要求,这里不作详细描述了,内容不少,有兴趣的自行查阅:Cutsom Elements 2016.02.26

和最新版的区别

前边我提到说文档的更新变化很快,截止至我写这个文章的时候,最新的文档是这个:Custom Elements 2016.07.21

细节不作描述了,讲讲我看到的最大变化,就是向 ES6 靠拢。大体有下边三点:

  • 从本来的扩展 prototype 来定义元素调整为建议使用 class extends 的方式
  • 注册自定义元素接口调整,更加方便使用,传入 type 和 class 便可
  • 生命周期回调调整,createdCallback 直接用 class 的 constructor

前两个点,咱们直接看下代码,本来的代码按照新的标准,应该调整为:

class ButtonHelloElement extends HTMLButtonElement {
  constructor() {
    super()

    this.addEventListener('click', () => {
      alert('hello world')
    })
  }
}

customElements.define('button-hello', ButtonHelloElement, { extends: 'button' })复制代码

从代码上看会感受更加 OO,编写上也比本来要显得方便一些,本来的生命周期回调是调整为新的:

  • constructor in class 做用至关于本来的 createdCallback
  • connectedCallback 做用至关于 attachedCallback
  • disconnectedCallback 做用至关于 detachedCallback
  • adoptedCallback 使用 document.adoptNode(node) 时触发
  • attributeChangedCallback 和本来保持一致

connect 事件和插入元素到 document 有些许区别,主要就是插入元素到 document 时,元素状态会变成 connected,这时会触发 connectedCallback,disconnect 亦是如此。

HTML Imports

概述

HTML Imports 是一种在 HTMLs 中引用以及复用其余的 HTML 文档的方式。这个 Import 很漂亮,能够简单理解为咱们常见的模板中的 include 之类的做用。

咱们最多见的引入一个 css 文件的方式是:

<link rel="stylesheet" href="/css/master.css">复制代码

Web Components 如今提供多了一个这个:

<link rel="import" href="/components/header.html">复制代码

HTMLLinkElement

本来的 link 标签在添加了 HTML Import 以后,多了一个只读的 import 属性,当出现下边两种状况时,这个属性为 null

  • link 不是用来 import 一个 HTML 的。
  • link 元素不在 document 中。

不然,这个属性会返回一个表示引入的 HTML 文件的文档对象,相似于 document。好比说,在上边的代码基础上,能够这样作:

const link = document.querySelector('link[rel=import]')
const header = link.import;

const pulse = header.querySelector('div.logo');复制代码

阻塞式

咱们要知道的是,默认的 link 加载是阻塞式的,除非你给他添加一个 async 标识。

阻塞式从某种程度上讲是有必要的,当你 improt 的是一个完整的自定义组件而且须要在主 HTML 中用标签直接使用时,非阻塞的就会出现错误了,由于标签尚未被注册。

document

有一点值得留意的是,在 import 的 HTML 中,咱们编写的 script 里边的 document 是指向 import 这个 HTML 的主 HTML 的 document。

若是咱们要获取 import 的 HTML 的 document 的话,得这么来:

const d = document.currentScript.ownerDocument复制代码

这样设计是由于 import 进来的 HTML 须要用到主 HTML 的 document。例如咱们上边提到的 registerElement

在一个被 import 的 HTML 文件中使用下边三个方法会抛出一个 InvalidStateError 异常:

  • document.open()
  • document.write()
  • document.close()

对于 HTML Import,标准文档中还有很大一部份内容是关于多个依赖加载的处理算法的,在这里就不详述了,有机会的话找时间再开篇谈,这些内容是须要浏览器去实现的。

HTML Templates

概述

这个东西很简单,用过 handlebars 的人都知道有这么一个东西:

<script id="template" type="text/x-handlebars-template"> ... </script>复制代码

其余模板引擎也有相似的东西,那么 HTML Templates 即是把这个东西官方标准化,提供了一个 template 标签来存放之后须要可是暂时不渲染的 HTML 代码。

之后能够这么写了:

<template id="template">
  ...
</template>复制代码

接口和应用

template 元素有一个只读的属性 content,用于返回这个 template 里边的内容,返回的结果是一个 DocumentFragment

具体是如何使用的,直接参考官方给出的例子:

<!doctype html>
<html lang="en">
  <head>
    <title>Homework</title>
  <body>
    <template id="template"><p>Smile!</p></template>
    <script> let num = 3; const fragment = document.getElementById('template').content.cloneNode(true); while (num-- > 1) { fragment.firstChild.before(fragment.firstChild.cloneNode(true)); fragment.firstChild.textContent += fragment.lastChild.textContent; } document.body.appendChild(fragment); </script>
</html>复制代码

使用 DocumentFragment 的 clone 方法以 template 里的代码为基础建立一个元素节点,而后你即可以操做这个元素节点,最后在须要的时候插入到 document 中特定位置即可以了。

Template 相关的东西很少,并且它如今已是归入生效的 标准文档 中了。

咱们接下来看看重磅的 Shadow DOM。

Shadow DOM

概述

Shadow DOM 好像提出很久了,最本质的需求是须要一个隔离组件代码做用域的东西,例如我组件代码的 CSS 不能影响其余组件之类的,而 iframe 又过重而且可能有各类奇怪问题。

能够这么说,Shadow DOM 旨在提供一种更好地组织页面元素的方式,来为日趋复杂的页面应用提供强大支持,避免代码间的相互影响。

看下在 chrome 它会是咋样的:

Shadow DOM in chrome

咱们能够经过 createShadowRoot() 来给一个元素节点建立 Shadow Root,这些元素类型必须是下边列表的其中一个,不然会抛出 NotSupportedError 异常。

  • 自定义的元素
  • article
  • aside
  • blockquote
  • body
  • div
  • header, footer
  • h1, h2, h3, h4, h5, h6
  • nav
  • p
  • section
  • span

createShadowRoot() 是如今 chrome 实现的 API,来自文档:www.w3.org/TR/2014/WD-… API 已经调整为 attachShadow()

返回的 Shadow Root 对象从 DocumentFragment 继承而来,因此可使用相关的一些方法,例如 shadowRoot.getElementById('id') 来获取 Shadow DOM 里边的元素。

简单的使用以下:

const div = document.getElementById('id')
const shadowRoot = div.createShadowRoot()
const span = document.createElement('span')

span.textContent = 'hello world'
shadowRoot.appendChild(span)复制代码

在这里,我把这个 div 成为是这个 Shadow DOM 的 宿主元素,下边的内容会延续使用这个称呼。

Shadow DOM 自己就为了代码隔离而生,因此在 document 上使用 query 时,是无法获取到 Shadow DOM 里边的元素的,须要在 Shadow Root 上作 query 才行。

在这里附上一个文档,里边有详细的关于新的标准和如今 blink 引擎实现的 Shadow DOM 的区别,官方上称之为 v0 和 v1:Shadow DOM v1 in Blink

API

Shadow Root 除了从 DocumentFragment 继承而来的属性和方法外,还多了另外两个属性:

  • host 只读属性,用来获取这个 Shadow Root 所属的元素
  • innerHTML 用来获取或者设置里边的 HTML 字符串,和咱们经常使用的 element.innerHTML 是同样的

另外,在最新的标准文档中,元素除了上边提到的 attachShadow 方法以外,还多了三个属性:

  • assignedSlot 只读,这个元素若是被分配到了某个 Shadow DOM 里边的 slot,那么会返回这个对应的 slot 元素
  • slot 元素的 slot 属性,用来指定 slot 的名称
  • shadowRoot 只读,元素下面对应的 Shadow Root 对象

slot 是什么?接着看下边的内容,看完下一节的最后一部分就会明白上述内容和 slot 相关的两个 API 有什么做用。

slot

slot 提供了在使用自定义标签的时候能够传递子模板给到内部使用的能力,能够简单看下 Vue 的一个例子。

咱们先来看下如今 chrome 能够跑的 v0 版本,这一个版本是提供了一个 content 标签,表明了一个占位符,而且有一个 select 属性用来指定使用哪些子元素。

<!-- component input-toggle template -->
<input type="checkbox"></input>
<content select=".span"></content>复制代码

自定义的元素里边的子元素代码是这样的:

<input-toggle name="hello">
  <span>hello</span>
  <span class="span">test</span>
</input-toggle>复制代码

那么展示的结果会和下边的代码是同样的:

<input-toggle name="hello">
  <input type="checkbox"></input>
  <span class="span">test</span>
</input-toggle>复制代码

这里只是说展示结果,实际上,input-toggle 里边应该是建立了一个 Shadow DOM,而后 content 标签引用了目标的 span 内容,在 chrome 看是这样的:

content tag in chrome

而后,是最新标准中的 slot 使用方式,直接上例子代码:

<!-- component input-toggle template -->
<input type="checkbox"></input>
<slot name="text"></slot>复制代码

在自定义的元素标签是这么使用 slot 的:

<input-toggle name="hello">
  <input type="checkbox"></input>
  <span class="span" slot="text">test</span>
</input-toggle>复制代码

经过 slot="text" 的属性来让元素内部的 slot 占位符能够引用到这个元素,多个元素使用这个属性也是能够的。这样子咱们便拥有了使用标签是从外部传 template 给到自定义元素的内部去使用,并且具有指定放在那里的能力。

CSS 相关

由于有 Shadow DOM 的存在,因此在 CSS 上又添加了不少相关的东西,其中一部分仍是属于讨论中的草案,命名之类的可能会有变动,下边说起的内容主要来自文档:Shadow DOM in CSS scoping 1,不少部分在 chrome 是已经实现的了,有兴趣能够写 demo 试试。

由于 Shadow DOM 很大程度上是为了隔离样式做用域而诞生的,主文档中的样式规则不对 Shadow DOM 里的子文档生效,子文档中的样式规则也不影响外部文档。

但不可避免的,在某些场景下,咱们须要外部能够控制 Shadow DOM 中样式,如提供一个组件给你,有时候你会但愿能够自定义它内部的一些样式,同时,Shadow DOM 中的代码有时候可能须要可以控制其所属元素的样式,甚至,组件内部能够定义上边提到的经过 slot 传递进来的 HTML 的样式。因此呢,是的,CSS 选择器中添加了几个伪类,咱们一一来看下它们有什么做用。

在阅读下边描述的时候,请留意一下选择器的代码是在什么位置的,Shadow DOM 内部仍是外部。

:host 用于在 Shadow DOM 内部选择到其宿主元素,当它不是在 Shadow DOM 中使用时,便匹配不到任意元素。

在 Shadow DOM 中的 * 选择器是没法选择到其宿主元素的。

:host( <selector> ) 括号中是一个选择器,这个能够理解为是一个用于兼容在主文档和 Shadow DOM 中使用的方法,当这个选择器在 Shadow DOM 中时,会匹配到括号中选择器对应的宿主元素,若是不是,则匹配括号中选择器可以匹配到的元素。

文档中提供了一个例子:

<x-foo class="foo">
  <"shadow tree">
    <div class="foo">...</div>
  </>
</x-foo>复制代码

在这个 shadow tree 内部的样式代码中,会有这样的结果:

  • :host 匹配 <x-foo> 元素
  • x-foo 匹配不到元素
  • .foo 只匹配到 <div> 元素
  • .foo:host 匹配不到元素
  • :host(.foo) 匹配 <x-foo> 元素

:host-context( <selector> ),用于在 Shadow DOM 中来检测宿主元素的父级元素,若是宿主元素或者其祖先元素可以被括号中的选择器匹配到的话,那么这个伪类选择器便匹配到这个 Shadow DOM 的宿主元素。我的理解是用于在宿主元素外部元素知足必定的条件时添加样式。

::shadow 这个伪类用于在 Shadow DOM 外部匹配其内部的元素,而 /deep/ 这个标识也有一样的做用,咱们来看一个例子:

<x-foo>
   <"shadow tree">
     <div>
       <span id="not-top">...</span>
     </div>
     <span id="top">...</span>
   </>
 </x-foo>复制代码

对于上述这一段代码的 HTML 结构,在 Shadow DOM 外部的样式代码中,会是这样的:

  • x-foo::shadow > span 能够匹配到 #top 元素
  • #top 匹配不到元素
  • x-foo /deep/ span 能够匹配到 #not-top#top 元素

/deep/ 这个标识的做用和咱们的 > 选择器有点相似,只不过它是匹配其对应的 Shadow DOM 内部的,这个标识可能还会变化,例如改为 >> 或者 >>> 之类的,我的感受, >> 会更舒服。

最后一个,用于在 Shadow DOM 内部调整 slot 的样式,在我查阅的这个文档中,暂时是以 chrome 实现的为准,使用 ::content 伪类,不排除有更新为 ::slot 的可能性。咱们看一个例子来了解一下,就算名称调整了也是差很少的用法:

<x-foo>
  <div id="one" class="foo">...</div>
  <div id="two">...</div>
  <div id="three" class="foo">
    <div id="four">...</div>
  </div>
  <"shadow tree">
    <div id="five">...</div>
    <div id="six">...</div>
    <content select=".foo"></content>
  </"shadow tree">
</x-foo>复制代码

在 Shadow DOM 内部的样式代码中,::content div 能够匹配到 #one#three#four,留意一下 #two 为何没被匹配到,由于它没有被 content 元素选中,即不会进行引用。若是更换成 slot 的 name 引用的方式亦是同理。

层叠规则,按照这个文档的说法,对于两个优先级别同样的 CSS 声明,没有带 !important 的,在 Shadow DOM 外部声明的优先级高于在 Shadow DOM 内部的,而带有 !important 的,则相反。我的认为,这是提供给外部必定的控制能力,同时让内部能够限制必定的影响范围。

继承方面相对简单,在 Shadow DOM 内部的顶级元素样式从宿主元素继承而来。

至此,Web Components 四个部分介绍结束了,其中有一些细节,浏览器实现细节,还有使用上的部分细节,是没有说起的,由于详细记录的话,还会有不少东西,内容不少。当使用过程当中有疑问时能够再次查阅标准文档,有机会的话会再完善这个文章。下一部分会把这四个内容组合起来,总体看下 Web Components 是怎么使用的。

Web Components

Web Components 总的来讲是提供一整套完善的封装机制来把 Web 组件化这个东西标准化,每一个框架实现的组件都统一标准地进行输入输出,这样能够更好推进组件的复用。结合上边各个部分的内容,咱们整合一块儿来看下应该怎么使用这个标准来实现咱们的组件:

<!-- components/header.html -->
<template id="">
<style> ::content li { display: inline-block; padding: 20px 10px; } </style>
<content select="ul"></content>
</template>
<script> (function() { const element = Object.create(HTMLInputElement.prototype) const template = document.currentScript.ownerDocument.querySelector('template') element.createdCallback = function() { const shadowRoot = this.createShadowRoot() const clone = document.importNode(template.content, true) shadowRoot.appendChild(clone) this.addEventListener('click', function(event) { console.log(event.target.textContent) }) } document.registerElement('test-header', { prototype: element }) })() </script>复制代码

这是一个简单的组件的例子,用于定义一个 test-header,而且给传递进来的子元素 li 添加了一些组件内部的样式,同时给组件绑定了一个点击事件,来打印点击目标的文本内容。

看下如何在一个 HTML 文件中引入而且使用一个组件:

<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title></title>

    <link rel="import" href="components/header.html">
  </head>
  <body>
    <test-header>
      <ul>
        <li>Home</li>
        <li>About</li>
      </ul>
    </test-header>
  </body>
</html>复制代码

一个 import<link> 把组件的 HTML 文件引用进来,这样会执行组件中的脚本,来注册一个 test-header 元素,这样子咱们即可以在主文档中使用这个元素的标签。

上边的例子是能够在 chrome 正常运行的。

因此,根据上边简单的例子能够看出,各个部分的内容是有机结合在一块儿,Custom Elements 提供了自定义元素和标签的能力,template 提供组件模板,import 提供了在 HTML 中合理引入组件的方式,而 Shadow DOM 则处理组件间代码隔离的问题。

不得不认可,Web Components 标准的提出解决了一些问题,必须交由浏览器去处理的是 Shadow DOM,在没有 Shadow DOM 的浏览器上实现代码隔离的方式多多少少有缺陷。我的我以为组件化的各个 API 不够简洁易用,依旧有 getElementById 这些的味道,可是交由各个类库去简化也能够接受,而 import 功能上没问题,可是加载多个组件时性能问题仍是值得商榷,标准可能须要在这个方面提供更多给浏览器的指引,例如是否有可能提供一种单一请求加载多个组件 HTML 的方式等。

在如今的移动化趋势中,Web Components 不只仅是 Web 端的问题,愈来愈多的开发者指望以 Web 的方式去实现移动应用,而多端复用的实现渐渐是以组件的形式铺开,例如 React NativeWeex。因此 Web Components 的标准可能会影响到多端开发 Web 化的一个模式和发展。

最后,再啰嗦一句,Web Components 我的以为仍是将来发展趋势,因此才有了这个文章。

相关文章
相关标签/搜索