本文翻译自 Custom Elements: defining new elements in HTML,在保证技术要点表达准确的前提下,行文风格有少许改编和瞎搞。css
原译文地址html
本文目录html5
注意!这篇文章介绍的 API 还没有彻底标准化,而且仍在变更中,在项目中使用这些实验性 API 时请务必谨慎。web
如今的 web 严重缺少表达能力。你只要瞄一眼“现代”的 web 应用,好比 GMail,就会明白个人意思。api
看看这一坨 DIV,这也叫现代?然而可悲的是,这就是咱们构建 web 应用的方式。难道 web 开发就不能追求更粗更硬更长……哦不对,是更高更快更强的奥林匹克精神?浏览器
HTML 为咱们提供了一个完美的文档组织工具,然而 HTML 规范定义的元素却颇有限。app
假如 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> <p>Heard of it?</p> </hangout-message> </hangout-discussion> </hangout-chat> <hangout-chat>...</hangout-chat> </hangout-module>
亮瞎狗眼颠覆三观!这清晰的结构,不识字也看得懂啊!最爽的是,它还有很强的可维护性,只要瞧一眼它的声明结构就能够清楚地知道它到底要干吗。dom
自定义元素容许开发者定义新的 HTML 元素类型。该规范只是 web 组件模块提供的众多新 API 中的一个,但它也极可能是最重要的一个。缺乏自定义元素带来的如下特性,web 组件根本玩不转:webapp
使用 document.register()
能够建立一个自定义元素
var XFoo = document.register('x-foo'); document.body.appendChild(new XFoo());
document.register()
的第一个参数是标签名,这个标签名必须包括一个连字符(-)。所以,诸如 <x-tags>
、<my-element>
、 <my-awesome-app>
都是合法的标签名,而 <tabs>
和 <foo_bar>
则不是。这个限定使解析器能很容易的区分自定义元素和 HTML 规范定义的元素,同时确保了 HTML 增长新标签时的向前兼容。
第二个参数是一个可选(译注:经测试,Chrome 29 中不能省略第二个参数)的对象,用于描述该元素的原型。在这里能够为元素添加自定义功能(公开属性和方法)。这个到 添加 JS 属性和方法 一节再细说。
自定义元素默认会继承 HTMLElement
的原型,所以上一个示例等同于:
var XFoo = document.register('x-foo', { prototype: Object.create(HTMLElement.prototype) });
调用 document.register('x-foo')
向浏览器注册了这个新元素,并返回一个能够用来建立 <x-foo>
元素实例的构造器。若是你不想使用构造器,也可使用其余实例化元素的技术。
提示:若是你不但愿在 window
全局对象中建立元素构造器,还能够把它放进命名空间:
var myapp = {}; myapp.XFoo = document.register('x-foo');
假设原生 <button>
元素不能知足你的需求,你想将其加强为一个“超级按钮”,能够经过建立一个继承 HTMLButtonElement
原型的新元素,来扩展 <button>
元素:
var MegaButton = document.register('mega-button', { prototype: Object.create(HTMLButtonElement.prototype) });
这类自定义元素被称为类型扩展自定义元素。它们以继承一个特定的 HTMLElement
的方式表达了“元素 X 是一个 Y”。
示例:
<button is="mega-button">
你有没有想过为何 HTML 解析器不会对不是规范定义的标签报错?好比咱们在页面中声明一个 <randomtag>
,一切都很和谐。根据 HTML 规范的表述,非规范定义的元素将使用 HTMLUnknownElement
接口。<randomtag>
不是规范定义的,它会继承自 HTMLUnknownElement
。
对自定义元素来讲,状况就不同了。拥有合法元素名的自定义元素将继承 HTMLElement
。你能够打开控制台(不知道快捷键的都滚粗……),运行下面这段代码,看看结果是否是 true
:
// “tabs”不是一个合法的自定义元素名 document.createElement('tabs').__proto__ === HTMLUnknownElement.prototype // “x-tabs”是一个合法的自定义元素名 document.createElement('x-tabs').__proto__ == HTMLElement.prototype
注意:在不支持 document.register()
的浏览器中,<x-tabs>
仍为 HTMLUnknownElement
。
因为自定义元素是由 JavaScript 代码 document.register()
注册的,所以它们可能在元素定义被注册到浏览器以前就已经声明或建立过了。好比你能够先在页面中声明 <x-tabs>
,再调用 document.register('x-tabs')
。
在被提高到其定义以前,这些元素被称为“unresolved 元素”。它们是拥有合法自定义元素名的 HTML 元素,只是尚未注册成为自定义元素。
下面这个表格看起来更直观一些:
类型 | 继承自 | 示例 |
---|---|---|
unresolved 元素 | HTMLElement |
<x-tabs> 、<my-element> 、<my-awesome-app> |
未知元素 | 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> “是一个”超级按钮 --> <button is="mega-button">
在 JS 中建立 DOM:
var megaButton = document.createElement('button', 'mega-button'); // megaButton instanceof MegaButton === true
看,这是一个接收第二个参数为 is
属性值的 document.createElement()
重载。
使用 new
操做符:
var megaButton = new MegaButton(); document.body.appendChild(megaButton);
如今,咱们已经学习了如何使用 document.register()
来向浏览器注册一个新标签。但这还不够,接下来咱们要向新标签添加属性和方法。
自定义元素最强大的地方在于,你能够在元素定义中加入属性和方法,给元素绑定特定的功能。你能够把它想象成一种给你的元素建立公开 API 的方法。
下面是一个完整的示例:
var XFooProto = Object.create(HTMLElement.prototype); // 1. 为 x-foo 建立 foo() 方法 XFooProto.foo = function() { alert('foo() called'); }; // 2. 定义一个只读属性 "bar". Object.defineProperty(XFooProto, "bar", {value: 5}); // 3. 注册 x-foo var XFoo = document.register('x-foo', {prototype: XFooProto}); // 4. 建立一个 x-foo 实例. var xfoo = document.createElement('x-foo'); // 5. 插入页面 document.body.appendChild(xfoo);
构造原型的方法多种多样,若是你不喜欢上面这种方式,还有一个更简洁的例子:
var XFoo = document.register('x-foo', { prototype: Object.create(HTMLElement.prototype, { bar: { get: function() { return 5; } }, foo: { value: function() { alert('foo() called'); } } }) });
以上两种方式,第一种使用了 ES5 的 Object.defineProperty,第二种则使用了 get/set。
元素能够定义特殊的方法,来注入其生存期内关键的时间点。这些方法各自有特定的名称和用途,它们被恰如其分地命名为生命周期回调:
回调方法名称 | 调用时间点 |
---|---|
createdCallback | 建立元素实例 |
enteredDocumentCallback | 向文档插入实例 |
leftDocumentCallback | 从文档中移除实例 |
attributeChangedCallback(attrName, oldVal, newVal) | 添加,移除,或修改一个属性 |
示例:为 <x-foo>
定义 createdCallback()
和 enteredDocumentCallback()
var proto = Object.create(HTMLElement.prototype); proto.createdCallback = function() {...}; proto.enteredDocumentCallback = function() {...}; var XFoo = document.register('x-foo', {prototype: proto});
全部生命周期回调都是可选的,你能够只在须要关注的时间点定义它们。举个例子,你有一个很复杂的元素,它会在 createdCallback()
打开一个 indexedDB 链接。在将其从 DOM 移除时,leftDocumentCallback()
会作一些必要的清理工做。注意:不要过于依赖这些生命周期方法(若是用户直接关闭浏览器标签,生命周期方法是没有机会执行的),仅将其做为可能的优化点。
另外一个生命周期回调的例子是为元素设置默认的事件监听器:
proto.createdCallback = function() { this.addEventListener('click', function(e) { alert('Thanks!'); }); };
咱们已经建立好 <x-foo>
并添加了 JavaScript API,但它尚未任何内容。要不咱们给它整点?
生命周期回调在这个时候就派上用场了。咱们甚至能够用 createdCallback()
给一个元素赋予一些默认的 HTML:
var XFooProto = Object.create(HTMLElement.prototype); XFooProto.createdCallback = function() { this.innerHTML = "<b>I'm an x-foo-with-markup!</b>"; }; var XFoo = document.register('x-foo-with-markup', {prototype: XFooProto});
实例化这个标签并在 DevTools 中观察,能够看到以下结构:
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 = "<b>I'm in the element's Shadow DOM!</b>"; };
var XFoo = document.register('x-foo-shadowdom', {prototype: XFooProto});
咱们并无直接设置 <x-foo-shadowdom>
的 innerHTML
,而是为其建立了一个用于填充标记的 Shadow Root。在 DevTools 中选中“显示 Shadow DOM”,你就会看到一个能够展开的 #document-fragment:
这就是 Shadow Root!
HTML Template 是另外一组跟自定义元素完美融合的新 API。
模板元素可用于声明 DOM 片断。它们能够被解析并在页面加载后插入,以及延迟到运行时才进行实例化。模板是声明自定义元素结构的理想方案。
示例:注册一个由模板和 Shadow DOM 建立的元素:
<template id="sdtemplate"> <style> p { color: orange; } </style> <p>I'm in Shadow DOM. My markup was stamped from a &lt;template&gt;.</p> </template> <script> var proto = Object.create(HTMLElement.prototype, { createdCallback: { value: function() { var t = document.querySelector('#sdtemplate'); this.createShadowRoot().appendChild(t.content.cloneNode(true)); } } }); document.register('x-foo-from-template', {prototype: proto}); </script>
短短几行作了不少事情,咱们挨个来看都发生了些什么:
<x-foo-from-template>
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 Root 中定义的样式不会暴露到宿主外部或对页面产生影响。对自定义元素来讲,元素自己是宿主。样式封装的属性也使得自定义元素可以为本身定义默认样式。
Shadow DOM 的样式是一个很大的话题!若是你想更多地了解它,推荐你阅读我写的其余文章:
为了缓解无样式内容闪烁的影响,自定义元素规范提出了一个新的 CSS 伪类 :unresolved
。在浏览器调用你的createdCallback()
(请看生命周期回调方法一节)以前,这个伪类均可以匹配到 unresolved 元素。一旦产生调用,就意味着元素已经完成提高,成为它被定义的形态,该元素就再也不是一个 unresolved 元素。
Chrome 29 已经原生支持 :unresolved
伪类。
示例:注册后渐显的 <x-foo>
标签:
x-foo { opacity: 1; transition: opacity 300ms; } x-foo:unresolved { opacity: 0; }
请记住 :unresolved
伪类只能用于 unresolved 元素,而不能用于继承自 HTMLUnkownElement
的元素(请看元素如何提高一节)。
<style> /* 给全部未提高元素添加边框 */ :unresolved { border: 1px dashed red; display: inline-block; } /* 未提高的 x-panel 文本内容为红色 */ x-panel:unresolved { color: red; } /* 完成注册的 x-panel 文本内容为绿色 */ 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>
了解更多 :unresolved
的知识,请看 Polymer 文档《元素样式指南》。
特性检测就是检查浏览器是否提供了 document.register()
接口:
function supportsCustomElements() { return 'register' in document; } if (supportsCustomElements()) { // 使用自定义元素 API } else { // 使用其余类库建立组件 }
Chrome 27 和 Firefox 23 都提供了对 document.register()
的支持,不过以后规范又有一些演化。Chrome 31 将是第一个支持新规范的版本。提示:在 Chrome 31 中使用自定义元素,须要开启 about:flags 中的“实验性 web 平台特性(Experimental Web Platform features)”选项。
在浏览器支持稳定以前,也有一些很好的过渡方案:
紧跟过标准的人都知道曾经有一个 <element>
标签。它很是好用,你只要像下面这样就能够声明式的注册一个新元素:
<element name="my-element"> ... </element>
不幸的是,在它的提高过程、边界案例,以及末日般的复杂场景中,须要处理大量的时序问题。<element>
所以被迫搁置。2013 年 8 月,Dimitri Glazkov 在 public-webapps 邮件组中宣告废弃 <element>
,至少目前看来是废掉了。
值得注意的是,Polymer 实现了用
document.register('polymer-element')
以及
从模板建立元素一节介绍的技术。
自定义元素为咱们提供了一个工具,经过它咱们能够扩展 HTML 的词汇,赋予它新的特性,并把不一样的 web 平台链接在一块儿。结合其余新的基本平台,如 Shadow DOM 和模板,咱们领略了 web 组件的宏伟蓝图。标记语言将再次变得很时髦!
若是你对使用 web 组件感兴趣,建议你去看看 Polymer 框架,从它开始玩吧。