Web Components Demo: Templates 和 Shadow DOM

最好在Chrome 36+测试教程中的示例代码。同时打开开发者工具,将Settings > General > Elements中的Show user agent shadow DOM选项选中。

Web Components

最近将大部分时间花在了Web Components上面,不过这些花费的时间是有价值的。我整理了一个小组件,能更好的帮助大家更好的理解一个整体的Web Components。

DEMO下载源码

Web Components主要由四个部分组成(模板、自定义元素、Shadow DOM和导入),但在这个案例中只关注其中的两个部分:模板(<template>和Shadow DOM)。其中更主要的是模板。在写这篇文章的时候,浏览器对Web Components的支持度还不是很广,为了让浏览器能正常的渲染,需要使用PolymerX-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

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.cssTwitter Bootstrap的样式文件bootstrap.css。Bootstrap提供了响应式布局功能,这里引入主要是为了让页面布局看上去好看一些。另外引入style.css文件,这个文件主要是对页面一些元素的样式做了定义,在整页案例中他是一个小角色。

HTML和过去一样,不同的是给Web Components中心部分template标签添加了一个IDsingleBook。把HTML代码和CSS样式以<style>放在了<template>里面。

<template>中有一个<article>标签:有关于书的数据将解析到这里面。因为模板是惰性的,这意味着如果不和外面通信,那么页面加载这部分是不可见的。

注意,<article>里面部分是空的:

  • 两个<span>标签

  • <img>标签中的srcalt属性

  • <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书特定的信息。每个列表都包含了titleauthorimageamazonLink属性。

var template = document.querySelector("#singleBook"),
    templateContent = template.content,
    host = document.querySelector("#allBooks"),
    root = host.createShadowRoot();

开始创建一个Shadow DOM。我通过var创建了四个变量。

  • template直接引用了要渲染的<template>,直接引用它的IDsingleBook

  • 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)如下所示:

Web Components

但是有一个问题,Bootstrap样式用于<template>模板中某些元素的样式被忽略了。任何包含panelbtn类名的元素应该会引用Bootstrap的样式,尤其是按钮…

Web Components

这里发生的一切,正如前面所说的模板内的代码不能和模板外的代码做任何的交流。从技术上说<template>在Shadow DOM,它是一个naturally-encapsulated。所以页面中三个样式文件(normalize.min.cssbootstrap.min.css 和styles.css)在模板的布局中都没生效。现在使用<link>将样式添加到Shadow DOM中是不允许的。

导入样式文件

style.css文件与模板布局无关,但其它两个样式文件有关系。解决方案就是通过