OK ~ ~ ~
什么是Shadow DOM
很合适的一个例子就是<video>标签,看下面的例子:
<video controls autoplay name="media">
<source id="mp4" src="" type="video/mp4">
</video>
video标签包裹着source标签,但查看页面显示时会发现并没有那么简单,
视频播放器本身有播放/暂停按钮、进度条、视频时间显示、音量控制以及播放时的一个全屏切换按钮。那这些东西都是哪里来的?
开启chrome中shadow DOM
实际上,各浏览器内置了播放器组件代码。打开chrome开发者工具,进入setting:
在preference的Elements之后,选中“show user agent shadow DOM”
这时候再回到DOM结构来看一下:
看到标灰的#shadow-root了吗?这里就是所有视频播放器控制组件的所在之处。浏览器之所以将其置灰,是为了表明这部分是在 shadow DOM里,对于页面的其他部分来说它是不可用的。这里的“不可用”意味着你写的CSS选择器和 JavaScript 代码都不会影响到这部分内容。实际上,让 <video> 标签的 UI 能够运行的标签和样式都被浏览器封装了。
再回到最初的问题上,什么是Shadow DOM?
简而言之,Shadow DOM 是一个 HTML 的新规范,其允许开发者封装自己的 HTML 标签、CSS 样式和 JavaScript 代码。这使得开发人员可以创建诸如 <video> 这样自定义的一级标签。总的来说,这些新标签和相关的 API 被称为 Web Components 。
Shadow DOM 基础
在学习Shadow DOM 基础之前,得了解一些基本的概念。
我们编写的HTML代码在浏览器中会转化成DOM,每个元素是一个节点,一段完整的HTML就形成了节点数。Shadow DOM 的独特之处在于它允许我们创建自己的节点树,这种节点树被称为 寄生树(shadow trees) 。寄生树对其中的内容进行了封装,有选择性的进行渲染。这就意味着我们可以插入文本、重新安排内容、添加样式等等。举个栗子:
<div class="hello">Hello World</div>
<script>
var host = document.querySelector('.hello');
var root = host.createShadowRoot();
root.textContent = '你好世界';
</script>
打开开发者工具看一下:
以上代码最终显示的并不是标签中写的 “Hello World”,这个文本内容被我们通过一个寄生树替换掉了。
创建一个Shadow DOM
要创建一个寄生树,我们首先要指定一个节点担任 宿主(shadow host) 。在这个例子里,我们将 .widget
当做我们的宿主。然后我们给宿主添加一个称作 寄生根(shadow root) 的新节点。寄生根 作为 寄生树 的第一个节点,其他的节点都是它的子节点。
再来一个例子:
<div class="hello">Hello World</div>
<script>
var host = document.querySelector('.hello');
var root = host.createShadowRoot();
var header = document.createElement('h1');
header.textContent = 'A title of HelloWorld';
var paragrah = document.createElement('p');
paragrah.textContent = 'A paragrah of helloworld';
root.appendChild(header);
root.appendChild(paragrah);
</script>
在原来代码上新增两个元素,来看看结果:
Shadow DOM 的操作与普通的DOM 的操作区别不大,仍然可以使用 appendChild
和 insertBefore
来将子节点添加到父节点上。
这里细心的会发现,原生DOM节点div 标签中的文本还是没有渲染出来,那如果想要这里的文本渲染出来要怎么做呢?
显示原文本内容
使用新标签 <content>,可以从宿主中获取 内容(content) ,并使用寄生根中的结构将这些内容 呈现 。像这种将内容与实现分离的方式让我们可以更加灵活的处理页面的呈现。
再来一个例子:
<div class="pokemon">pikaqiu</div>
<template class="wild-pokemon">
<h1>A wild <content></content> appeared!</h1>
</template>
<script>
var host = document.querySelector('.pokemon');
var root = host.createShadowRoot();
var template = document.querySelector('.wild-pokemon');
root.appendChild(document.importNode(template.content, true));
</script>
结果如下:
使用 <content>
标签,我们创建了一个 插入点(insertion point) ,其将类名为 pokemon
的 div 中的文本 投射 出来,使之能够在我们的寄生节点 <h1>
中展示。插入点十分强大,它允许我们在不改变源代码的情况下改变渲染顺序,这也意味着我们可以对要呈现的内容进行选择。
内容选择
既然说到了内容选择,那就来一个例子:
<div class="info">
<span class="first-name">王</span>
<span class="last-name">小二</span>
<span class="city">苏州</span>
<span class="identity">学生</span>
<p>我叫王小二,王是王小二的二,二是二百五的五</p>
</div>
<template class="info-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=".identity"></content></dd>
</dl>
<p><content select=""></content></p>
</template>
<script>
var host = document.querySelector('.info');
var root = host.createShadowRoot();
var template = document.querySelector('.info-template');
root.appendChild(template.content);
</script>
在这个例子中我们创建了一个非常简单的简历组件。因为每个定义的字段都需要特定的内容,我们必须告诉 <content>
标签有选择性的插入内容。为了做到这一点,我们使用 select
属性。 select
属性使用 CSS 选择器来选取想要展示的内容。
譬如说, <content select=".last-name">
会在宿主里寻找任何样式名称为 .last-name
的元素。如果找到一个匹配的元素,其就会将这个元素渲染到 shadow DOM 中对应的 <content>
标签中去。当然,我们可以发现,最终的结果是按照 template 中代码顺序展示的,那么如果我们想要改变渲染的顺序,就可以在不动宿主内容的前提下对展示的效果进行了改变。
如果你实践了上面的代码,你就发现页面显示的非常简单甚至丑陋,因为我们并没有添加任何样式,那如何在 Shadow DOM 上使用 CSS 样式呢?
shadow DOM 样式
这里要介绍新的概念, 影子边界(shadow boundary) 以及 影子宿主(shadow hosts)。
所谓影子边界,就是分离 常规 DOM 与 shadow DOM 的壁障。影子边界的主要好处就是防止 常规 DOM 中的样式泄露到 shadow DOM 中。这就意味着即使你在主文档中有一个针对全部 <h3>
标签的样式选择器,这个样式也不会不经允许的影响到 shadow DOM 的元素。
影子边界
如下例:
<style>
button {
font-size: 18px;
font-family: cursive;
}
</style>
<button>Regular button</button>
<div></div>
<script>
var host = document.querySelector('div');
var root = host.createShadowRoot();
root.innerHTML = '<style>button { font-size: 24px; color: blue; } </style>' + '<button>Shadow button</button>'
</script>
写了两个按钮,一个是普通的 DOM,另一个是 shadow DOM。<style>
标签规定所有的 button
都要用花体字以及 18px 的字号。由于影子边界的存在,第二个按钮忽略掉这个样式标签并使用自己的样式。由于我们没有重写 font-family
属性,所以它使用了浏览器默认的 sans serif 字体来实现。同时,影子边界也保护主文档不受 shadow DOM 样式的侵袭。你可能注意到影子按钮有一个蓝色的 color
属性,但是原文档中的按钮还是保持了它默认的显示样式。
影子宿主
影子边界大概就这样,下面讲一下影子宿主怎么加样式。
<style>
.widget {
text-align: center;
}
</style>
<div class="widget">
<p>Hello World!</p>
</div>
<script>
var host = document.querySelector('.widget');
var root = host.createShadowRoot();
root.innerHTML = `<style>
:host{
border: 2px dashed red;
text-align: left;
font-size: 28px;
}
</style>
<content></content>`
</script>
结果是给组件添加一个红色边框,这看似没啥,但其中可是发生了很多有趣的事情(有趣个屁哟)。
:host
伪类选择器
首先,应用于 :host
的样式是继承自 shadow DOM 里的元素的。所以我们的 <p>
标签里的字体大小有 28px。同时注意到,页面上的样式可以设置 :host
中的 text-align
的文本对齐方式为居中。 :host
的选择器的优先级被设定为低于页面选择器的优先级,所以如果有需要的话它可以轻松的被页面重写样式。在这个例子里页面样式 .widget
击败了影子样式 :host
。
总的来说,:host
是一个伪类选择器,它匹配<icon-toggle>
s shadow DOM 的“host”元素,即<icon-toggle>
元素本身。由于 :host
是 伪类选择器 ,我们可以将其应用于多个标签上来改变我们组件的外观。例如:
<p>My Paragraph</p>
<div>My Div</div>
<button>My Button</button>
<template class="shadow-template">
<style>
:host(p) {
color: blue;
}
:host(div) {
color: green;
}
:host(button) {
color: red;
}
:host(*) {
font-size: 24px;
}
</style>
<content select=""></content>
</template>
<script>
var root1 = document.querySelector('p').createShadowRoot();
var root2 = document.querySelector('div').createShadowRoot();
var root3 = document.querySelector('button').createShadowRoot();
var template = document.querySelector('.shadow-template');
root1.appendChild(document.importNode(template.content, true));
root2.appendChild(document.importNode(template.content, true));
root3.appendChild(document.importNode(template.content, true));
</script>
从上面代码中可以看到,我们可以利用 :host
选择器来改变我们组件的某一个特定标签样式。我们还可以根据类名、ID、属性等等来进行匹配选择——任何有效的 CSS 选择器都可以正常工作。
比方说,如果你想写一个自适应的组件,你可以在 :host
中写各种诸如 .widget-fixed
、 .widget-flex
、 .widget-fluid
的样式,或者在表单元素的 :host
中写 .valid
和 .error
样式。
通过使用 *
选择器,我们可以创造应用于全部 :host
元素的默认的样式,正如在这个例子中我们设置所有组件的 font-size
为 24px。通过这一方式你可以构建组件的基本外观,然后在通过不同方式的选择器给你的组件增光添彩。