简介
网页严重缺乏表现力。要理解我的意思,不妨了解一下 Gmail 这样的“现代”网络应用:
<div>
汤没有任何现代元素。不过,我们还是通过这种方式来构建 Web 应用。好难过。
我们不应该对我们的平台提出更高的要求吗?
性感标记。我们来解决这个问题
HTML 是一款出色的文档构建工具,但其词汇仅限于 HTML 标准定义的元素。
如果 Gmail 的标记不是很糟糕,会怎么样?如果它很漂亮,会怎么样?
<hangout-module>
<hangout-chat from="Paul, Addy">
<hangout-discussion>
<hangout-message from="Paul" profile="profile.png"
profile="118075919496626375791" datetime="2013-07-17T12:02">
<p>Feelin' this Web Components thing.
<p>Heard of it?
</hangout-message>
</hangout-discussion>
</hangout-chat>
<hangout-chat>...</hangout-chat>
</hangout-module>
真是太棒了!这款应用也很有意义。它有意义、易于理解,最重要的是,它具有可维护性。未来的我/您只需查看其声明式骨干,即可确切了解其用途。
使用入门
自定义元素 可让 Web 开发者定义新类型的 HTML 元素。该规范是 Web 组件类别下的若干新 API 基元之一,但它可能非常重要。如果没有自定义元素解锁的功能,则 Web 组件不存在:
- 定义新的 HTML/DOM 元素
- 创建从其他元素延伸的元素
- 以逻辑方式将自定义功能捆绑到单个代码中
- 扩展现有 DOM 元素的 API
注册新元素
自定义元素是使用 document.registerElement()
创建的:
var XFoo = document.registerElement('x-foo');
document.body.appendChild(new XFoo());
document.registerElement()
的第一个参数是元素的标记名称。名称必须包含短划线 (-)。例如,<x-tags>
、<my-element>
和 <my-awesome-app>
都是有效名称,而 <tabs>
和 <foo_bar>
则不是。此限制使解析器能够区分自定义元素与常规元素,但也可确保在将新标记添加到 HTML 时向前兼容性。
第二个参数是(可选)对象,用于描述元素的 prototype
。您可以在此处为元素添加自定义功能(例如公共属性和方法)。稍后会详细介绍。
默认情况下,自定义元素继承自 HTMLElement
。因此,上例等同于:
var XFoo = document.registerElement('x-foo', {
prototype: Object.create(HTMLElement.prototype)
});
调用 document.registerElement('x-foo')
会告知浏览器有新元素,并返回可用于创建 <x-foo>
实例的构造函数。或者,如果您不想使用构造函数,也可以使用其他元素实例化方法。
扩展元素
借助自定义元素,您可以扩展现有(原生)HTML 元素以及其他自定义元素。如需扩展元素,您需要向 registerElement()
传递要从其继承的元素的名称和 prototype
。
扩展原生元素
假设您对普通用户 <button>
不满意。您希望增强其功能,使其成为“超级按钮”。如需扩展 <button>
元素,请创建一个继承 HTMLButtonElement
的 prototype
和 extends
作为元素名称的新元素。在本例中,“button”:
var MegaButton = document.registerElement('mega-button', {
prototype: Object.create(HTMLButtonElement.prototype),
extends: 'button'
});
从原生元素继承的自定义元素称为类型扩展自定义元素。它们从专用版本的 HTMLElement
继承,以表示“元素 X 是 Y”。
示例:
<button is="mega-button">
扩展自定义元素
如需创建扩展 <x-foo>
自定义元素的 <x-foo-extended>
元素,只需继承其原型并说明您要从哪个标记继承即可:
var XFooProto = Object.create(HTMLElement.prototype);
...
var XFooExtended = document.registerElement('x-foo-extended', {
prototype: XFooProto,
extends: 'x-foo'
});
如需详细了解如何创建元素原型,请参阅下文的添加 JS 属性和方法。
如何升级元素
您有没有想过,为什么 HTML 解析器在处理非标准代码时不会匹配?
例如,如果我们在网页上声明 <randomtag>
,浏览器将非常乐意接受它。根据 HTML 规范:
抱歉,<randomtag>
!您不是标准用户,并且继承自“HTMLUnknownElement
”。
自定义元素则并非如此。具有有效自定义元素名称的元素会继承 HTMLElement
。您可以启动控制台 Ctrl + Shift + J
(在 Mac 上为 Cmd + Opt + J
),然后粘贴以下代码行进行验证;它们会返回 true
:
// "tabs" is not a valid custom element name
document.createElement('tabs').__proto__ === HTMLUnknownElement.prototype
// "x-tabs" is a valid custom element name
document.createElement('x-tabs').__proto__ == HTMLElement.prototype
无法解析的元素
由于自定义元素是使用 document.registerElement()
通过脚本注册的,因此可以在浏览器注册其定义之前声明或创建它们。例如,虽然您可以在页面上声明 <x-tabs>
,但最终调用了 document.registerElement('x-tabs')
,但很晚。
在元素升级为其定义之前,它们被称为未解析的元素。这些 HTML 元素具有有效的自定义元素名称,但尚未注册。
下表可能有助于您了解相关信息:
名称 | 继承自 | 示例 |
---|---|---|
未解析的元素 | HTMLElement |
<x-tabs> ,<my-element> |
未知元素 | HTMLUnknownElement |
<tabs> ,<foo_bar> |
实例化元素
创建元素的常见技术仍然适用于自定义元素。与所有标准元素一样,您可以在 HTML 中声明它们,也可以使用 JavaScript 在 DOM 中创建这些元素。
实例化自定义代码
声明它们:
<x-foo></x-foo>
在 JS 中创建 DOM:
var xFoo = document.createElement('x-foo');
xFoo.addEventListener('click', function(e) {
alert('Thanks!');
});
使用 new
运算符:
var xFoo = new XFoo();
document.body.appendChild(xFoo);
实例化类型扩展元素
实例化类型扩展样式的自定义元素与自定义标记非常相似。
声明它们:
<!-- <button> "is a" mega button -->
<button is="mega-button">
在 JS 中创建 DOM:
var megaButton = document.createElement('button', 'mega-button');
// megaButton instanceof MegaButton === true
如您所见,现在有一个 document.createElement()
的过载版本,它使用 is=""
属性作为其第二个参数。
使用 new
运算符:
var megaButton = new MegaButton();
document.body.appendChild(megaButton);
到目前为止,我们已经了解了如何使用 document.registerElement()
告知浏览器新标记的存在,但这并没有什么用处。我们来添加属性和方法。
添加 JS 属性和方法
自定义元素的好处在于,您可以通过在元素定义中定义属性和方法,将量身定制的功能与该元素捆绑在一起。不妨将其视为为元素创建公共 API 的一种方式。
下面是一个完整示例:
var XFooProto = Object.create(HTMLElement.prototype);
// 1. Give x-foo a foo() method.
XFooProto.foo = function() {
alert('foo() called');
};
// 2. Define a property read-only "bar".
Object.defineProperty(XFooProto, "bar", {value: 5});
// 3. Register x-foo's definition.
var XFoo = document.registerElement('x-foo', {prototype: XFooProto});
// 4. Instantiate an x-foo.
var xfoo = document.createElement('x-foo');
// 5. Add it to the page.
document.body.appendChild(xfoo);
当然,构建 prototype
的方法有无数种。如果您不喜欢创建这样的原型,可以使用下面这种更精简的版本:
var XFoo = document.registerElement('x-foo', {
prototype: Object.create(HTMLElement.prototype, {
bar: {
get: function () {
return 5;
}
},
foo: {
value: function () {
alert('foo() called');
}
}
})
});
第一种格式允许使用 ES5 Object.defineProperty
。第二个则允许使用 get/set。
生命周期回调方法
元素可以定义特殊方法,以便在其存续的特定时间内运行代码。 这些方法被恰当地命名为生命周期回调。它们都有特定的名称和用途:
回调名称 | 调用时间 |
---|---|
createdCallback | 创建元素的实例 |
attachedCallback | 文档中插入了一个实例 |
detachedCallback | 从文档中移除了实例 |
attributeChangedCallback(attrName, oldVal, newVal) | 添加、移除或更新了属性 |
示例:在 <x-foo>
上定义 createdCallback()
和 attachedCallback()
:
var proto = Object.create(HTMLElement.prototype);
proto.createdCallback = function() {...};
proto.attachedCallback = function() {...};
var XFoo = document.registerElement('x-foo', {prototype: proto});
所有生命周期回调都是可选的,但在必要时请定义它们。例如,假设您的元素足够复杂,并在 createdCallback()
中打开 IndexedDB 的连接。在它从 DOM 中移除之前,请在 detachedCallback()
中执行必要的清理工作。注意:您不应该依赖于此属性(例如,当用户关闭标签页时),而应将其视为可能的优化钩子。
生命周期回调的另一个用例是用于在元素上设置默认事件监听器:
proto.createdCallback = function() {
this.addEventListener('click', function(e) {
alert('Thanks!');
});
};
添加标记
我们已创建 <x-foo>
并为其提供 JavaScript API,但它是空的!我们要给它一些 HTML 来呈现吗?
生命周期回调在这里会派上用场。具体而言,我们可以使用 createdCallback()
为元素赋予一些默认 HTML:
var XFooProto = Object.create(HTMLElement.prototype);
XFooProto.createdCallback = function() {
this.innerHTML = "**I'm an x-foo-with-markup!**";
};
var XFoo = document.registerElement('x-foo-with-markup', {prototype: XFooProto});
将此标记实例化并在开发者工具中进行检查(右键点击,选择“Inspect Element”)后,系统应显示以下内容:
▾<x-foo-with-markup>
**I'm an x-foo-with-markup!**
</x-foo-with-markup>
将内部结构封装到 Shadow DOM 中
Shadow DOM 本身就是一款强大的封装内容工具。将其与自定义元素搭配使用,即可产生神奇的效果!
Shadow DOM 为自定义元素提供了以下功能:
- 可以隐藏自己的胆量,从而保护用户免受血腥的实现细节的侵扰。
- 样式封装(完全免费)。
从 Shadow DOM 创建元素与创建可渲染基本标记的元素类似。差异在于 createdCallback()
:
var XFooProto = Object.create(HTMLElement.prototype);
XFooProto.createdCallback = function() {
// 1. Attach a shadow root on the element.
var shadow = this.createShadowRoot();
// 2. Fill it with markup goodness.
shadow.innerHTML = "**I'm in the element's Shadow DOM!**";
};
var XFoo = document.registerElement('x-foo-shadowdom', {prototype: XFooProto});
我没有设置元素的 .innerHTML
,而是为 <x-foo-shadowdom>
创建了一个阴影根,然后用标记填充了该根。在开发者工具中启用“Show Shadow DOM”设置后,您会看到可展开的 #shadow-root
:
▾<x-foo-shadowdom>
▾#shadow-root
**I'm in the element's Shadow DOM!**
</x-foo-shadowdom>
那是影根!
通过模板创建元素
HTML 模板是另一种新的 API 基元,非常适合自定义元素领域。
示例:注册使用 <template>
和 Shadow DOM 创建的元素:
<template id="sdtemplate">
<style>
p { color: orange; }
</style>
<p>I'm in Shadow DOM. My markup was stamped from a <template>.
</template>
<script>
var proto = Object.create(HTMLElement.prototype, {
createdCallback: {
value: function() {
var t = document.querySelector('#sdtemplate');
var clone = document.importNode(t.content, true);
this.createShadowRoot().appendChild(clone);
}
}
});
document.registerElement('x-foo-from-template', {prototype: proto});
</script>
<template id="sdtemplate">
<style>:host p { color: orange; }</style>
<p>I'm in Shadow DOM. My markup was stamped from a <template>.
</template>
<div class="demoarea">
<x-foo-from-template></x-foo-from-template>
</div>
这几行代码实现了丰富的功能。我们来了解一下发生的所有情况:
- 我们在 HTML 中注册了一个新元素:
<x-foo-from-template>
- 元素的 DOM 是从
<template>
创建的 - 使用 Shadow DOM 隐藏元素的可怕细节
- Shadow DOM 提供了元素样式封装(例如,
p {color: orange;}
不会将整个页面变成橙色)
太好了!
设置自定义元素的样式
与任何 HTML 标记一样,您的自定义代码的用户可以使用选择器设置其样式:
<style>
app-panel {
display: flex;
}
[is="x-item"] {
transition: opacity 400ms ease-in-out;
opacity: 0.3;
flex: 1;
text-align: center;
border-radius: 50%;
}
[is="x-item"]:hover {
opacity: 1.0;
background: rgb(255, 0, 255);
color: white;
}
app-panel > [is="x-item"] {
padding: 5px;
list-style: none;
margin: 0 7px;
}
</style>
<app-panel>
<li is="x-item">Do</li>
<li is="x-item">Re</li>
<li is="x-item">Mi</li>
</app-panel>
设置使用 Shadow DOM 的元素样式
将 Shadow DOM 融入“有效”策略后,效果将要深得多。使用 Shadow DOM 的自定义元素会继承其诸多优势。
Shadow DOM 向元素注入样式。在影子根中定义的样式不会从宿主中泄漏,也不会从页面中渗入。对于自定义元素,元素本身就是宿主。样式封装的属性还允许自定义元素为自己定义默认样式。
阴影 DOM 样式设置是一个很大的主题!如果希望详细了解这方面的内容,建议您参阅我的其他几篇文章:
使用 :unresolved 防范 FOUC
为了减少 FOUC,自定义元素会指定一个新的 CSS 伪类 :unresolved
。用它来定位未解析的元素,直到浏览器调用 createdCallback()
为止(请参阅生命周期方法)。完成上述操作后,该元素就不再是未解析元素了。升级过程已完成,元素已转换为其定义。
示例:在“x-foo”标记注册后淡入:
<style>
x-foo {
opacity: 1;
transition: opacity 300ms;
}
x-foo:unresolved {
opacity: 0;
}
</style>
请注意,:unresolved
仅适用于未解析的元素,而不适用于从 HTMLUnknownElement
继承的元素(请参阅元素的升级方式)。
<style>
/* apply a dashed border to all unresolved elements */
:unresolved {
border: 1px dashed red;
display: inline-block;
}
/* x-panel's that are unresolved are red */
x-panel:unresolved {
color: red;
}
/* once the definition of x-panel is registered, it becomes green */
x-panel {
color: green;
display: block;
padding: 5px;
display: block;
}
</style>
<panel>
I'm black because :unresolved doesn't apply to "panel".
It's not a valid custom element name.
</panel>
<x-panel>I'm red because I match x-panel:unresolved.</x-panel>
历史记录和浏览器支持
功能检测
特征检测就是检查 document.registerElement()
是否存在:
function supportsCustomElements() {
return 'registerElement' in document;
}
if (supportsCustomElements()) {
// Good to go!
} else {
// Use other libraries to create components.
}
浏览器支持
document.registerElement()
最初是在 Chrome 27 和 Firefox ~23 中通过标志启用的。不过,自那时起,该规范已经发生了很大变化。Chrome 31 是第一个真正支持更新版规范的版本。
在为浏览器提供出色的支持之前,Google 的 Polymer 和 Mozilla 的 X-Tag 都使用了 polyfill。
HTMLElementElement 怎么了?
对于遵循标准化工作的人员,您知道曾经有 <element>
。那真是太棒了。您可以使用它以声明方式注册新元素:
<element name="my-element">
...
</element>
很遗憾,升级流程、极端情况和末日般的场景存在太多时间问题,无法全部解决。<element>
不得不搁置。2013 年 8 月,Dimitri Glazov 发布到 public-webapps,宣布 Google 将会移除该应用(至少目前为止)。
值得注意的是,Polymer 使用 <polymer-element>
实现了声明形式的元素注册。如何估测?它使用了 document.registerElement('polymer-element')
以及我在基于模板创建元素中介绍的技术。
总结
自定义元素为我们提供了工具来扩展 HTML 的词汇,教给它一些新技巧,以及避开网络平台的障碍。将它们与 Shadow DOM 和 <template>
等其他新平台基元结合使用,我们就可以开始实现 Web 组件的原理了。标记代码又可以变得时尚了!
如果您有兴趣开始使用 Web 组件,建议您查看 Polymer。这套功能足以满足您的需求。