注意:这篇文章介绍的 API 尚未完全标准化,并且仍在变动中,在项目中使用这些实验性 API 时请务必谨慎。
现在的 web 严重缺乏表达能力。你只要瞧一眼“现代”的 web 应用,比如 GMail,就会明白我的意思:
堆砌 <div>
一点都不现代。然而可悲的是,这就是我们构建 web 应用的方式。在现有基础上我们不应该有更高的追求吗?
您还可以参考以下HTML5/CSS3相关文章:
《使用CSS3开启GPU硬件加速提升网站动画渲染性能》
《HTML5 WebSockets 基础使用教程》
《关于HTML 5 canvas 的基础教程》
《让所有IE支持HTML5的解决方案》
HTML 为我们提供了一个完美的文档组织工具,然而 HTML 规范定义的元素却很有限。
假如 GMail 的标记不那么糟糕,而是像下面这样漂亮,那会怎样?
查看演示(需翻墙)
真是令人耳目一新!这个应用太合理了,既有意义,又容易理解。最妙的是,它是可维护的,只要查看声明结构就可以清楚地知道它的作用。
自定义元素,救救我们!就指望你了!
自定义元素允许开发者定义新的 HTML 元素类型。该规范只是 Web 组件模块提供的众多新 API 中的一个,但它也很可能是最重要的一个。没有自定义元素带来的以下特性,Web 组件都不会存在:
注册新元素
使用 document.registerElement()
可以创建一个自定义元素:
document.registerElement()
的第一个参数是元素的标签名。这个标签名必须包括一个连字符(-)。因此,诸如 <x-tags>
、<my-element>
和 <my-awesome-app>
都是合法的标签名,而 <tabs>
和 <foo_bar>
则不是。这个限定使解析器能很容易地区分自定义元素和 HTML 规范定义的元素,同时确保了 HTML 增加新标签时的向前兼容。
第二个参数是一个(可选的)对象,用于描述该元素的 prototype。在这里可以为元素添加自定义功能(例如:公开属性和方法)。稍后详述。
自定义元素默认继承自 HTMLElement,因此上一个示例等同于:
调用 document.registerElement('x-foo')
向浏览器注册了这个新元素,并返回一个可以用来创建 <x-foo>
元素实例的构造器。如果你不想使用构造器,也可以使用其他实例化元素的技术。
提示:如果你不希望在 window 全局对象中创建元素构造器,还可以把它放进命名空间:
假设平淡无奇的原生 <button>
元素不能满足你的需求,你想将其增强为一个“超级按钮”,可以通过创建一个继承 HTMLButtonElement.prototype
的新元素,来扩展 <button>
元素:
提示:要创建扩展自元素 B 的元素 A,元素 A 必须继承元素 B 的 prototype。
这类自定义元素被称为类型扩展自定义元素。它们以继承某个特定 HTMLElement 的方式表达了“元素 X 是一个 Y”。
示例:
你有没有想过为什么 HTML 解析器对非标准标签不报错?比如,我们在页面中声明一个 <randomtag>
,一切都很和谐。根据 HTML 规范的表述:
非规范定义的元素必须使用 HTMLUnknownElement
接口。
- HTML 规范
<randomtag>
是非标准的,它会继承 HTMLUnknownElement
。
对自定义元素来说,情况就不一样了。拥有合法元素名的自定义元素将继承 HTMLElement。你可以按 Ctrl+Shift+J(Mac 系统为 Cmd+Opt+J)打开控制台,运行下面这段代码,得到的结果将是 true:
注意:在不支持 document.registerElement()
的浏览器中,<x-tabs>
仍为 HTMLUnknownElement
。
UNRESOLVED 元素
由于自定义元素是通过脚本执行 document.registerElement()
注册的,因此 它们可能在元素定义被注册到浏览器之前就已经声明或创建过了。例如:你可以先在页面中声明 <x-tabs>
,以后再调用 document.registerElement('x-tabs')
。
在被提升到其定义之前,这些元素被称为 unresolved 元素。它们是拥有合法自定义元素名的 HTML 元素,只是还没有注册成为自定义元素。
下面这个表格看起来更直观一些:
类型 | 继承自 | 示例 |
---|---|---|
unresolved 元素 | HTMLElement |
<x-tabs> 、<my-element> 、<my-awesome-app> |
未知元素 | HTMLUnknownElement |
<tabs> 、<foo_bar>
|
把 unresolved 元素想象成尚处于中间状态,它们都是等待被浏览器提升的潜在候选者。浏览器说:“你具备一个新元素的全部特征,我保证会在赋予你定义的时候将你提升为一个元素”。
我们创建普通元素用到的一些技术也可以用于自定义元素。和所有标准定义的元素一样,自定义元素既可以在 HTML 中声明,也可以通过 JavaScript 在 DOM 中创建。
实例化自定义标签
声明元素:
在 JS 中创建 DOM:
使用 new 操作符:
实例化类型扩展元素
实例化类型扩展自定义元素的方法和自定义标签惊人地相似。
声明元素:
在 JS 中创建 DOM:
看,这是接收第二个参数为 is=""
属性的 document.createElement()
重载版本。
使用 new 操作符:
现在,我们已经学习了如何使用 document.registerElement()
来向浏览器注册一个新标签。但这还不够,接下来我们要向新标签添加属性和方法。
添加 JS 属性和方法
自定义元素最强大的地方在于,你可以在元素定义中加入属性和方法,给元素绑定特定的功能。你可以把它想象成一种给你的元素创建公开 API 的方法。
这里有一个完整的示例:
构造 prototype 的方法多种多样,如果你不喜欢上面这种方式,再看一个更简洁的例子:
以上两种方式,第一种使用了 ES5 的 Object.defineProperty
,第二种则使用了 get/set。
元素可以定义特殊的方法,来注入其生存期内关键的时间点。这些方法各自有特定的名称和用途,它们被恰如其分地命名为生命周期回调:
回调名称 | 调用时间点 |
---|---|
createdCallback | 创建元素实例 |
attachedCallback | 向文档插入实例 |
detachedCallback | 从文档中移除实例 |
attributeChangedCallback(attrName, oldVal, newVal) | 添加,移除,或修改一个属性 |
所有生命周期回调都是可选的,你可以只在需要关注的时间点定义它们。例如:假设你有一个很复杂的元素,它会在 createdCallback()
打开一个 IndexedDB
连接。在将其从 DOM 移除时,detachedCallback()
会做一些必要的清理工作。注意:不要过于依赖这些生命周期方法(比如用户直接关闭浏览器标签),仅将其作为可能的优化点。
另一个生命周期回调的例子是为元素设置默认的事件监听器:
如果你的元素太笨重,是不会有人用它的。生命周期回调可以帮你大忙!
添加标记
我们已经创建好 <x-foo>
并添加了 JavaScript API,但它还没有任何内容。不如我们给点 HTML 让它渲染?
生命周期回调在这个时候就派上用场了。我们甚至可以用 createdCallback()
给一个元素赋予一些默认的 HTML:
实例化这个标签并在 DevTools 中观察(右击,选择“审查元素”),可以看到如下结构:
Shadow DOM 本身是一个封装内容的强大工具,配合使用自定义元素就更神奇了!
Shadow DOM 为自定义元素提供了:
1.一种隐藏内部实现的方法,从而将用户与血淋淋的实现细节隔离开。
2.简单有效的样式隔离。
从 Shadow DOM 创建元素,跟创建一个渲染基础标记的元素非常类似,区别在于 createdCallback()
回调:
我们并没有直接设置 <x-foo-shadowdom>
的 innerHTML,而是为其创建了一个用于填充标记的 Shadow Root。在 DevTools 设置中勾选“Show Shadow DOM”,你就会看到一个可以展开的 #shadow-root:
这就是 Shadow Root!
HTML 模板是另一组跟自定义元素完美融合的新 API。
<template>
元素可用于声明 DOM 片段。它们可以被解析并在页面加载后插入,以及延迟到运行时才进行实例化。模板是声明自定义元素结构的理想方案。
示例:注册一个由 <template>
和 Shadow DOM 创建的元素:
短短几行做了很多事情,我们挨个来看都发生了些什么:
<x-foo-from-template>
<template>
创建的p {color: orange;}
不会把整个页面都搞成橙色)真不错!
和其他 HTML 标签一样,自定义元素也可以用选择器定义样式:
有了 Shadow DOM 场面就热闹得多了,它可以极大增强自定义元素的能力。
Shadow DOM 为元素增加了样式封装的特性。Shadow Root 中定义的样式不会暴露到宿主外部或对页面产生影响。对自定义元素来说,元素本身就是宿主。样式封装的属性也使得自定义元素能够为自己定义默认样式。
Shadow DOM 的样式是一个很大的话题!如果你想更多地了解它,推荐你阅读我写的其他文章:
使用 :unresolved 伪类避免无样式内容闪烁(FOUC)
为了缓解无样式内容闪烁的影响,自定义元素规范提出了一个新的 CSS 伪类 :unresolved
。在浏览器调用你的 createdCallback()
(见生命周期回调方法一节)之前,这个伪类都可以匹配到 unresolved
元素。一旦产生调用,就意味着元素已经完成提升,成为它被定义的形态,该元素就不再是一个 unresolved
元素了。
提示:Chrome 29 已经原生支持 CSS :unresolved
伪类。
示例:注册后渐显的
标签:
请记住 :unresolved
伪类只能用于 unresolved 元素,而不能用于继承自 HTMLUnkownElement
的元素(见元素如何提升一节)。
了解更多 :unresolved
伪类的知识,请看 Polymer 文档《元素样式指南》。
Chrome 27 和 Firefox 23 都提供了对 document.registerElement()
的支持,不过之后规范又有一些演化。Chrome 31 将是第一个真正支持新规范的版本。
提示:在 Chrome 31 中使用自定义元素,需要开启 about:flags 中的“实验性 web 平台特性(Experimental Web Platform features)”选项。
在浏览器支持稳定之前,也有一些很好的兼容方案:
一直关注标准的人都知道曾经有一个
标签。它非常好用,你只要像下面这样就可以声明式地注册一个新元素:
然而很不幸,在它的提升过程、边界案例,以及末日般的复杂场景中,需要处理大量的时序问题。
因此被迫搁置。2013 年 8 月,Dimitri Glazkov 在 public-webapps 邮件组中宣告移除
。
值得注意的是,Polymer 实现了以
的形式声明式地注册元素。这是怎么做到的?它用的正是 document.registerElement('polymer-element')
以及我在从模板创建元素一节介绍的技术。
自定义元素为我们提供了一个工具,通过它我们可以扩展 HTML 的词汇,赋予它新的特性,并把不同的 web 平台连接在一起。结合其他新的基本平台,如 Shadow DOM 和 ,我们领略了 web 组件的宏伟蓝图。标记语言将再次变得很时髦!
如果你对使用 web 组件感兴趣,建议你看看 Polymer,它已经有了你想要的一切
订阅彬Go以查看HTML5最新资源、教程。
感谢来自Leo的翻译
英文原文:《Custom Elements: defining new elements in HTML》(需翻墙)
译文原文:http://forcefront.com/2013/custom-elements-defining-new-elements-in-html