关于本系列HTML5 的标准 UI 组件模型目前仍正不断演变。在本系列文章中,HTML5 专家 David Geary 将向您展示如何使用现有技术创建您自己的专属 HTML5 组件,以及如何利用现有的规范定义一个真实的 HTML5 组件系统。 在这个由三部分组成的系列文章的前两部分中,您已经学习了如何实现一个专属 HTML5 滑块组件。该组件提供了许多复杂功能;例如,当用户单击轨道时,该组件会使用 CSS3 转换顺畅地为滑块旋钮制作动画效果。但从用户角度来看(正如您在第 1 部分中看到的),在应用程序中使用它没有在内置 HTML 元素中使用它那样简单。
要使用这个专属滑块,您必须: - 向您的 HTML 添加一个空的
DIV 。 - 创建一个新的
COREHTML5.Slider 。 - 将您在第二步创建的滑块附加到第一步创建的空的
DIV 中。 - 实现事件处理程序,以便将控制权转交给滑块。
除了难以使用之外,该专属滑块还容易被滥用,无论是有意的还是无意的。任何人都可以访问这个滑块的元素,因为(几乎和当今其他所有 JavaScript 专属组件一样)滑块可直接将它们添加到 DOM 树中。 专属组件专属组件是一个无需遵循标准 API 来实现组件的组件。
理想的做法是减少先前的步骤,仅 将滑块标记添加到 HTML中。在本文中,您所要做的就是,使用以下 API 将专属滑块转换成为标准的 HTML5 组件:
- Shadow DOM:Shadow DOM API 支持您将元素添加到通过常用方法(比如
document.getElementById() )无法访问的 DOM 树。Shadow DOM 中的元素隐藏在暗处;它们对 DOM 树其他部分实际上是不可见的,在默认情况下,它们不受周围文档中的 CSS 的影响。
- 模板:HTML5 模板是一个惰性文档片段。当您需要显示这些片段时,可以 压制(stamp out)(意思是 创建)该模板,让片段出现在 DOM 树中。这些模板允许您封装那些您可以创建和定制的 HTML、JavaScript 和 CSS。
- 定制元素定制元素是组件的标签。定制组件的标签必须包含一个连字符(例如
<custom-tag> ),除此之外,它们与内置 HTML 标签没什么区别。 - HTML 导入:HTML 导入允许您从一个 HTML 文件中导入另一个,这使得您可以在其自身文件中实现定制组件。
这些 API 目前仍正开发中。撰写文本之时,还没有浏览器支持所有这些 API。因此,如果现在开始实现 HTML5 标准组件,那么可以从 Polymer 和 X-Tags 这两个开源项目中进行选择,它们提供了缺失的功能。
本文向您展示了如何:
HTML 5 专题HTML5 代表了 Web 业务和云业务在实现方式上的里程碑式改变。本 HTML 5专题将顺应潮流为您介绍一些和 HTML5 新特性相关的内容,及其炫酷的效果。 - 在 Shadow DOM 中封装组件标记。
- 从惰性 HTML 模板中创建活动 DOM 元素。
- 创建定制元素并将它们连接到 JavaScript。
- 导入 HTML 文件。
- 使用 Polymer 实现组件。
- 使用 X-Tags 实现组件。
仅适用于 Chrome在撰写本文时,Shadow DOM 示例只能在最新版的 Chrome 浏览器
中运行。 Shadow DOM图 1 显示了一个正在播放视频的 HTML5 应用程序,以及 Chrome 的 Element 检查器(在 Tools->Developer tools下提供),该检查器将显示 video 元素内的元素:
图 1. 查看 video 元素的内部请注意,尽管 video 元素显然包含一个播放 / 暂停按钮、进程指示器和其他元素,但 Element 检查器显示 video 元素内部只有一个 source 元素。您看不见它们,因为它们位于 Shadow DOM 中,默认情况下是看不见的。图 2 展示了如何在 Chrome 的 Element 检查器中显示 Shadow DOM 元素:
图 2. 在 Chrome 中启动 Shadow DOM要得到 图 2所示的设置对话框,单击 Chrome 开发人员工具窗口右下角的齿轮图标。向下滚动 Show Shadow DOM 复选框。在选中该复选框时,Chrome 的 Element 检查器显示了 Shadow DOM 中的元素,如图 3 所示:
图 3. 查看 video 元素的 Shadow DOM图 3和 图 1一样,显示了 video 元素中的元素,但这一次 video 元素的内部构件是可见的,因为 Chrome 正在显示 Shadow DOM 元素。
Shadow DOM(直到近期才向浏览器实现者提供)已被证明对开发人员非常有用,因为在默认情况下,Shadow DOM 元素是不可访问的,而且不受周围 DOM 树的影响。因为从本质上讲,它们封装在自己的世界中,其他组件不会影响它们的工作方式。
图 4 显示了一个人为示例 (contrived example) 的结果,该示例展示了如何将元素放入 Shadow DOM:
图 4. 改变按钮的影子根 (shadow root)该应用程序开始看起来如 图 4左边的屏幕截图所示,只显示一个标题和一个按钮。如果单击该按钮,那么应用程序将会使用图片和字幕替换文本(如右边屏幕截图所示),图片和字幕位于按钮的 Shadow DOM 中。
清单 1 显示了应用程序的 HTML,如 图 4所示:
清单 1. 填充按钮的 Shadow DOM <!DOCTYPE html>
<html>
<head>
<title>Shadow DOM</title>
<style>
button {
font: 18px Century Schoolbook;
border: thin solid gray;
background: rgba(200, 200, 200, 0.5);
padding: 10px;
}
</style>
</head>
<body>
<h1>Shadow DOM</h1>
<button id='button'>Click this button</button>
<script>
var b = document.getElementById('button');
b.onclick = function (e) {
var sr= b.webkitCreateShadowRoot();
sr.innerHTML= '<p>This content is in the shadow DOM</p>' +
'<img src="beach.png">';
};
</script>
</body>
</html> 在浏览器中加载应用程序时,清单 1底部的 JavaScript 开始运行,并创建一个 onclick 事件处理程序,该程序使用 webkitCreateShadowRoot() 方法创建 影子根,即 Shadow DOM 树中的顶端节点。事件处理程序然后使用两个元素填充影子根:一个段落和一个图像。
Shadow DOM 提供了封装
Shadow DOM 为定制组件提供了面向对象的编程的最基本原则 —封装。组件开发人员可以使用 Shadow DOM 以 CSS 样式或编程访问的形式保护其组件的内部构件免受外部干扰。这项功能对于确保组件可以安全互操作至关重要。
无论何时为元素创建一个影子根并随后设置该元素的内部 HTML,都会覆盖元素的原始内容,正如 图 4中的应用程序所示。
默认情况下,按钮没有 Shadow DOM,因此,浏览器允许您创建一个,如 清单 1所示。但是对于元素(比如 video 元素)来说,默认情况下已经有一个 Shadow DOM,该浏览器不太适用。如图 5 所示,sr = b.webkitCreateShadowRoot();
导致 Chrome Element 检查器发出一个错误消息:A Node was inserted somewhere it doesn't belong。
图 5. 尝试更改 video 元素的影子根(并获得失败)回页首 模板、定制元素和 HTML 导入现在,您已经看到了 Shadow DOM 正在运行,我将它和其他主要 HTML5 Web 组件(模板、定制元素和 HTML 导入)投入实际使用。
图 6 显示了 HTML5 Denver Meetup 组中的一个简单的无序 meetup 列表:
图 6. 一个无序的 meetup 列表创建 图 6中的无序列表的标记如清单 2 所示:
清单 2. meetup 无序列表的 HTML <!DOCTYPE html>
<html>
<head>
<title>Meetups Component</title>
</head>
<body>
<ul>
<li class="mobile">Sencha Touch.
<span class='date'>Jan 23</span></li>
<li class="html5-in-general">HTML5 & Semantic Markup.
<span class='date'>Jun 18</span></li>
<li class="html5-in-general">HTML5 & Windows 8.
<span class='date'>Jul 23</span></li>
<li class="javascript">HTML5 JavaScript on Crack.
<span class='date'>Aug 20</span></li>
<li class="javascript">CoffeeScript for Recovering JavaScript Programmers.
<span class='date'>Sept 17</span></li>
<li class="html5-in-general">ClojureScript and CouchDB.
<span class='date'>May 21</span></li>
<li class="html5-in-general">Polyfills for the Pragmatist.
<span class='date'>Apr 23</span></li>
<li class="design">CSS3 for Programmers.
<span class='date'>Feb 20</span></li>
<li class="lightning">Quick-start web apps;
Graphics; Data visualization; Adaptive patterns; JSON.
<span class='date'>Oct 22</span></li>
<li class="lightning">Web workers; CSS3; HTTP; Audio, video, & canvas;
Charting; JS evolution.
<span class='date'>March 19</span></li>
</ul>
</body>
</html> 清单 2的 HTML 非常普通,只创建了一个有列表项的无序列表。图 7 显示了相同的列表项,但并不是在无序列表中,这些列表项位于一个定制组件中,而且由该组件操作:
图 7. 一个定制 meetup 列表组件
清单 3 显示了 图 7所示的应用程序的标记:
清单 3. 一个 meetup-talks 组件,而不是无序列表 <!DOCTYPE html>
<html>
<head>
<title>Meetups Component</title>
<script src="../polymer/polymer.js"></script>
<link rel="components" href="meetup-component.html">
</head>
<body>
<meetup-talks>
<li class="mobile">Sencha Touch.
<span class='date'>Jan 23</span></li>
<li class="html5-in-general">HTML5 & Semantic Markup.
<span class='date'>Jun 18</span></li>
<li class="html5-in-general">HTML5 & Windows 8.
<span class='date'>Jul 23</span></li>
<li class="javascript">HTML5 JavaScript on Crack.
<span class='date'>Aug 20</span></li>
<li class="javascript">CoffeeScript for Recovering JavaScript Programmers.
<span class='date'>Sept 17</span></li>
<li class="html5-in-general">ClojureScript and CouchDB.
<span class='date'>May 21</span></li>
<li class="html5-in-general">Polyfills for the Pragmatist.
<span class='date'>Apr 23</span></li>
<li class="design">CSS3 for Programmers.
<span class='date'>Feb 20</span></li>
<li class="lightning">Quick-start web apps;
Graphics; Data visualization; Adaptive patterns; JSON.
<span class='date'>Oct 22</span></li>
<li class="lightning">Web workers; CSS3; HTTP; Audio, video, & canvas;
Charting; JS evolution.
<span class='date'>March 19</span></li>
</meetup-talks>
</body>
</html> 请注意,清单 3和 清单 2的标记几乎完全相同,它们有三个差异: - 清单 3中的标记包含一个名为 polymer.js 的文件。该文件是 Polymer 开源项目中附带的,该项目提供了 HTML 5 Web Component API(Shadow DOM、模板,等等)的实现。Polymer 项目(我将在接下来的 Polymer小节对其进行详细讨论)是由 Chrome 开发团队实现的。
- 清单 3使用了一个
link 元素来导入其他 HTML5 文件。link 元素的使用被称为 HTML 导入。在撰写本文之时,HTML 导入还无法在任何浏览器中实现;在本例中,该功能来自 Polymer 项目。
- 清单 2中的无序列表元素可使用 清单 3中的
meetup-talks 定制元素替换。
meetup-talks 定制元素是在 meetup-component.html 中实现的,如清单 4 所示:
清单 4. meetup-list 元素<element name="meetup-list">
<template>
<style>
/* styles that follow only apply to the ShadowDOM of the <meetup-list> element */
#meetups {
display: block;
padding: 15px;
padding-top: 0px;
background: lightgray;
border: thin solid cornflowerblue;
}
.title {
color: blue;
font-size: 1.5em;
}
</style>
<div id='meetups'>
<p class='title'>HTML5 in General</p>
<content select='.html5-in-general'></content>
<p class='title'>JavaScript</p>
<content select='.javascript'></content>
<p class='title'>Design</p>
<content select='.design'></content>
<p class='title'>Lightning</p>
<content select='.lightning'></content>
</div>
</template>
<script>
Polymer.register(this);
</script>
</element> 清单 4让一切变得很有趣。首先,我声明了一个名为 meetup-list 的新元素,请注意,根据 HTML 规范,需要在名称中包含一个连字符。在清单结尾处,有一行 JavaScript 代码向 Polymer 全局对象注册了 meetup-list 元素。小心地声明和注册定制元素,以便可以在 HTML 页面中使用它们。
我在 element 元素内部声明了一个 template 。模板以声明方式而不是以编程方式定义了 Shadow DOM。因此,在 清单 4中,我创建了一个 Shadow DOM 模板。每当有人在 HTML 页面中使用 meetup-list 元素时,浏览器就会使用此 Shadow DOM 模板为有问题的元素创建一个新的 Shadow DOM 实例。
在该模板中,我定义了两个 只应用于该模板中的元素的 CSS 样式。您可能还记得,因为一个模板代表一个 Shadow DOM,所以周围文档中定义的 CSS 样式反而 不会影响该模板中的元素。例如,如果您要将段落元素的样式添加到 清单 3中的 HTML 中,该样式不会影响模板中的段落。
除了 CSS 样式之外,该模板也在后续 Shadow DOM 实例中定义了一些元素。content 元素使用一个 CSS 选择器从 meetup-list 的 原始内容中选择列表项。因此,除了以声明方式指定 Shadow DOM 实例之外,该模板还可以 有选择地将元素的原始内容插入模板中。请注意,您可以使用 <content></content> 将一个元素的所有原始内容插入其模板中。
回页首 立即实现 Web 组件我在本文中讨论的所有 Web Components API 都是相对较新的,各个浏览器供应商对它们的支持程度各不相同。为了立即有效实现这些组件,不能只编写代码并将代码加载到浏览器中;还需要知道在 HTML5 业务中哪些内容是 polyfill或 shim:允许您使用新功能(如果可用的话)的解决方案,但在不可用时,会返回一个受支持的备用解决方案。
目前,有两个项目提供了 Web Components polyfill:Polymer 和 Mozilla X-Tags。Polymer 只能在 Chrome
中使用,并需要一个 Web 服务器来使用 HTML 导入,它不适于生产。X-Tags 几乎可以在所有现有浏览器中使用,不需要 Web 服务器;但是,X-Tags 只实现定制元素 API,不能实现模板和 Shadow DOM。
接下来,我将分别向您展示如何使用 Polymer 和 X-Tags,将一个标准组件包装在本系列的上一篇文章中实现的专属滑块中。
回页首 Polymer按照定义,专属组件不需要遵循组件标准,因此没有实现或使用它们的标准方法。但是,通过将它们包装在标准组件中,可以使它们更易于使用。首先,我使用 Polymer 将这个专属滑块包装在一个标准组件中。图 8 显示了该滑块的 Polymer 版本:
图 8. 一个 polymer 滑块清单 5 显示了如何使用滑块的 Polymer 版本: 清单 5. 使用 Polymer 滑块标签 <!DOCTYPE html>
<html>
<head>
<title>Slider with Polymer</title>
<script src="../polymer/polymer.js"></script>
<script src="lib/slider.js"></script>
<link rel="import" href="./slider.html" />
</head>
<body>
<x-slider id='slider'
fillColor='goldenrod'
strokeColor='red'>
</x-slider>
</body>
</html> 如果将 清单 5和本系列第一篇文章中的清单 2(它显示了如何单独使用该专属滑块)进行比较,您会发现 清单 5显然更简单一些。您可以使用 x-slider 元素创建一个只有一行 HTML 代码的滑块。
x-slider element 是在清单 6 中实现的:
清单 6. Polymer 滑块组件<element name="x-slider" attributes="strokeColor fillColor">
<template>
<style>
x-slider {
display: block;
}
.x-slider-button {
float: left;
margin-left: 2px;
margin-top: 5px;
margin-right: 5px;
vertical-align: center;
border-radius: 4px;
border: 1px solid rgb(100, 100, 180);
background: rgba(255, 255, 0, 0.2);
box-shadow: 1px 1px 4px rgba(0,0,0,0.5);
cursor: pointer;
width: 25px;
height: 20px;
}
.x-slider-button:hover {
background: rgba(255, 255, 0, 0.4);
}
#xSliderDiv {
position: relative;
float: right;
width: 80%;
height: 65%;
}
</style>
<div id='x-slider-buttons-div'
style='width: {{width}}px; height: {{height}}px;'>
<button class='x-slider-button' id='xSliderMinusButton'>-</button>
<button class='x-slider-button' id='xSliderPlusButton'/>+</button>
<div id='xSliderDiv'></div>
</div>
</template>
<script>
Polymer.register(this, {
width: 350,
height: 50,
strokeColor: 'blue',
fillColor: 'cornflowerblue',
ready: function () {
var self = this,
slider = new COREHTML5.Slider(this.strokeColor,
this.fillColor, 0);
setTimeout( function () { // This timeout is a hack
slider.appendTo(self.$.xSliderDiv);
slider.draw();
}, 200);
this.$.xSliderMinusButton.onclick = function () {
if (slider.knobPercent >= 0.1) {
slider.knobPercent -= 0.1;
slider.erase();
slider.draw();
}
};
this.$.xSliderPlusButton.onclick = function () {
if (slider.knobPercent <= 0.9) {
slider.knobPercent += 0.1;
slider.erase();
slider.draw();
}
};
}
});
</script>
</element> 清单 6类似于 清单 4。这两个清单都定义了一个新元素,并向 Polymer 全局对象注册该元素。它们还定义了一个包含 CSS 样式和标记的模板。清单 6与 清单 4的区别是:清单 6 使用了 Polymer
数据绑定和一个 ready 事件处理程序。
双大括号(如 {{width}} )表示一个元素属性。您可以在您的元素标记中使用双大括号。在 清单 6中,我将元素的封闭 DIV 的宽和高设置为页面作者使用元素属性设置的宽和高。
您可以在一个对象中声明属性,该对象可传递到 Polymer.register() 方法;例如,
清单 6为 x-slider 元素声明了 4 个属性:width 、height 、strokeColor 和 fillColor 。如果您使用
element 元素的 attributes 属性来指定属性(也称为 发布属性),页面制作者可在其元素中使用那些属性,就像 清单 5中的 x-slider 元素一样。
根据 Polymer 文档的记载,当一个组件已经完成其自身的初始化之后,它将调用其 ready() 方法(如果该方法存在的话)。该文档没有进一步详细介绍何时对组件进行初始化,但该操作显然是在浏览器准备绘制组件之前完成的。因为是在准备绘制组件之前对组件进行初始化的,而且此时 ready() 方法是生命周期中的惟一方法,所以我必须添加一个 setTimeout() 程序 (hack),该程序在调用 ready() 方法后的 200 毫秒后绘制了一个滑块。像这样的不完善之处无疑会随时间逐渐消失。(Web Components 规范还定义了一个 inserted 回调函数,在将一个元素插入 DOM 后,浏览器会调用此函数,但是在本文发布时 Polymer 还不支持此函数。)
Polymer 也提供一个关于定制元素的 $ 属性,参考了元素属性的映射。我使用该映射来访问最终包含该专属滑块的 DIV 。
回页首 X-Tags滑块的 X-Tags 版本如图 9 所示: 图 9. 一个 X-Tags 滑块组件滑块的 Polymer 实现和 X-Tags 实现从视觉上讲很难区分;我使用不同颜色来实现它们只是为了说明它们是使用不同框架实现的。清单 7 显示了 图 9中显示的应用程序如何使用滑块的 X-Tags 版本(也可以作为 x-slider 实现)
:
清单 7. 使用 X-Tags 滑块组件 <!DOCTYPE html>
<html>
<head>
<title>Slider with x-tag</title>
<link rel="stylesheet" type="text/css" href="x-slider.css" />
</head>
<body>
<x-slider id='slider'
slider-fill-color='rgba(50, 105, 200, 0.8)'
slider-stroke-color='navy'>
</x-slider>
</body>
<script type="text/javascript" src="lib/x-tag.js"></script>
<script type="text/javascript" src="lib/slider.js"></script>
<script type="text/javascript" src="x-slider.js"></script>
</html> 清单 7和 清单 5类似。这两个清单都使用了一个简单的 x-slider 元素将滑块放入页面中。区别在于 Polymer 版本使用了 HTML 导入来包含定义该滑块的 HTML 片段,而 X-Tags 版本包括 JavaScript,因为它不支持 HTML 导入。
实现该组件的 JavaScript 如清单 8 所示:
清单 8. 滑块的 JavaScript function getFirstAncestor(name, startingElement) {
var element = startingElement
, localName = element.localName;
while (localName !== name) {
element = element.parentNode;
localName = element.localName;
}
return element;
};
function getSlider(element) {
return getFirstAncestor('x-slider', element).slider;
};
xtag.register('x-slider', {
onCreate: function () {
var content =
"<div id='x-slider-buttons-div'>" +
"<button class='x-slider-button' id='x-slider-minus-button'>-</button>" +
"<button class='x-slider-button' id='x-slider-plus-button'>+</button>" +
"</div>" +
"" +
"<div id='x-slider-slider-div'></div>" +
"" +
"<div id='x-slider-readout-div'></div>";
stroke = this.getAttribute('slider-stroke-color'),
fill = this.getAttribute('slider-fill-color');
// 'this' is the x-slider HTML element
this.max = 100;
this.innerHTML = content;
this.slider = new COREHTML5.Slider(stroke, fill, 0);
this.slider.appendTo('x-slider-slider-div');
this.slider.draw();
},
events: {
'click:touch:delegate(#x-slider-plus-button)': function(event, slider) {
var slider = getSlider(this) // 'this' is the button
, value = getFirstAncestor('x-slider', this).getValue();
if (slider.knobPercent <= 0.9) {
slider.knobPercent += 0.1;
}
slider.erase();
slider.draw();
console.log(value);
},
'click:touch:delegate(#x-slider-minus-button)': function(event, slider) {
var slider = getSlider(this) // 'this' is the button
, value = getFirstAncestor('x-slider', this).getValue();
if (slider.knobPercent >= 0.1) {
slider.knobPercent -= 0.1;
}
slider.erase();
slider.draw();
console.log(value);
},
},
methods: {
getValue: function () {
// 'this' is the x-slider HTML element
return this.slider.knobPercent * this.max;
}
}
}); 使用 清单 8中的 X-Tags 定义组件和使用 清单 6中的 Polymer 定义组件有很大的差别。X-Tags 完全使用 JavaScript 来定义元素,这涉及到使用字符串串联和设置一个元素的内部 HTML(以便设置成定制元素的内容)。这和 Polymer 形成鲜明对比,Polymer 在一个可读性更强的、更容易维护的 HTML 中定义定制元素内容。
X-Tags 没有类似 Polymer 的 $ 的属性,这使得它更易于访问定制元素中的元素,因此我必须实现一个 getFirstAncestor() 函数来访问存放在专属滑块中的元素。
最后,X-Tags 为实现事件提供了支持,以便处理控制滑块旋钮的按钮单击;不过,对于此练习,该支持似乎有点大材小用。 为了完整起见,X-Tags 版本滑块的 CSS 如清单 9 所示:
清单 9. 滑块的 CSS x-slider {
display: block;
}
.x-slider-button {
float: left;
margin-left: 2px;
margin-top: 15px;
margin-right: 5px;
vertical-align: center;
border-radius: 4px;
border: 1px solid rgb(100, 100, 180);
background: rgba(255, 255, 0, 0.2);
box-shadow: 1px 1px 4px rgba(0,0,0,0.5);
cursor: pointer;
width: 25px;
height: 20px;
}
.x-slider-button:hover {
background: rgba(255, 255, 0, 0.4);
}
#x-slider-buttons-div {
width: 25%;
height: 100%;
}
#x-slider-slider-div {
position: relative;
float: right;
margin-top: -40px;
width: 80%;
height: 65%;
} 回页首 结束语目前,JavaScript 框架可以实现大多数遵从专属 API 而非标准的专属组件。因此,每当您移动到一个新的框架时,都会面临一个相当漫长的学习过程。因为,专属组件通常没有被封装,所以应用程序中的其他组件可能对这些专属组件功能产生不利影响。
在 本系列文中中,您已经了解了如何使用 HTML5 API 实现标准和专属组件。实现这些组件的 HTML5 规范相对不够成熟,浏览器供应商的支持程度也各不相同。现在您就可以开始实现标准组件了,但您还需要获得浏览器以外的其他支持。在本文中,我简要讨论了两个框架,Polymer 和 X-Tags,它们可以实现标准组件。
随着 Web Components 规范的日益成熟,而且浏览器供应商实现了他们的功能,不久以后我们将会有一个可在其中构建标准组件的可行平台。那时,实现 HTML5 应用程序将会比今天更有趣。
回页首 下载
|