Web Components Demo: Templates 和 Shadow DOM

来源:转载

最好在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的简介

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,就算是成功达到目标。

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:核心技术与案例实战 》。



分享给朋友:
您可能感兴趣的文章:
随机阅读: