对开发者来说,复用别人优秀的代码已经是一个标配了,将第三方UI整合到你的网站里也是一个让人头疼的事情。要使用别人优秀代码需要引入一大块的JavaScript和包含恐怖的!important的CSS代码。虽然像React和一些其他现代化的前端框架,但是经常会引入一个大的框架,结果只用了一个小组件。
为了增强用户自定义HTML元素的能力,一些web标准已经起草了。每个标准都有各自依赖的工具方法,当这些标准合并到一起的时候,使得原生支持有可能:用户自定义的元素像传统HTML元素的能力。
目前,Chrome和Firefox支持了Custom Elements
和Shadow DOM
。本文中会以这两个标准为基础,向你展示如何利用这两个特性来制作自定义标签。
<template />
:刷新器 第一个介绍的是<template>
标签,它通常可用于复制多次的HTML代码片段,它携带HTML标签且不会讲编译后的DOM添加到当前文档中。
<template>
<h1>This won't display!</h1>
<script>alert("this won't alert!");</script>
</template>
<template>
将编译之后的HTML添加到一个“document fragment”(包含一部分HTML文档的轻量容器)中,“document fragment”被添加进其他DOM中才会展开,所以它们对于想要暂存一些元素非常有用。
“如果我有一些DOM在一个展开的容器,当我需要的时候我要怎么做呢?”
你可以很简单地将template内的document fragment 插入到当前文档中。
let template = document.querySelector('template');
document.body.appendChild(template.content);
上面这段代码运行起来没问题,你已经展开释放了这个document fragment。如果这段代码运行两次,就会有一个报错,因为第二次的时候 template.content 已经是空了。所以,如果你想要做一个可复制的片段来插入的话,你需要这样:
document.body.appendChild(template.content.cloneNode(true));
cloneNode这个方法有一个参数,用来确定是只是拷贝代码本身还是将它的子元素都包含进去。
template标签在重复使用HTML结构片段的场景下是很理想的解决方案,它可以让你在定义组件的内部构造时派上用场,所以<template>
会被引入Web Components俱乐部。
Custom Elements(自定义标签)
Custom Elements是Web Components标准的一个代表性部分。它可以让开发者定义自己自定义的HTML标签。它是建立在ES6的class语法上,如果你熟悉JavaScript或其他语言的的class,你就可以继承或使用其他class类来拓展自定义标签。
class MyClass extends BaseClass {
// 类在这里定义
}
// 或者
class MyElement extends HTMLElement {}
浏览器不允许内置的HTMLElement类或子类被继承,Custom Elements打破了这个限制。
要定义一个标签,需要一个“自定义标签注册表”来声明自定义标签的映射类,代码见下图。
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);
可以用下面的方式使用标签。
<hey-there>Greeting</hey-there>
<hey-there name="Potch">Personalized Greeting</hey-there>
Shadow DOM
我们已经有了友好的Custom Elemen,我们曾经遇到过一些非常漂亮的样式,想把它用在我们的网站上,并可以与其他人共享代码。当我们自定义的<button>
元素第一次合到其他网站的CSS时,我们怎么才能避免合并时的噩梦呢?Shadow DOM提供了解决方案。
Shadow DOM标准引入了“shadow root”的概念。一个shadow root有标准的DOM函数,并且可以被添加到其他DOM节点中。Shadow root只显示他们的内容,且不会在包含它们父节点的文档中出现。
// attachShadow 函数创建一个 shadow root.
let shadow = div.attachShadow({ mode: 'open' });
let inner = document.createElement('b');
inner.appendChild(document.createTextNode('Hiding in the shadows'));
// shadow root 支持标准的appendChild方法
shadow.appendChild(inner);
div.querySelector('b'); // 空的
在上面的例子中,<div>
包含<b>
,且<b>
被渲染到页面中,但是传统的DOM函数不能读取到它。不止是它,容器页面的样式也不能被读取到。这意味着一个shadow root外面的样式不能进去,shadow root里面的样式也不会渗出。这边边界不是说它是一个安全性,而是页面上其他的脚本不能取到shadow root的创建,如果你有一个对shadow root的引用,那你就可以直接读取它的内容。
Shadow root的内容的样式,需要添加单独的<style>
(或<link>
)到root中。如下:
let style = document.createElement('style');
style.innerText = 'b { font-weight: bolder; color: red; }';
shadowRoot.appendChild(style);
let inner = document.createElement('b');
inner.innerHTML = "这是shadow中的粗体文本";
shadowRoot.appendChild(inner);
我们现在可以正确地用<template>
了。<b>
会被root内的样式影响,而不会被任何root之外的<b>
的样式影响。
如果一个自定义标签没有shadow的内容呢?我们可以用一个特殊的元素(叫<slot>
)来将它们合到一起。
<template>
Hello, <slot></slot>!
</template>
如果上面这个<template>
被吸附到一个shadow root上,那么下面的代码:
<hey-there>World</hey-there>
会被渲染成:Hello, World! 。
将shadow root和非shadow内容混合到一起的能力可以让你制作复杂内部结构且从外部看起来很简单的的丰富的自定义元素。<slot>
有更加丰富的能力,这里只是介绍了一个简单的用法,想要了解更多的slot的能力,可以去查看详细的文档。
结论
Web Components的标准是建立在为了提供多种底层能力,可以让开发者将它们整合到一起。Custom Element已经被应用到创建VR内容,让创建VR变得更简单,并衍生出了多个UI组件库,Web Components未来会创作者手里发挥出更大的能力。