本文来自 mweb.baidu.com 做最好的无线WEB研发团队
是随着 Web 应用不断丰富,过度分离的设计也会带来可重用性上的问题。于是各家显神通,各种 UI 组件工具库层出不穷,煞有八仙过海之势。于是 W3C 坐不住了,大手一挥,说道:不如让我们统一一个 Web Components 的标准吧怎么样。
Web Components 的核心思想就是把 UI 元素组件化,即将 HTML、CSS、JS 封装起来,使用的时候就不需要这里贴一段 HTML,那里贴一段样式,最后再贴一段 JS 了。一般来说,它其实是由四个部分的功能组成的:
- 模板,
<template>
标签 - 自定义元素
- Shadow DOM(隐匿 DOM)
- Imports(导入)
我们还是通过一个简单的例子看看这些新玩意儿都是些什么吧。
一段简单的 HTML
假设我们有一个提供 App 介绍的代码片段,为了不让事情变得更复杂,这里只有 HTML 和 CSS,不关 JS 什么事。
<div class="app-info"> <div class="app-bar"> <img class="app-icon" src="http://img.dayanjia.com/di/TOY7/6c2442a7d933c8950f39059ed31373f083020094.png" width="36" height="36"/> <div class="app-name">百度手机助手</div> <a class="app-downbtn" href="http://gdown.baidu.com/data/wisegame/de5074e4e28aecec/baidushoujizhushou_16783385.apk">下载</a> </div> <div class="app-description"> 百度手机助手是Android手机的权威资源平台,拥有最全最好的应用、游戏、壁纸资源,帮助您在海量资源中精准搜索、高速下载、轻松管理,万千汇聚,一触即得。海量资源:免费获取数十万款应用和游戏,更有海量独家正版壁纸,任你挑选。 </div> </div>
.app-info { padding: 0.2em; border-bottom: 1px dotted #ddd; } .app-bar { display: flex; align-items: center; font-size: 14px; } .app-name { flex-grow: 2; margin-left: 1em; } .app-downbtn { text-decoration: none; padding: 0.2em 1.1em; margin-right: 1em; color: #fff; background: #5573eb; } .app-description { font-size: 12px; }
看上去就是这样的:
模板
HTML 模板这个东西已经存在很久了,模板的实现无非是这么几种。一种是直接写在 DOM 里,但是给它一个display: none
的样式。使用这种模板,我们可以很方便地用 JavaScript 来操作 DOM 结构,但是如果你在模板里写了一个 img
元素之类,不好意思,即使你看不到,这个图片的网络请求还是要发一下的。此外,与模板相对应的 CSS 也是和页面其他部分平行的关系,你需要给模板加一个 ID 之类的选择器前缀来指定样式,以保证不和页面中的其他元素冲突。
第二种是使用 <script>
标签,但是给它指定一个非脚本的 type
属性,这样浏览器就不会把它当做 JS 来执行了:
<script id="template" type="x-tmpl-mustache"> Hello {{ name }}! </script>
这种方法的好处在于,DOM 元素是不会预先渲染的,因为在被 JS 取得模板数据并插入 DOM 之前,它都是一堆死气沉沉的纯文本。同时这也是它的弊端,因为是纯文本,所以你要手动处理这些复杂的标签,需要格外小心 XSS 之类的问题。
于是新的 <template>
标签就被提出了,它可以看做是结合了上面两种方法的优势。我们将上面的 HTML 模版化后:
<template id="appTmpl"> ... 和之前一样的内容 ... </template>
使用下面的 JS 就可以访问到模板,并将其插入 DOM 中。
var tmpl = document.querySelector(‘#appTmpl‘); // 取到 t 以后,可以像操作 DOM 一样随意修改其中的内容 // 然后需要从模板创建一个深拷贝(Deep Copy),将其插入 DOM var clone = document.importNode(tmpl.content, true); // 创建深拷贝还可以使用下面的方法: // var clone = tmpl.content.cloneNode(true); document.body.appendChild(clone);
最后的效果和之前看到的其实是一样的。
当然了,这个模板的实现其实还是很原始的,并没有像 Mustache、Handlebars 等模板库的占位符替换的功能。
Shadow DOM
这个 Shadow 不太好翻译,反正理解成「隐藏在黑暗中的 DOM」就差不多了。所以说,Shadow DOM 其实是在文档的主 DOM 中生成了一块子 DOM,这个子 DOM 的 CSS 环境是和主文档隔离的。可以说,使用 Shadow DOM,我们就拥有了一个组件封装的原始模型。从外面看,它只是一个 DOM 节点,但是这其实是一个黑盒,里面还可以包含复杂的结构。这种抽象其实在大自然中随处可见,例如当我们谈论太阳系的时候,我们会把地球作为一个节点,但是当我们深入地球这个节点时,会发现还存在地月系这个结构。
使用 Shadow DOM,我们需要在一个元素上创建一个根(Root),然后将模板内文档添加到这个根上即可。
<template id="appTmpl"> <style> /* ... 将 CSS 移动到模板内 ... */ </style> ... 原来的模板内容 ... </template> <div class="app"></div>
var tmpl = document.querySelector(‘#appTmpl‘); var host = document.querySelector(‘.app‘); var root = host.createShadowRoot(); root.appendChild(document.importNode(tmpl.content, true));
最终的效果看上去是一样的,但是我们已经将这个 App 信息组件封装了一层 DOM。
自定义元素
现在我们已经能够使用一句 <div class="app"></div>
外加一些 JS 来显示这个 App 信息的组件了(如果它够的上被称作是一个「组件」的话)。但是,我们能不能再给力一点,使用一个自己命名的元素呢?答案当然是肯定的。通过自定义元素的功能,就可以实现通过 <app-info></app-info>
这样的方式来调用它了。
HTML 除了上文的那些模板以外,只需要一个简单的容器。同时,接下来的例子中,我们还可以看到如何使用属性来替换模版中的变量,因此模板中也要做出一些修改。
<template id="appTmpl"> <style> /* ... CSS 省略 ... */ </style> <div class="app-info"> <div class="app-bar"> <img class="app-icon" src="" width="36" height="36"/> <div class="app-name"></div> <a class="app-downbtn" href="">下载</a> </div> <div class="app-description"> <content selector=".description"></content> </div> </div> </template> <app-info name="百度手机助手" downurl="http://gdown.baidu.com/data/wisegame/de5074e4e28aecec/baidushoujizhushou_16783385.apk" iconurl="http://img.dayanjia.com/di/TOY7/6c2442a7d933c8950f39059ed31373f083020094.png"> <p class="description">百度手机助手是Android手机的权威资源平台,拥有最全最好的应用、游戏、壁纸资源,帮助您在海量资源中精准搜索、高速下载、轻松管理,万千汇聚,一触即得。海量资源:免费获取数十万款应用和游戏,更有海量独家正版壁纸,任你挑选。</p> </app-info>
可以看到,Shadow DOM 也可以拥有子元素,而这些子元素在模板中将会使用 <content>
标签进行定位并替换。接下来,我们使用 JavaScript 创建这个名叫 app-info 的自定义元素。
var tmpl = document.querySelector(‘#appTmpl‘); // 创建新元素的 Prototype var appInfoProto = Object.create(HTMLElement.prototype); // 自定义元素在不同的生命周期有不同的 Callback 可以使用。 // createdCallback 是在创建时调用的,此外还有 // attachedCallback(插入 DOM 时的回调)、 // detachedCallback(从 DOM 中移除时的回调)、 // attributeChangedCallback(属性改变时的回调) appInfoProto.createdCallback = function() { var root = this.createShadowRoot(); var name = this.getAttribute(‘name‘) || ‘‘; var downUrl = this.getAttribute(‘downurl‘) || ‘‘; var iconurl = this.getAttribute(‘iconurl‘) || ‘‘; tmpl.content.querySelector(‘.app-name‘).textContent = name; tmpl.content.querySelector(‘.app-downbtn‘).href = downUrl; tmpl.content.querySelector(‘.app-icon‘).src = iconurl; // 将模板插入 Shadow DOM root.appendChild(document.importNode(tmpl.content, true)); }; // 注册自定义元素 var appInfo = document.registerElement(‘app-info‘, { prototype: appInfoProto });
最后看到的效果,其实和之前的没什么不同,但是我们很清楚,一个简单的 Web Component 雏形已经诞生了。
通过 Chrome 的开发工具我们可以很清楚地看到 <template>
中的文档片段和我们自定义的 <app-info>
元素中存在的 Shadow DOM。
导入
Web Components 的最后一部分是导入,这就比较容易理解了,就是提供了一个可复用的途径。我们可以像导入 CSS 一样,导入外部文件中的 HTML 代码。
<link rel="import" href="app-info.html">
小结
Web Components 这个东西还非常新,但是它代表了 Web 前端今后的一个发展方向。包括比较火的 AngularJS 等框架,其中的一些功能也或多或少地在使用 Web Components 的思想,并且推动其标准化(见 the future of AngularJS)。
同时,也是因为它太新了,所以可能还会有非常大的改变,也许过几个月再来看这篇文章,部分内容就已经过时了:D 此外,当前浏览器对 Web Components 的支持也很有限,在 Chrome 35+ 中,本文中的全部例子都可以正常展现,其他浏览器就基本上悲剧了。对于这样一个新生状态,还处于快速变化期的事物,我也仅仅是浅尝辄止,本文更多在于抛砖引玉,若有疏漏还请读者多多指正。
针对 Web Components 的功能,Google 出了一个叫做 polymer 的项目,用于填补目前浏览器尚不能实现的部分,此外还内建了许多做好的组件。其实这个项目也推出挺久的了,但是一直不温不火,风头赶不上同是出自 Google 的 AngularJS。但是今年 Google IO 大会中,它却被作为 Material Design 的一部分拿出来介绍了,可见其还是很受重视的。下次如果有机会,可以介绍一下它。
参考资料:
- Introduction to Web Components – W3C Editor’s Draft 9 June 2014
- A Guide to Web Components
- HTML’s New Template Tag – standardizing client-side templating
- Shadow DOM 101