-
在注册后创建元素实例
-
元素插入到 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 = ‘hello world’
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 中要这么使用:
hello world
使用 is
属性来声明一个扩展的类型,看起来也蛮酷的。生命周期和自定义元素标签的保持一致。
当我们需要多个标签组合成新的元素时,我们可以使用自定义的元素标签,但是如果只是需要在原有的 HTML 标签上进行扩展的话,使用 is
的这种元素扩展的方式就好。
原有的 createElement
和 createElementNS
,在 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 文件的方式是:
Web Components 现在提供多了一个这个:
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 的人都知道有这么一个东西:
其他模板引擎也有类似的东西,那么 HTML Templates 便是把这个东西官方标准化,提供了一个 template
标签来存放以后需要但是暂时不渲染的 HTML 代码。
以后可以这么写了:
…
接口和应用
template 元素有一个只读的属性 content
,用于返回这个 template 里边的内容,返回的结果是一个 DocumentFragment
。
具体是如何使用的,直接参考官方给出的例子:
<!doctype html>
Smile!
使用 DocumentFragment
的 clone 方法以 template
里的代码为基础创建一个元素节点,然后你便可以操作这个元素节点,最后在需要的时候插入到 document 中特定位置便可以了。
Template 相关的东西不多,而且它现在已经是纳入生效的 标准文档 中了。
我们接下来看看重磅的 Shadow DOM。
Shadow DOM
概述
Shadow DOM 好像提出好久了,最本质的需求是需要一个隔离组件代码作用域的东西,例如我组件代码的 CSS 不能影响其他组件之类的,而 iframe
又太重并且可能有各种奇怪问题。
可以这么说,Shadow DOM 旨在提供一种更好地组织页面元素的方式,来为日趋复杂的页面应用提供强大支持,避免代码间的相互影响。
看下在 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,来自文档:https://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
属性用来指定使用哪些子元素。
自定义的元素里边的子元素代码是这样的:
hello
test
那么展现的结果会和下边的代码是一样的:
test
这里只是说展现结果,实际上,input-toggle
里边应该是创建了一个 Shadow DOM,然后 content
标签引用了目标的 span
内容,在 chrome 看是这样的:
然后,是最新标准中的 slot 使用方式,直接上例子代码:
在自定义的元素标签是这么使用 slot 的:
test
通过 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 中时,会匹配到括号中选择器对应的宿主元素,如果不是,则匹配括号中选择器能够匹配到的元素。
文档中提供了一个例子:
<“shadow tree”>
</>
在这个 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/
这个标识也有同样的作用,我们来看一个例子:
<“shadow tree”>
…
…
</>
的 * 选择器是无法选择到其宿主元素的。
:host( <selector> )
括号中是一个选择器,这个可以理解为是一个用于兼容在主文档和 Shadow DOM 中使用的方法,当这个选择器在 Shadow DOM 中时,会匹配到括号中选择器对应的宿主元素,如果不是,则匹配括号中选择器能够匹配到的元素。
文档中提供了一个例子:
<“shadow tree”>
</>
在这个 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/
这个标识也有同样的作用,我们来看一个例子:
<“shadow tree”>
…
…
</>