最好在Chrome 36+测试教程中的示例代码。同时打开开发者工具,将
Settings > General > Elements
中的Show user agent shadow DOM
选项选中。
最近将大部分时间花在了Web Components上面,不过这些花费的时间是有价值的。我整理了一个小组件,能更好的帮助大家更好的理解一个整体的Web Components。
Web Components主要由四个部分组成(模板、自定义元素、Shadow DOM和导入),但在这个案例中只关注其中的两个部分:模板(<template>
和Shadow DOM)。其中更主要的是模板。在写这篇文章的时候,浏览器对Web Components的支持度还不是很广,为了让浏览器能正常的渲染,需要使用Polymer和X-Tag的polyfill库。我想在内部工作中使用之前先仔细研究一下polyfill。
Web Components的简介
Web Components是一种新兴技术,用来规范组件的定制,这些都要非常感谢W3C组织。Web Components的目的就是允许开发人员使用HTML、CSS和JavaScript来自定义元素。这些元素可以被认为是一些小部件(widgets)。
一个很好的示例就是自定义元素<github-card>
。如果你有一个GitHub账号,你可以打开<github-card>
示例页面,在输入框中输入你的GitHub的用户名,可以看到你的GitHub相关信息。然后你可以到<github-card>
文档下载相应的源码,查看如何使用这样的一个标签元素。
Web Components主要组成:
- 模板(
<template>
标签):定义的标记块,不会被渲染但可以随后被激活使用。阅读更多的细节... - Shadow DOM:封装的DOM子树,更可靠的用户界面元素组成。最好是把它想成DOM中的DOM。 阅读更多的细节...
- Custom Elements (自定义元素):让用户自定义新的标签名和新的脚本接口。例如
<github-card>
。阅读更多的细节... - HTML Imports(导入):可以通过
<link>
标签,把一小块的HTML代码加载到页面中。阅读更多的细节...
在W3C规范文档中,Web Components除了上述的四个部分还有一个Decorators,基于CSS选择器来应用模板,从而对文档进行丰富的视觉和行为的变化。更多的详细信息可以点击这里阅读。但很多开发人员不喜欢它,所以没有很多人去敲定其规范,有可能将来会消失。
比如<github-card>
具有Web Components所有功能部分,每个都可能很好的使用。但当它们一起工作的时候,就组成了一个Web Components。从概念上讲,它有点类似于AJAX,组合在一起执行一个任务。
专注模板(一切从模板开始)
我读过Web Components中所有部分(包括"decorator")的介绍和写过一点代码,但给我的感觉是,学习Web Components最好的方法不是阅读而是动手去写。所以,针对Web Components这几个组成部分,我们从模板开始着手。
对于模板,我想展示一个基于JavaScript数据对象创建的简单的书籍列表。事情就是这样开始的...
HTML
<!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 (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;
}
JavaScript(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,就算是成功达到目标。
本文根据@Kai Gittens的《Web Components Demo: Templates and (some) Shadow DOM》所译,整个译文带有我们自己的理解与思想,如果译得不好或有不对之处还请同行朋友指点。如需转载此译文,需注明英文出处:http://kaidez.com/web-components-demo/。
如需转载,烦请注明出处:http://www.w3cplus.com/web-components/web-components-demo-template-and-some-shadow-dom.html