原文地址:
原文作者:
2018年11月15日发表于 , , , 以及
译者水平有限,如果有错误欢迎指正!
背景
自从第一个动态的 DHTML 光标拖拽的诞生,以及“本周网站”的徽章为网站增色,可复用代码对 web 开发者极具诱惑力。但是在自己的网站中引入三方 UI 组件一直是一个比较头疼的事情。
引入别人造好的轮子会带来很多 javascript 和 css冲突,想想那些可怕的 !important 吧。使用现代前端框架比如React可能会好一些,但是为了为了重用一些组件而引入一个框架显然是有些笨重。 HTML5 把一些常用的组件引入 web 标准,像 和 ,但是,为每个常用Web UI库添加新标准标签并不是一个可持续维护的方式。
这时,一些 web 标准草案就应运而生了。每个标准有其独立的功能,但是把他们组合在一起,就能解决之前不能用原生方案解决的问题,并且它们非常难伪造,因为自定义 HTML 组件可以像传统 HTML 标签一样使用。这些组件把复杂的实现封装在内部,就像富文本编辑器和视频播放器一样。
标准发展
整体来说,这组标准就是 。在2018年前端组件化并不是什么新鲜事物。的确,从2014年开始,chrome一直以这样或那样的方式实现这些标准,其他浏览器也有相应的polyfills。
在标准委员会工作了一段时间之后,Web Components 标准从早期的形式(现称为version 0版本)演变成了更成熟的version 1版本,并且被主流浏览器实现。 对此增加了两个支持:Custom Elements、shadow DOM。现在一起来看看怎么扮演 HTML 的发明家吧!
Web Components已经存在了一段时间,相关资源比较多。本文只作为初级读物,介绍一些列的新性能和资源,如果你想了解更多(你也应该了解更多),请移步 和 。
自定义 HTML 标签需要浏览器以前没赋予开发者的新功能。我将在每一单元列出这些从前不能实现的地方,以及他们所使用的其他 web 新技术。
<template>
标签: 一个小复习
第一个标签是老朋友了,它满足的需求早于 Web Components。有时你只想存储一些 HTML。也许有时你要多次复制标签,也许有时你还不想马上创建一个UI。<template>
标签包含并解析 HTML ,但不把解析出的 添加到当前文档中。
This won't display!
复制代码
那么解析出的 DOM 去哪了呢?它被添加到了“文档碎片”中,可以把它理解成一个包含 html 的薄容器。当被添加到 DOM 中时文档碎片就解体了,当你想保留一组稍后使用的标签,又不想保留其容器时,文档碎片非常有用。
“那么,我该怎么使用一个正在解体的容器中的标签呢?”
答案是:你只需把模版的文档碎片插入当前文档即可:
let template = document.querySelector('template');document.body.appendChild(template.content);复制代码
上面这段代码可以正常执行,但是如果你刚解体了文档碎片就会报错!如果你重复运行上述代码就会报错。因为第二次运行时 template.content
已经没有了。我们应该用一个碎片的拷贝代替 template.content
,然后再插入这个拷贝,代码如下:
document.body.appendChild(template.content.cloneNode(true));复制代码
cloneNode
方法顾名思义,接收一个参数控制只拷贝标签本身还是包括它的子标签。
新知识点:
<template>
标签包含 HTML,但是不向当前文档添加。
总结:
- 使用
cloneNode
Custom Elements
Custom Elements是 Web Components标准的代表。它确实让开发人员实现了自定义 HTML 标签。这一切的实现得益于 。如果你对 javascript 或者其他面向对象语言很熟悉的话,你可以像这样通过继承来实现自己的类:
class MyClass extends BaseClass { // class definition goes here}复制代码
我们来试一下这样写:
class MyElement extends HTMLElement {}复制代码
不久之前这样写还会报错。浏览器不允许原生 HTMLElement 类或其子类被继承。Custom Elements 解除了这一限制。
浏览器会把 <p>
标签映射到 HTMLParagraphElement 原生类,但是它怎么映射自定义类呢?除了继承内部类外,还有一个“自定义标签注册表”用于声明这种映射:
customElements.define('my-element', MyElement);复制代码
现在页面上的每个 <my-element>
标签都与一个 MyElement 元素对应。 页面每解析一个 <my-element>
标签就调用一次 MyElement 的构造函数。
为什么标签名带中横线呢?标准制定者希望未来开发者可以自由的自定义标签,这意味着开发者都可以创建 <h7>
或者 <vr>
这样的标签。为了避免未来的冲突,所有自定义标签必须加中横线,同时原生 HTML 标签保证绝不包含中横线。问题解决!
除了标签创建时会调用构造函数,还有一系列生命周期函数会在特定时刻被调用:
connectedCallback
当元素被添加到文档中时调用。这个函数可能多次调用,比如标签移动、移除或重新添加时。disconnectedCallback
与connectedCallback
相对应。attributeChangeCallback
元素属性更改时调用。
下面是一个稍复杂的例子:
class GreetingElement extends HTMLElement { constructor() { super(); this._name = 'Stranger'; } connectedCallback() { this.addEventListener('click', e => alert(`Hello, ${this._name}!`)); } attributeChangedCallback(attrName, oldValue, newValue) { if (attrName === 'name') { if (newValue) { this._name = newValue; } else { this._name = 'Stranger'; } } }}GreetingElement.observedAttributes = ['name'];customElements.define('hey-there', GreetingElement);复制代码
在页面上这样使用:
Greeting Personalized Greeting 复制代码
如果要继承一个 HTML 原生标签,你可能会想定义一个看起来完全不同新标签。比如让 <hey-there>
去继承 <button>
:
class GreetingElement extends HTMLButtonElement复制代码
同时要在自定义标签注册表中体现出继承一个已有标签:
customElements.define('hey-there', GreetingElement, { extends: 'button' });复制代码
我们应该用被继承的标签加 is
属性来表示这种继承关系,而不是直接用自定义标签,我们这样使用继承 <button>
的 <hey-there>
标签:
复制代码
这不是多此一举,这样程序就会知道 <hey-there>
是继承的 <button>
。
这些对所有的传统 web 标签都适用。我们可以使用 <template>
设置一系列事件处理程序,添加自定义样式,甚至可以封装一个内部结构。其他人可以通过 HTML 标签、 DOM 调用、或者新框架(其中一些框架支持在虚拟 DOM 中自定义标签名)的方式在自己的代码中引用你的自定义组件。因为这些都是标准的 DOM 接口,所以 Custom Elements 实现了真正的可移植组件。
新知识点:
- Custom Elements 可以继承原生 HTMLElement 类和其子类。
- 通过
customElements.define()
维护自定义标签注册表。 - 特定生命周期函数在标签创建、添加到DOM、属性被修改等时刻调用。
总结: 特别是
Shadow DOM
我们写出了友好的 custom element
,也为其添加了漂亮的样式。现在我们想把它用在我们的站点上,也想把代码分享出去,让更多的人用在他们的网站上。但是我们怎么避免自定义 <button>
标签和其他网站的 css 冲突?答案是使用 Shadow DOM。
Shadow DOM 标准提出了 shadow root 的概念。shadow root 有标准的 DOM 方法,也可以像其他 DOM 节点一样添加到文档中。shadow root 的亮点在于其内容不会出现在包含其父节点的文档中:
// attachShadow creates a shadow root.let shadow = div.attachShadow({ mode: 'open' });let inner = document.createElement('b');inner.appendChild(document.createTextNode('Hiding in the shadows'));// shadow root supports the normal appendChild method.shadow.appendChild(inner);div.querySelector('b'); // empty复制代码
在上面的例子中,<div>
包含 <b>
并且 <b>
标签也渲染在了页面上,但是常规的 DOM 方法却找不到它。不仅如此,页面的样式也影响不到它。这意味着 shadow root 既不受外部样式影响,其内部样式也不会泄漏。但这边界不涉及安全性,页面上的 js 可以检测到 shadow root 的创建,通过 shadow root 的引用,可以查询到它里面的内容。
为 shadow root 里的内容设置样式可以通过给根节点添加<style>
(或者 <link>
)标签:
let style = document.createElement('style');style.innerText = 'b { font-weight: bolder; color: red; }';shadowRoot.appendChild(style);let inner = document.createElement('b');inner.innerHTML = "I'm bolder in the shadows";shadowRoot.appendChild(inner);复制代码
现在我们可以真正使用 <template>
标签了!不管用哪种方法,shadow root 内部的 <b>
标签样式只会被根标签上的样式控制,不会受外部影响。
如果 custom element 不使用 shadow DOM 怎么办?我们依然可以使用一个新标签 <slot>
:
Hello,! 复制代码
如果这个模板被添加到一个 shadow root 中,那么下述标签:
World 复制代码
将被渲染为:
Hello, World!复制代码
这种将 shadow DOM 和非 shadow DOM 整合使用的功能,可以把 custom element 复杂的实现封装在其内部,而把调用变的简单。slot
的威力远不止这些,还有多重 slot
、命名 slot
、针对特定内容的 css 伪类 slot
等。建议查阅文档了解更多。
新知识点
- 一种准屏蔽 DOM 结构 —— shadow root
- 创建和访问shadow root 的 DOM API
- shadow root 的样式作用域
- 用于shadow root 和样式作用域的新
最终效果
最后来一起实现这个漂亮的按钮吧!我们给这个按钮取名 <fancy-button>
。它的奇妙之处在于,它有定制的样式,也允许我们为它添加图标使它变得美观。我们把样式封装在 shadow root 中,这样就可以保证在任何引用它的网站上样式保持不变。
你可以查看下面这个完整的交互型代码示例。请仔细查看 custom element 的 js 定义以及 <template>
标签的样式和结构。
总结
Web Components标准建立在这样一种理念之上:提供多个底层功能,开发者以标准制定者未曾设想的方式把这些功能组合起来使用。Custom Elements 已经被用于在页面上、等,并使这些变得简单。尽管标准的敲定过程很漫长,Web Components 标准为 Web 开发者提供了更多的可能。现代浏览器已经支持了这项技术,Web Components 的未来在你手中,使用它来创造奇迹吧!