英文链接:Shadow DOM: The Basics, 27 AUGUST 2013 on Web Components, Shadow DOM
在我的上一篇博文里,我介绍了 Shadow DOM 并举例说明为啥它这么重要。今天,我们要干点苦力活——“码”上见分晓!在本文的最后,你将能够创建自己的封装组件——它封装了外部的内容,并重新整理内部结构来产生一个完全不同的东西。
让我们开始吧!
环境支持
为了能尝试下面的实例,建议使用 Chrome 33 或者更高版本的 Chrome。
一个简单的例子
让我们看一个非常简单的 HTML 文档。
<body> <div class="my-widget"> <h1>我的组件的标题</h1> <p>一些组件的内容</p> </div> <div class="my-other-widget"> <h1>我的另一个组件的标题</h1> <p>一些另一个组件的内容</p> </div> </body>
当 HTML 转化成 DOM,每个元素都会变成一个节点(node)。而一组互相嵌套的一组节点则被称为节点树(node tree)。
Shadow DOM 的独特之处在于它允许我们创建自己的节点树,这种节点树被称为影子树(shadow trees)。影子树对其中的内容进行了封装,有选择性的进行渲染。这就意味着我们可以插入文本、重新安排内容、添加样式等等。举个栗子:
<div class="widget">Hello, world!</div> <script> var host = document.querySelector(‘.widget‘); var root = host.createShadowRoot(); root.textContent = ‘我在你的 div 里!‘; </script>
通过上面的代码,我们已经通过一个影子树替换掉了我们 .widget
div 的文本内容。要创建一个影子树,我们首先要指定一个节点担任影子宿主(shadow host)。在这个例子里,我们将 .widget
当做我们的影子宿主。然后我们给影子宿主添加一个称作影子根(shadow root)的新节点。影子根作为影子树的第一个节点,其他的节点都是它的子节点。
如果你使用 Chrome 开发者工具检查这个元素,你会看到下面这样的结构:
看到 #shadow-root
是怎么被标灰显示的吗?这就是我们刚才创建的影子根。结果表明,影子宿主里面的内容没有被渲染,反而影子根里面的内容被渲染了出来。
这就是当实例运行时,我们看到 Im inside yr div!
而不是 Hello, world!
的原因。
让我们在一张图里可视化地查看一下整个过程:
为了更深入的了解 Shadow DOM,我们再举一个栗子:
又一个简单的例子
<body> <div class="widget">Hello, world!</div> <script> var host = document.querySelector(‘.widget‘); var root = host.createShadowRoot(); var header = document.createElement(‘h1‘); header.textContent = ‘一只野生的影子标题出现了!‘; var paragraph = document.createElement(‘p‘); paragraph.textContent = ‘一些影子文本也探头探脑滴冒了出来…‘; root.appendChild(header); root.appendChild(paragraph); </script> </body>
我沿用了之前的例子并给它添加了两个新元素,之所以这样做是为了说明 Shadow DOM 的操作与普通的DOM 的操作区别真的不大。你仍然可以使用 appendChild
和 insertBefore
来将子节点添加到父节点上,在 Chrome 的开发者工具里可以查看到这样的代码:
和之前一样,影子宿主 里面的内容 Hello world
并没有被渲染,取而代之展现的的是 影子根。
你可能会说:“这个我知道,但我如果想要渲染影子宿主里的内容,那该怎么玩?”
既然你诚心诚意的发问了,我就大发慈悲的告诉你——这绝对是 Shadow DOM 的一个杀手锏!请耐心读下去,我会向你展示它的神奇之处。
content 标签
在之前的两个例子里,我们用影子根里面的内容完全替换掉了影子宿主里面的内容。但这种奇技淫巧在实际开发中没什么用。真正有用的是我们可以从影子宿主中获取内容,并使用影子根中的结构将这些内容呈现。像这种将内容与实现分离的方式让我们可以更加灵活的处理页面的呈现。
想要引用影子宿主里面的内容,我们首先需要采用一个新的标签—— <content>
标签。这儿又是一个栗子:
<body> <div class="pokemon"> 胖丁 </div> <template class="pokemon-template"> <h1>一只野生的 <content></content> 出现了!</h1> </template> <script> var host = document.querySelector(‘.pokemon‘); var root = host.createShadowRoot(); var template = document.querySelector(‘.pokemon-template‘); root.appendChild(document.importNode(template.content, true)); </script> </body>
译者注:pokemon 就是口袋妖怪的意思。
使用 <content>
标签,我们创建了一个插入点(insertion point),其将 .pokemon
div 中的文本投射(projects) 出来,使之得以在我们的影子节点 <h1>
中展示。插入点十分强大,它允许我们在不改变源代码的情况下改变渲染顺序,这也意味着我们可以对要呈现的内容进行选择。
你可能已经注意到我们已经使用了一个模板标签 <template>
而不是整个用 JavaScript 来构建 shadow DOM。我发现使用 <template>
标签令 shadow DOM 的使用过程更加简单。
让我们看一个使用进阶的例子来证明如何使用多个插入点。
select 属性
<body> <div class="bio"> <span class="first-name">劳勃</span> <span class="last-name">多德森</span> <span class="city">旧金山</span> <span class="state">加利福尼亚州</span> <p>我专注前端开发(HTML/CSS/JavaScript),还会一点 Node 和 Ruby。</p> <p>从事写作,偶尔也会写点博客。</p> <p>尽管我是南方人,但我最近都在美丽的旧金山工作和生活。</p> </div> <template class="bio-template"> <dl> <dt>名字</dt> <dd><content select=".first-name"></content></dd> <dt>姓氏</dt> <dd><content select=".last-name"></content></dd> <dt>城市</dt> <dd><content select=".city"></content></dd> <dt>州</dt> <dd><content select=".state"></content></dd> </dl> <p><content select=""></content></p> </template> <script> var host = document.querySelector(‘.bio‘); var root = host.createShadowRoot(); var template = document.querySelector(‘.bio-template‘); root.appendChild(template.content); </script> </body>
在这个例子中我们创建了一个非常简单的简历组件。因为每个定义的字段都需要特定的内容,我们必须告诉 <content>
标签有选择性的插入内容。为了做到这一点,我们使用 select
属性。select
属性使用 CSS 选择器来选取想要展示的内容。
举例来说,<content select=".last-name">
会在影子宿主里寻找任何样式名称为 .last-name
的元素。如果找到一个匹配的元素,其就会将这个元素渲染到 shadow DOM 中对应的 <content>
标签中去。
改变顺序
通过插入点,我们不必修改 content 内容的结构而改变渲染的顺序。请记住,内容存在于影子宿主中,而呈现的方式存在于影子根也就是 shadow DOM 中。这有一个不错的示例,它展示了如何将姓氏一栏和名字一栏的渲染顺序进行交换。
<template class="bio-template"> <dl> <dt>姓氏</dt> <dd><content select=".last-name"></content></dd> <dt>名字</dt> <dd><content select=".first-name"></content></dd> <dt>城市</dt> <dd><content select=".city"></content></dd> <dt>州</dt> <dd><content select=".state"></content></dd> </dl> <p><content select=""></content></p> </template>
通过对我们的 template
模板进行简单的修改,我们就在不更改影子宿主内容的前提下对展示的效果进行了替换。为了更好的理解上面的内容,请看一下 Chrome 开发者工具的检查元素:
如图所示,.first-name
节点依然是影子宿主的第一个子节点,但是我们让它显示在 .last-name
节点之后了。我们通过改变插入点的顺序来完成了这一切,仔细回味一下你就会发现这个功能的强大之处。
贪心插入点(Greedy Insertion Points)
你可能已经注意到了,在 .bio-template
模板的最后,我们有一个 content
标签,他的 select
属性值为空。
<p><content select=""></content></p>
这种被称作通配符选择器(wildcard selection),其可以抓取影子宿主中所剩余的全部内容。以下三种选择器是完全相等的:
<content></content> <conent select=""></conent> <content select="*"></content>
我们来实验一把,将通配符选择器移到 template
模板的顶部:
<template class="bio-template"> <p><content select=""></content></p> <dl> <dt>Last Name</dt> <dd><content select=".last-name"></content></dd> <dt>First Name</dt> <dd><content select=".first-name"></content></dd> <dt>City</dt> <dd><content select=".city"></content></dd> <dt>State</dt> <dd><content select=".state"></content></dd> </dl> </template>
你会注意到所有的内容都被挪到 <p>
标签中去了,这完全的改变了我们组件的展示。这是因为这个选择器是贪心的,而且元素只能被选择一次。我们一旦把贪心选择器放在了模板的顶部,他就会将所有内容都抓取,不给其他 select
选择器留一点内容。
Dominic Cooney(@connsta)在他的博文 Shadow DOM 101 中对贪心选择器有很好的描述。文中他将选择器的原理比作舞会的邀请函:
<content>
元素是一封将文档(document)内容邀请去 Shadow DOM 渲染舞会的请柬。这些邀请按序发出;谁能收到请柬取决于请柬发往的地址(也就是select
属性)。对于内容元素来说,一旦收到请柬就会欣然接受并立即动身——谁会不接受这样一份盛大舞会请柬呢?如果接下来又有一份请柬发送到这一地址,额,不好意思,现在家里已经没人能去了。
掌握选择器和插入点的用法也是挺麻烦的,所以 Eric Bidelman(@ebidel)写了一个插入点可视化工具来帮助阐明这一概念。
他还录了一个视频以便解释这一概念:
结论
我还想多唠点,不过今天还是先收个尾吧。明天我们将深入讨论 CSS 样式封装,再往后我们还会讨论 JavaScript 和用户交互的内容。和往常一样,有问题的话可以到我的 twitter上艾特我或者给我留言~感谢阅读~