Warning:
This wiki has been archived and is now read-only.
Web-Components
W3C工作草案2013年6月6日
这个版本
http://www.w3.org/TR/2013/WD-components-intro-20130606/
最新版本
http://www.w3.org/TR/components-intro/
最新编辑的草稿
https://dvcs.w3.org/hg/webcomponents/raw-file/tip/explainer/index.html
上一个版本
http://www.w3.org/TR/2012/WD-components-intro-20120522/
修订历史
https://dvcs.w3.org/hg/webcomponents/log/tip/explainer/index.html
参与
Discuss on public-webapps@w3.org (Web Applications Working Group)
File bugs (w3.org's Bugzilla)
编辑
Dominic Cooney, Google, <dominicc@google.com>
Dimitri Glazkov, Google, <dglazkov@chromium.org>
Copyright © 2013 W3C® (MIT, ERCIM, Keio, Beihang), All Rights Reserved. W3C liability, trademark and document use rules apply.
摘要
这份文档简述了Web组件的概况,是一个非标准的参考规范。它用易于理解的文字和插图总结了Web组件主要功能的规范信息。
本文档的状况
本节描述本文档发布时的状况。其它的文档也许会取代本文档。W3C推荐标准和该文档的最新版本可以在W3C技术文档索引这个列表中找到(http://www.w3.org/TR/)。
本文档是Web应用工作组发布的第一个公开的工作草案。 如果您想对本文档进行评论,请将其发送到public-webapps@w3.org(subscribe, archives ),欢迎反馈。
作为一个已发布的工作草案并不意味着其已经得到了W3C成员的认可,它可能随时被更新、更换或者废弃,所以在其他地方引用这个文档是不合适的。
该文档是由一个小组在2004年2月5日的W3C专利政策下产生的。 该文档仅供参考。 W3C维护着一个由小组成员交付的公开专利的公共列表,该页面还包括公开一项专利的相关说明。小组成员中认为自己拥有专利基本要求中包含的实际知识的个体必须按照W3C专利政策第6条中的规定公开相关信息。
Contents
关于本文档
正如前面提到的,该文档的相关规范工作还在进行中。它把所有Web组件相关的概念融合成一个连贯的,非标准的参考规范。
每个概念都会被提炼并且制订成规范文档。
这是一个迭代的过程。
这里所描述的有时可能会落后于规范文档,但有时又可能会为这些文档指明前进的道路。 一旦所有的前瞻性信息被提取到相应的规范文档,那么该文档中的这些描述将最终更新至规范文档中的信息来准确概括。
简介
Web组件模型(“Web Components”)包含五部份:
- Templates,定义的标记块,不会被渲染但可以随后被激活使用。
- Decorators,基于CSS选择器来应用模板,从而对文档进行丰富的视觉和行为的变化。
- Custom Elements,让用户自定义新的标签名和新的脚本接口。
- Shadow DOM,封装的DOM子树,更可靠的用户界面元素组成。
- Imports,定义Templates、Decorators和Custom Elements如何作为资源打包加载。
这五部份每一份都可以单独使用。当组合使用时,Web应用作者可以使用Web组件在一个较高的层面定义widgets,而这些丰富的视觉和交互在目前不可能单独使用CSS做到,还有脚本库的易用性和重用性在目前也不可能做到。
本文档将会逐节说明作者如何组合使用这些Web组件。
Templates
Web组件的HTML Templates规范是这部份的规范性描述。
打算以后使用<template>
元素包含HTML标签,解析器虽然会解析在<template>
元素中的内容,但它是惰性的:脚本不会被执行,图片不会被下载等。 <template>元素不会被渲染。
<template>
元素具有content
属性,其将<template>
里的内容存放在文档片段中, 当要使用时,可以用content
移动或复制这些节点:
<template id="commentTemplate"> <div> <img src=""> <div class="comment-text"></div> </div> </template> <script> function addComment(imageUrl, text) { var t = document.querySelector("#commentTemplate"); var comment = t.content.cloneNode(true); // Populate content. comment.querySelector('img').src = imageUrl; comment.querySelector('.comment-text').textContent = text; document.body.appendChild(comment); } </script>
因为<template>
中包含的内容不是在文档中,是在content
文档片段中,所以通过这种方法可以“激活”<template>
中包含的内容,这样其中的脚本可以执行了,图片也可以获取到了。 例如,可以将大量脚本存储在<template>
中,但不解析它们,直到需要时才去解析。
Decorators
与Web组件的其他部份不同,Decorators目前还没有规范。 这一部分只是勾勒出Decorators的一个概况。 Web组件Decorators这部分还没有规范性的描述。
decorators
用来增强或覆盖已有的元素的呈现效果。应用中的decorators由CSS控制,不过,可以用来应用展示decorators
的外部标签是唯一的。
<decorator>
元素包含<template>
元素,用来渲染<decorator>
。
<decorator id="details-open"> <template> <a id="summary"> ▾ <content select="summary"></content> </a> <content></content> </template> </decorator>
<content>
元素用于<decorator>
的内容在渲染的时候。
<decorator>
使用CSS属性decorator
:
details[open] { decorator: url(#details-open); }
还需要附加以下代码:
<details open> <summary>Timepieces</summary> <ul> <li>Sundial <li>Cuckoo clock <li>Wristwatch </ul> </details>
将会被渲染成下面这样:
- Sundial
- Cuckoo clock
- Wristwatch
尽管CSS属性decorator
可以指向Web上的任何资源,但在文档定义加载之前decorator
不会被应用。 decorators
仅用于纯粹的展示,不能运行脚本(包括内联事件处理),也不能被被编辑。
Decorators 中的事件处理
Decorators可以添加事件处理器来实现交互。
因为decorator
是瞬时的,要么应用要么未应用,所以对template
中的节点添加事件监听或者依赖于状态对decorator
没什么用。 decorator
的事件是事件控制器的媒介。
图例. Event handler registration
要用事件控制器注册一个事件监听器,需要<template>
包含一个<script>
元素。 一旦decorator
元素被插入到文档中或者作为外部文档的一部分被加载,则脚本会被执行一次。该脚本必须注册到一个数组中:
<decorator id="details-open"> <script> function clicked(event) { event.target.removeAttribute('open'); } [{selector: '#summary', type: 'click', handler: clicked}]; </script> <template> <a id="summary"> <!-- as illustrated above -->
事件控制器会处理这个数组,并且分发到decorator
被应用到的任何事件处理器中。
图例:Event routing and retargeting
事件监听器被调用时,该事件的目标是decorator的内容,而不是template中的内容。 在上面的例子中,点击▾(模板中定义的)调用的是clicked
函数(该事件注册在#summary元素上,也是模板中定义的),但event.target
属性是指<details>
元素。 这个重定向是必要的,它不会影响该文档的DOM结构。
去除open
属性后这个decorator将不再被应用,因为具有decorator
属性的选择器匹配不上了。已被渲染的元素会恢复到应用decorator之前的状态。 不过可以再写一个decorator应用到“closed”时的渲染,这种方式通过简单的触发不同的decorator实现状态交互:
<style> details { decorator: url(#details-closed); } details[open] { decorator: url(#details-open); } </style> <decorator id="details-closed"> <script> function clicked(event) { event.target.setAttribute('open', 'open'); } [{selector: '#summary', type: 'click', handler: clicked}]; </script> <template> <a id="summary"> ▸ <content select="summary"></content> </a> </template> </decorator> <decorator id="details-open"> <!-- as illustrated above -->
这里用到了两个decorator,一个呈现关闭视图,另一个呈现打开视图。 每个decorator通过事件处理器来响应点击,从而切换元素的打开状态。 内容元素的选择属性将在后面详细说明。
Custom Elements
Web组件关于这部分的规范性描述在这里:Custom Elements规范。
Custom elements是一种新型的DOM元素,可以让开发者自定义。与Decorator的无状态性和瞬时性不同,Custom elements可以封装状态,并提供脚本接口。下表总结了Decorator和Custom elements之间的主要区别。
Decorators | Custom Elements | |
---|---|---|
有效期 | 瞬时的,CSS选择匹配时 | 稳定的,等同于元素的有效期 |
应用和未应用动态转换 | 可以,基于CSS选择器 | 不可以,在元素创建时就已经固定了 |
通过脚本访问 | 不可以,不能通过DOM访问;不能添加接口 | 可以,通过DOM访问;可以提供接口 |
状态 | 无状态 | 有状态的DOM对象 |
行为 | 通过改变decorator进行模拟 | 首选使用脚本和事件 |
定义一个Custom Element
<element>
元素定义了一个Custom Element,可以使用extends
属性指明元素类型:
<element extends="button" name="fancy-button"> … </element>
extends
属性指定该元素扩展自哪一个HTML标签。
name
属性定义了该Custom Element的名称,名称中必须包括一个连字符。
因为不是所有的设备都支持Custom Element,因此开发者在extends
时应该使用最接近其意义的HTML标签名字。 比如定义一个能够响应点击进行交互的Custom Element,那么开发者最好将其定义为扩展自button。
假如没有任何一个HTML标签在语义上与开发者定义的Custom Element相近,那么可以省略extends
属性。这些元素将使用的name
属性值作为标签名称。出于这个原因,这种元素被称为自定义标签,不支持Custom Element的设备将一视同仁的把其作为HTMLUnknownElement对待。
方法和属性
您可以定义Custom Element的脚本API,用嵌套的<script>
标签把Custom Element的原型对象的方法和属性放进去:
<element name="tick-tock-clock"> <script> ({ tick: function () { … } }); </script> </element>
脚本中最后一个值的属性将被复制到原型对象中。 在上面的例子中,<tick-tock-clock>
元素会有一个tick
方法。
周期回调
Custom element具备周期回调能力。它们是:readyCallback
,自定义元素创建后被调用;insertedCallback
,自定义元素插入到文档后被调用;removedCallback
,自定义元素从文档中删除后被调用。
下面的例子演示了如何使用Template,Shadow DOM(下面详细介绍)和周期回调创建clock元素:
<element name="tick-tock-clock"> <template> <span id="hh"></span> <span id="sep">:</span> <span id="mm"></span> </template> <script> var template = document.currentScript.parentNode.querySelector('template'); function start() { this.tick(); this._interval = window.setInterval(this.tick.bind(this), 1000); } function stop() { window.clearInterval(this._interval); } function fmt(n) { return (n < 10 ? '0' : '') + n; } ({ readyCallback: function () { this._root = this.createShadowRoot(); this._root.appendChild(template.content.cloneNode()); if (this.parentElement) { start.call(this); } }, insertedCallback: start, removedCallback: stop, tick: function () { var now = new Date(); this._root.querySelector('hh').textContent = fmt(now.getHours()); this._root.querySelector('sep').style.visibility = now.getSeconds() % 2 ? 'visible' : 'hidden'; this._root.querySelector('mm').textContent = fmt(now.getMinutes()); }, chime: function () { … } }); </script> </element>
使用Custom Elements标记
因为Custom Elements使用已有的HTML标签div
、button
、option
等,所以当使用Custom Elements时我们需要一个属性来指明。这个属性叫做is
,其值为Custom Elements的名称。比如:
<element extends="button" name="fancy-button"> <!-- definition --> … </element> <button is="fancy-button"> <!-- use --> Do something fancy </button>
在脚本中使用Custom Elements
可以在脚本中使用register
方法注册Custom Elements,作为直接使用<element>
元素的一种替代方法。 在这种情况下,可以通过直接操纵原型对象设置元素的方法和属性。 register
返回一个可以用来创建Custom Elements实例的函数:
var p = Object.create(HTMLButtonElement.prototype, {}); p.dazzle = function () { … }; var FancyButton = document.register('button', 'fancy-button', {prototype: p}); var b = new FancyButton(); document.body.appendChild(b); b.addEventListener('click', function (event) { event.target.dazzle(); });
不管是是直接使用<element>
还是使用register
方法来使用,都可以在脚本中使用标准方法createElement
来实例化。
var b = document.createElement('button', 'fancy-button'); alert(b.outerHTML); // will display '<button is="fancy-button"></button>' var c = document.createElement('tick-tock-clock'); alert(c.outerHTML); // will display '<tick-tock-clock></tick-tock-clock>'
元素更新
Custom Elements被加载时,每个相匹配的符合定义的元素都会被更新,使得其脚本API和周期回调可用。
可以对没有样式的内容使用CSS伪类:unresolved
防止闪烁,此伪类将匹配任何没有定义的Custom Elements:
<style> tick-tock-clock:unresolved { content: '??:??'; } </style> <tick-tock-clock></tick-tock-clock> <!-- will show ??:?? -->
此伪类也可以在脚本中使用,以避免与尚未更新的元素发生交互:
// Chime ALL the clocks! Array.prototype.forEach.call( document.querySelectorAll('tick-tock-clock:not(:unresolved)'), function (clock) { clock.chime(); });
要通知页面中其它已经更新的元素,可以添加一个自定义事件。脚本会推迟与元素交互,直到其已经更新至可以监听此事件。
扩展Custom Elements
除了HTML元素,你也可以通过<element>
元素中的extends
属性指定Custom Elements的自定义名称,或是另一个自定义原型:
<element extends="tick-tock-clock" name="grand-father-clock"> … </element> <script> var p = Object.create(Object.getPrototypeOf(document.createElement('tick-tock-clock'))); p.popOutBirdie = function () { … } var CuckooClock = document.register('cuckoo-clock', {prototype: p}); var c = new CuckooClock(); c.tick(); // inherited from tick-tock-clock c.popOutBirdie(); // specific to cuckoo-clock </script>
Shadow DOM 影子 DOM
影子 DOM 规范 是 Web Components 此部分的规范性描述。
影子 DOM 是 DOM 节点的附属树。这些影子 DOM 子树可以关联到元素,但是不作为元素的子节点出现,相反子树有自己的作用域。比如, 影子 DOM 子树可以包含 ID 和样式,它们可以与文档里的 ID 和样式重复,但是由于影子 DOM 与文档独立(不像子节点列表),影子 DOM 里面的 ID 和样式不会与文档中的冲突。
调用 createShadowRoot 方法可以把影子 DOM 应用到某个元素。返回 ShadowRoot节点可用于填充 DOM 节点。
带有影子 DOM 的元素叫影子宿主。元素有了影子 DOM ,它的子元素不被渲染;取而代之的是影子 DOM 的内容。
插入点
影子 DOM 子树在渲染输出时,可以使用<content>
元素指定插入点。带影子 DOM 的元素的子元素在插入点显示,<content>
元素作为一个插入点,仅用于渲染 —— 元素出现在 DOM 中的位置没有变化。
在影子 DOM 子树可以有多个插入点!select属性用于选择子元素出现在哪个插入点,正如 details-open装饰器例子所阐述的。
<a id="#summary"> ▾ <content select="summary"></content> </a> <content></content>
插入点用于重新整理或有选择地忽略子元素的渲染,但是它们不会导致多次渲染。通过选择子元素决定每个元素的渲染,一旦子元素被选中将在一个插入点被渲染,其它的不能声明它,这就是 details-open 装饰器仅渲染 summary 一次的原因。
二次规划
影子 DOM 子树中的元素也许会有自己的影子根节点。这样的话,嵌套影子 DOM 子树的插入点对外部影子 DOM 子树的插入点起作用,如下所示:
<!-- document --> <div id="news"> <h1>Good day for kittens</h1> <div class="breaking">Kitten rescued from tree</div> <div>Area kitten "adorable"—owner</div> <div class="breaking">Jiggled piece of yarn derails kitten kongress</div> </div> <!-- #news' shadow --> <template id="t"> <content select="h1"></content> <div id="ticker"> <content id="stories"></content> </div> </template> <!-- #ticker's shadow --> <template id="u"> <content class="highlight" select=".breaking"></content> <content></content> </template> <script> // Set up shadow DOM var news = document.querySelector('#news'); var r = news.createShadowRoot(); var t = document.querySelector('#t'); r.appendChild(t.content.cloneNode(true)); var ticker = r.querySelector('#ticker'); var s = ticker.createShadowRoot(); var u = document.querySelector('#u'); s.appendChild(u.content.cloneNode(true)); </script>
从概念上讲,第一个文档和第一个影子根节点联合创建了以下虚拟 DOM 树。插入点已被抹去,影子宿主元素的子节点出现在相应位置。
<div id="news"> <h1>Good day for kittens</h1> <div id="ticker"> <div class="breaking">Kitten rescued from tree</div> <div>Area kitten "adorable"—owner</div> <div class="breaking">Jiggled piece of yarn derails kitten kongress</div> </div> </div>
然后,中间的虚拟树与第二个影子根节点联合创建另一个虚拟 DOM 树。所有的 breaking 新闻出现在了顶部。这是因为 <content class="highlight" select=".breaking">
插入点。注意元素被重新整理,尽管 breaking
class 没有出现在 #ticker
元素的子元素,因为插入点是从中间的虚拟树选择的。通过多个插入点规划节点的方式叫做二次规划。
<div id="news"> <h1>Good day for kittens</h1> <div id="ticker"> <div class="breaking">Kitten rescued from tree</div> <div class="breaking">Jiggled piece of yarn derails kitten kongress</div> <div>Area kitten "adorable"—owner</div> </div> </div>
后备内容
插入点也可能有内容,这叫做后备内容,如果没有内容分配给插入点,后备内容才显示。例如,如果没有标题 /或者 stories 可用,新闻 ticker 影子 DOM 可以修改成显示默认文字:
<!-- #news' shadow --> <template id="t"> <content select="h1">Today's top headlines</content> <div id="ticker"> <content id="stories"> No news <button onclick="window.location.reload(true);">Reload</button> </content> </div> </template>
多影子子树
任何元素都可以有多个影子 DOM 子树。不要如此迷惑!实际上,当你扩展一个已有影子 DOM 子树的自定义元素时非常平常。可怜的旧树发生了什么?我们可以丢弃它,你难道不想吗?难道想重用它?
有另一种插入点用于此目的:<shadow>
元素,它把先前用到的影子 DOM 子树放进去(也叫旧树)。比如,有个自定义元素扩展了 <tick-tock-clock> 元素,增加了方向标:
<element name="sailing-watch" extends="tick-tock-clock"> <template> <shadow></shadow> <div id="compass">N</div> </template> <script> … </script> </element>
由于一个元素可以有多个影子,我们需要理解这些影子如何交互,对呈现这些交互元素的孩子有什么影响。
首先,影子 DOM 子树的应用顺序很重要,因为你不能移除影子根,顺序是:
- 用户代理影子 DOM (排第一位)
- 基础自定义元素的影子 DOM
- 首先派生的自定义元素的影子 DOM
- ...
- 使用脚本添加的影子 DOM
- 装饰器影子(通过 CSS 规则应用和删除 —— 不是学术上的影子 DOM ,只是它的插入点功能跟影子 DOM 类似)
下面,我们看下影子 DOM 子树的堆栈,从最后应用的子树(最年轻的)开始,向后遍历它。在树的顺序中遇到的每个<content>
插入点,照常抓取所需的宿主元素的子元素。
有意思的是,一旦我们调动子元素到正确的位置用于渲染,我们检查一下是否有<shadow>
元素,如果没有,就结束。
如果有,我们在取代<shadow>
元素的列表中,找下一个影子子树,首先取代<content>
插入点,然后是第一个<shadow>
,如此反复,直到堆栈的终点。
然后,我们就有了奇妙的影子 DOM 世界树,准备用于渲染。
有个简单的诀窍可以记住这一过程:
- 最新应用的影子 DOM 子树,
<content>
元素可以得到最新鲜的孩子元素。 - 一旦下一个最近的影子 DOM 子树有了自己的路径 —— 如果允许 —— 可以查找剩下的孩子元素。
- 循环往复,直到当前的影子 DOM 子树没有
<shadow>
元素,或者我们已经处理到此元素最旧的 DOM 子树。
CSS 和 影子 DOM
当创建了一个自定义元素,很自然会考虑到它的标记(属性和内容)和脚本接口。在页面中如何与样式交互,往往是同样值得思考的问题。影子 DOM 为开发者提供了许多控制,包括影子 DOM 内容如何与样式交互。
影子 DOM 子树被无形的边界包围,它是默认应用的用户代理样式,而不是开发者的样式。继承像往常一样生效。通常你想要:在以上<sailing-watch>例子中,如果页面有样式,周围的文本是漂亮的海绿色,符合航海的主题,影子 DOM 中的文本(比如方向标的“N”)将是海绿色,因为颜色是可继承的属性。但是如果开发者把所有<div>
元素应用了橙色边框,方向标将不会有橙色边框,因为边框属性不是继承的。
影子宿主有两个属性控制这一行为。第一个,applyAuthorStyles,将应用开发者样式表。写影子 DOM 适当设置这个属性,可以尽可能地匹配周围内容的外观。有个忠告:CSS 选择器匹配影子 DOM 子树,而不是 flattened 树。使用这一属性时,需关注孩子,子孙,兄弟和“nth-of-x”选择器。
第二个影子宿主属性resetStyleInheritance可以控制影子 DOM 子树上周围内容的样式.如果属性设置成true
,所有在影子边界的属性被重置为初始值。
如果applyAuthorStyles
是false
,设置resetStyleInheritance
为true
,一切将从零开始。你的页面元素与样式隔绝——设置继承的属性——你可以用浏览器重置样式创建真正想要的风格。
插入点有相似的边界。影子 DOM 子树的样式不会应用到分布到插入点的宿主元素的孩子。但是一些插入点是非常特殊的选择器,它用途特殊,需要给分布的内容应用样式。影子 DOM 的::distributed 伪类用于此目的。看看新闻例子:
<!-- #ticker's shadow --> <template id="u"> <style> content::distributed(*) { display: inline-block; } *::distributed(.breaking) { text-shadow: 0 0 0.2em maroon; color: orange; } </style> <content class="highlight" select=".breaking"></content> <content></content> </template>
终于,有两个可控的方式允许页面给影子 DOM 子树的内容应用样式。首先在影子 DOM 子树通过分配伪 ID 暴露一个特殊元素。然后,开发者样式可以作为伪元素引用它。比如外部的“新闻”影子 DOM 想让开发者给新闻显示的 ticker 部分加样式,可以设置伪属性,开发者样式就可以处理组件的这部分:
<script> // Set up shadow DOM … var ticker = r.querySelector('#ticker'); ticker.pseudo = 'x-ticker'; … </script> <!-- change the appearance of the ticker part --> <style> #news::x-ticker { background: gray; color: lightblue; } </style>
页面可以有选择地给影子 DOM 子树的内容应用样式的另一种方式是使用 CSS Variables 。比如,我们可以定义一对突出的颜色,而不是把栗色硬编码成橙色:
<!-- #ticker's shadow --> <template id="u"> <style> @host { :scope { white-space: nowrap; overflow-style: marquee-line; overflow-x: marquee; } } content::distributed(*) { display: inline-block; } *::distributed(.breaking) { text-shadow: 0 0 0.2em var(highlight-accent, maroon); color: var(highlight-primary, orange); } </style> <content class="highlight" select=".breaking"></content> <content></content> </template> <!-- change the appearance of the ticker part --> <style> #news::x-ticker { background: gray; color: lightblue; var-highlight-primary: green; var-highlight-accent: yellow; } </style>
伪元素用于开放所有特定元素的属性让开发者加样式。CSS Variables 用于把开发者样式放到一个有限的属性集,但是有些是重复的,比如主题颜色。
Shadow DOM 中的事件
为了确保影子 DOM 子树的元素没有暴露给外部的子树,当从内部子树派发事件时,有件事会发生。
首先,一些事件(像 mutation 和 selectstart 事件)禁止跑出影子 DOM 子树——它们也不监听外部情况。
retargeted这些事件横跨了影子 DOM 边界——它们的target
和relatedTarget
值被改进,指向影子 DOM 子树的宿主元素。
一些情况,像DOMFocusIn
,mouseover
,mouseout
必须额外注意:如果在影子子树里的两个元素间移动鼠标,不想触发文档的事件,因为重定位目标后会出现一些无意义的东西。(什么?元素仅仅记录了鼠标从它自己移回它自己的信息?!)
Imports
HTML Imports 规范是关于 Web 组件这一部分的标准描述。
自定义元素和装饰器可以从外部文件加载,需使用 link 标签:
<link rel="import" href="goodies.html">
仅有<decorator>
元素和<element>
被用户代理解析,尽管通过 import属性编写脚本对文档 DOM 是可行的。跨源检索的文档使用 CORs 判定设计的定义,为了跨站点运行。
附录 A. 接口和元素
这一部分提供了 Web 组件定义的接口和元素的索引,链接到它们的规范定义。
接口
- CSSHostRule
- CSSRule (部分的)
- 文档 (部分的—register, createElement, createElementNS)
- 元素(部分的)
- HTMLLinkElement (部分的)
- ShadowRoot
元素