最好在Chrome 36+测试教程中的示例代码。同时打开开发者工具,将 Settings > General > Elements
中的 Show user agent shadow DOM
选项选中。
最近将大部分时间花在了Web Components上面,不过这些花费的时间是有价值的。我整理了一个小组件,能更好的帮助大家更好的理解一个整体的Web Components。
DEMO 下载源码
Web Components主要由四个部分组成(模板、自定义元素、Shadow DOM和导入),但在这个案例中只关注其中的两个部分:模板( <template>
和Shadow DOM)。其中更主要的是模板。在写这篇文章的时候,浏览器对Web Components的支持度还不是很广,为了让浏览器能正常的渲染,需要使用 Polymer 和 X-Tag 的polyfill库。我想在内部工作中使用之前先仔细研究一下polyfill。
Web Components是一种新兴技术,用来规范组件的定制,这些都要非常感谢W3C组织。Web Components的目的就是允许开发人员使用HTML、CSS和JavaScript来自定义元素。这些元素可以被认为是一些小部件(widgets)。
一个很好的示例就是自定义元素 <github-card>
。如果你有一个GitHub账号,你可以打开 <github-card>
示例页面 ,在输入框中输入你的GitHub的用户名,可以看到你的GitHub相关信息。然后你可以到 <github-card>
文档 下载相应的源码,查看如何使用这样的一个标签元素。
Web Components主要组成:
<template>
标签): 定义的标记块,不会被渲染但可以随后被激活使用。 阅读更多的细节... <github-card>
。 阅读更多的细节... <link>
标签,把一小块的HTML代码加载到页面中。 阅读更多的细节... 在 W3C规范文档 中,Web Components除了上述的四个部分还有一个 Decorators ,基于CSS选择器来应用模板,从而对文档进行丰富的视觉和行为的变化。更多的详细信息可以 点击这里阅读 。但很多开发人员不喜欢它,所以没有很多人去敲定其规范,有可能将来会消失。
比如 <github-card>
具有Web Components所有功能部分,每个都可能很好的使用。但当它们一起工作的时候,就组成了一个Web Components。从概念上讲,它有点类似于AJAX,组合在一起执行一个任务。
我读过Web Components中所有部分(包括"decorator")的介绍和写过一点代码,但给我的感觉是,学习Web Components最好的方法不是阅读而是动手去写。所以,针对Web Components这几个组成部分,我们从模板开始着手。
对于模板,我想展示一个基于JavaScript数据对象创建的简单的书籍列表。事情就是这样开始的...
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>JavaScript Books</title> <link rel="stylesheet" href="css/normalize.min.css"> <link rel="stylesheet" href="css/bootstrap.min.css"> <link rel="stylesheet" href="css/styles.css"> </head> <body> <div id="container"> <header> <h1 class="page-header">JavaScript Books</h1> <h2>Built with templates & Shadow DOM</h1> </header> <template id="singleBook"> <style> .templateArticle { display: inline-block; margin: 6px; } .btn { margin: 10px; float: right; } .thumbnail { margin-bottom: 0; } .bookTitleClass { text-align: left; } #bookTitle { font-style: italic; } </style> <article class="templateArticle panel panel-default"> <header class="panel-heading"> <h2 class="panel-title bookTitleClass"> <span id="bookTitle"></span> <br /> by <span id="bookAuthor"></span> </h2> </header> <img src="" alt="" class="thumbnail"> <a href="" id="btnPurchase" class="btn btn-primary" role="button" target="blank">Buy at Amazon</a> </article> </template> <section id="allBooks" class="allBooksClass"></section> <script src="scripts/main.js"></script> </div> </body> </html>
css/style.css
) body { margin: 20px; } h1, h2 { text-align: center; } footer { text-align: center; margin-top: 20px; } .allBooksClass { margin-top: 30px; text-align: center; }
js/main.js
) (function(){ var jsBooks = { "book1" : { "title": "Object-Oriented Javascript", "author": "Stoyan Stefanov", "image": "images/ooj.jpg", "amazonLink": "http://amzn.to/1sRFbEC" }, "book2" : { "title": "Effective Javascript", "author": "David Herman", "image": "images/effectivejs.jpg", "amazonLink": "http://amzn.to/1pLu1A5" }, "book3" : { "title": "JavaScript: The Good Parts", "author": "Douglas Crockford", "image": "images/goodparts.jpg", "amazonLink": "http://amzn.to/1ukjoIN" }, "book4" : { "title": "Eloquent Javascript", "author": "Marijn Haverbeke", "image": "images/eloquentjavascript.jpg", "amazonLink": "http://amzn.to/1lPP6pn" } }; var template = document.querySelector("#singleBook"), templateContent = template.content, host = document.querySelector("#allBooks"), root = host.createShadowRoot(); for (key in jsBooks) { var title = jsBooks[key].title, author = jsBooks[key].author, image = jsBooks[key].image, amazonLink = jsBooks[key].amazonLink; templateContent.querySelector("img").src = image; templateContent.querySelector("img").alt = templateContent.querySelector("#bookTitle").innerHTML = title; templateContent.querySelector("#bookAuthor").innerHTML = author; templateContent.querySelector("#btnPurchase").href = amazonLink; root.appendChild(document.importNode(templateContent, true)); } })();
index.html
引入了 normalize.css
和 Twitter Bootstrap 的样式文件 bootstrap.css
。Bootstrap提供了响应式布局功能,这里引入主要是为了让页面布局看上去好看一些。另外引入 style.css
文件,这个文件主要是对页面一些元素的样式做了定义,在整页案例中他是一个小角色。
HTML和过去一样,不同的是给Web Components中心部分 template
标签添加了一个 ID
名 singleBook
。把HTML代码和CSS样式以 <style>
放在了 <template>
里面。
<template>
中有一个 <article>
标签:有关于书的数据将解析到这里面。因为模板是惰性的,这意味着如果不和外面通信,那么页面加载这部分是不可见的。
注意, <article>
里面部分是空的:
<span>
标签 <img>
标签中的 src
和 alt
属性 <a>
标签中的 href
属性 这些空的部分将是用来填充我们的对象数据。接下来我们一起来看看...
(function(){ ... })();
所有东西都包裹在一个 IIFE
var jsBooks = { "book1" : { "title": "Object-Oriented Javascript", "author": "Stoyan Stefanov", "image": "images/ooj.jpg", "amazonLink": "http://amzn.to/1sRFbEC" }, ... };
JavaScript 数据对象。这里仅列出其中的一个列表,而每个列表都包括了四个项目,每一个项目都是JavaScript书特定的信息。每个列表都包含了 title
, author
, image
和 amazonLink
属性。
var template = document.querySelector("#singleBook"), templateContent = template.content, host = document.querySelector("#allBooks"), root = host.createShadowRoot();
开始创建一个Shadow DOM。我通过 var
创建了四个变量。
template
直接引用了要渲染的 <template>
,直接引用它的 ID
名 singleBook
templateContent
定义了模板要渲染的内容取决于页面加载时 <template>
的 content
属性值。 详细阅读,点击这里 host
直接引用了所谓的 shadow root
,也就是模板内容将要加载到页面的那个元素。在这个示例中,就是页面中的 <section id="allBooks">
元素。它通常被称为 shadow root
,你可以定义成任何你想要的变量名,但一般约定其变量名为 host
root
直接引用了 shadow root
,将生成的内容插入到 template
中。 host.createShadowRoot()
内容插入到 root
中。在这个示例就中是 <section id="allBooks">
元素中。它可能更会认为是一个真正的Shadow DOM,内容加载到 root
时,将会返回Web页面的文档片段( 有关于文档片段的内容可以点击这里了解 )。其实你也可以将其定义你想定义的变量名,不过默认情况下,大家喜欢将其命名为 root
。 for (key in jsBooks) { ... };
使用一个 for ... in
循环,将 jsBooks
对象内容填充到模板中。代码拆解为:
var title = jsBooks[key].title, author = jsBooks[key].author, image = jsBooks[key].image, amazonLink = jsBooks[key].amazonLink;
将 jsBooks
对象中的列表值指定给对应的变量:
templateContent.querySelector("img").src = image;
循环遍历模板中的 <img>
标签的 src
属性,并且将 image
值赋予给它。
templateContent.querySelector("img").alt = templateContent.querySelector("#bookTitle").innerHTML = title;
循环遍历模板中的 <img>
标签的 alt
属性,并且将 title
值赋予给它。
同时遍历模板中的 #bookTitle
元素(一个 <span>
标签),并且把 title
值赋予给它。
templateContent.querySelector("#bookAuthor").innerHTML = author;
循环遍历模板中的 #bookAuthor
元素(一个 <span>
标签),并且把 author
值赋予给它。
templateContent.querySelector("#btnPurchase").href = amazonLink;
循环遍历模板中的 #btnPurchase
元素(仅有的 <a>
标签)的 href
属性,并且将 amazonLink
值赋予给它。
root.appendChild(document.importNode(templateContent, true));
接下来,我们要花点时间来讨论这行代码。
在代码中,我们所有数据对象填充到模板中,都是由 templateContent
变量完成。但它返回的是文档片段。
文档片段不是页面DOM的一部分,在这个示例中,将文档片段视为外部的一个文件。通过 document.importNode()
函数可以将外部文档(所说的文档片段)填充真实的参数,将内容重复的复制(复制一切)。
从那里,我们把 root
当作父元素,并将文档片段当作其子元素填充到里面。常使用 document.importNode()
将文档片段填充到 root
中。
有关于 document.importNode()
更多的介绍, 可以点击这里进行了解 。
如果我们在一个选中了 Show user agent shadow DOM
的Chrome 36+浏览器中审查 index.html
。通过开发都工具的 Inspect Element
查看示例中的 <section>
标签(show host),你将看到的模板内容(show host)如下所示:
但是有一个问题,Bootstrap样式用于 <template>
模板中某些元素的样式被忽略了。任何包含 panel
和 btn
类名的元素应该会引用Bootstrap的样式,尤其是按钮...
这里发生的一切,正如前面所说的模板内的代码不能和模板外的代码做任何的交流。从技术上说 <template>
在Shadow DOM,它是一个naturally-encapsulated。所以页面中三个样式文件( normalize.min.css
, bootstrap.min.css
和 styles.css
)在模板的布局中都没生效。现在使用 <link>
将样式添加到Shadow DOM中是不允许的。
style.css
文件与模板布局无关,但其它两个样式文件有关系。解决方案就是通过 @import
在模板的 <style>
中将样式文件引入进来。
<style> @import url("css/normalize.min.css"); @import url("css/bootstrap.min.css"); ... </style>
使用 @import
是如何解决这个问题的呢?正如Google的 @Rob Dodson 在他的文章《 A Guide to Web Components 》介绍在样式表中使用Polymer的声明来解决 XHR
的请求。
注意,通过 @import
引入的样式文件不能是域名的地址。比如这个示例,如果直接通过 @import
导入BootStrap官网提供的CDN地址, template
的布局样式仍然无效。
另外一个问题,循环克隆模板中的内容,造成样式表越来越多,几乎增加了四倍,但我们实际上只需要一个就够:
可以通过改变循环的过程:每次循环的时候,只循环定义了类名 .templateArticle
的 <article>
标签,并将其插入到 <section>
标签中。而在循环外,将 <style>
添加到 <section>
标签中,也就是 shadow host
。
需要改变的JavaScript从这里开始:
(function(){ ... root.appendChild(document.importNode(templateContent, true)); } })();
改变成:
(function(){ ... root.appendChild(document.importNode(templateContent.querySelector(".templateArticle"), true)); } root.appendChild(document.importNode(templateContent.querySelector("style"), true)); })();
现在在Shadow DOM中只有一个 <style>
,而且样式都是对的:
因为使用 appendChild()
将 <style>
添加到 <section>
中,所以 <style>
放在底部。如查要产生这样的代码,我将会尝试使用类似 jQuery.prepend()
方法将其移到顶部。
当然在这个项目中, <style>
放在底部并不会影响其工作。只不过我在学习模板和Shadow DOM制作,才想这样做,结构更清晰。如果想了解有关于 jQuery.prepend()
更多方面的知识,可以 点击这里 。
上面Rob Dodson文章的 链接 和HTML5 Rocks有一系列关于Web Components的 教程 。Rob Dodson的那篇文章详细介绍了Web Components,当然你可能会觉得这篇文章有点老。不过接下来我会一篇一篇的阅读HTML5 Rocks 上介绍Web Components的 文章 。
W3C上有一篇老文章叫作 Web Components的介绍 ( 中文译文 )。这是一年前的发布的一个工作草案,如果你阅读的话要记住,它是一篇老的规范草案,并没有更新。
的确如此,W3C也提到了最近在 Wiki上审阅Web Components 。并且有很多链接都指向HTML5 Rocks,还有在Github上也提供了 Shadow DOM ( 译文 ), Custom Elements ( 译文 )和 HTML Imports ( 译文 )规范说明。WHATWG上也提供了 模板规范 的正确版本。
规范可以详细阅读,但要找到一个好的方法去阅读。
最主要的是,微软公司已经发布了Web Components的特性得到支持和不支持具体时间。我假设,未来都会支持Web Components的特性。有关于详细的时间表可以浏览 modern.ie 状态页面。
通过polyfill来提出IE的问题是一个很好的建议。注意,目前Polymer是最受欢迎的Web Components的polyfill,不过其只支持IE10。有关于Polymer浏览器的兼容性可以 点击这里阅读 。
X-Tag虽然没有Polymer那么受欢迎,但它得到较多浏览器的支持,包括IE9。有关于X-Tag更多信息,可以阅读 X-Tag的官方文档 。
使用类似Polymer和X-Tag这样的Web Components库,就可以在工作中使用Web Components。所以在使用之前,最好先阅读他们的底层代码。
我不能说我的代码是完美的,但我完成了我定下的目标,能够解决我的问题,我所面临的问题是编码而不是阅读。我觉得能写出一个比以前更优秀的Web Components,就算是成功达到目标。
DEMO 下载源码
本文根据 @Kai Gittens 的《 Web Components Demo: Templates and (some) Shadow DOM 》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处: http://kaidez.com/web-components-demo/ 。
常用昵称“大漠”,W3CPlus创始人,目前就职于手淘。中国Drupal社区核心成员之一。对HTML5、CSS3和Sass等前端脚本语言有非常深入的认识和丰富的实践经验,尤其专注对CSS3的研究,是国内最早研究和使用CSS3技术的一批人。CSS3、Sass和Drupal中国布道者。2014年出版《 图解CSS3:核心技术与案例实战 》。