本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《
Shadow DOM
是指浏览器的一种能力,它允许在文档(
document
)渲染时插入一棵
DOM
元素子树,但是这棵子树不在主
DOM
树中。看一个简单的
slider
:
<input id="foo" type="range"/>
把这段代码放到
webkit
内核的浏览器中,它会这样显示:
很简单吧,这里有一个滑槽,还有一个滑块可以沿滑槽滑动。
嗯。一切看起来都那么美好,喝杯咖啡先……等下等下,这里居然有一个可以在
input
元素中滑动的元素!为什么我不能通过
JavaScript
看到它?
var slider = document.getElementsById("foo");
console.log(slider.firstChild); //
返回
null
2.2 这是一种魔法么?
我的观点来看,不是。这只是
shadow DOM
在起作用。你看,浏览器的开发者们已经意识到了手工编写这些
DOM
元素的表现和行为很困难而且很
SB
。所以,从一定程度上讲,他们骗了我们,给了我们一个输入框,但拥有比输入框更多的功能。
他们为你——
web
开发者设定了一个边界,界定了哪些是你可以访问的,哪些实现细节是访问不到的。然而,浏览器本身却可以随意跨越这个边界。设置这 样一个边界之后,它们就可以在你看不见的地方使用熟悉的
web
技术、同样的
HTML
元素去创建更多的功能,而不是像你一样要在页面上用
div
和
span
来 堆。
有一些很简单,就像上面说的
slider
。而有一些却相当复杂。我们来看一下
video
元素,它有一些按钮、进度条、
hover
态的音量控制,像这样:
所有的这一切都只是
HTML
和
CSS
——但是是隐藏在
shadow DOM
子树中的。
借用
XXX
的一首诗,“它是怎样工作的?”为了直观一些,我们假装可以用
JavaScript
操作它。看这个简单的页面:
<style> p { color: Green; } </style>
</head>
<p>My Future is so bright</p>
<div id="foo"></div>
<script>
var foo = document.getElementById('foo');
//
注意:这里只是模拟,不是真实的
API
foo.shadow = document.createElement('p');
foo.shadow.textContent = 'I gotta wear shades';
</script>
</body>
</html>
我们获得了一个这样的
DOM
树:
<p>My Future is so bright</p>
<div id="foo"></div>
但是它像是被这样渲染出来的:
<p>My Future is so bright</p>
<div id="foo"> <!-- shadow subtree begins -->
<p>I gotta wear shades</p>
</div> <!-- shadow subtree ends -->
看起来是这样:
注意一下,为什么渲染的句子的第二部分不是绿色的?这是因为文档(
document
)中选择器
p
不能获取到
shadown DOM
。很酷对不对?!如果一个框架开发者被赋予这样的能力会怎么样?想象一下你只需要写你的
widget
,而不用担心被不知哪里蹦出来的选择器愚弄…… 简直令人陶醉。
为了保持自然,
shadow DOM
子树中的事件可以在文档(
document
)中被监听。比如,你点击一下
audio
元素中的静音按钮,你可以在一个包裹它的
div
中监听到这个事件。
<div onclick="alert('who dat?')">
<audio controls src="test.wav"></audio>
但是,如果你要确认事件的来源,会发现它是
audio
元素,而不是它内部的按钮。
<div onclick="alert('fired by:' + event.target)">
<audio controls src="test.wav"></audio>
为什么这样?因为当事件穿过
shadown DOM
边界的时候,会被重新设定
target
,以避免暴露
shadow DOM
子树内部结构。用这种方式,你可以监听到从
shadow DOM
中产生的事件,而实现者也可以继续隐藏细节。
通过
CSS
访问(
Reaching into
)
Shadow
另一个需要提到的技巧是怎样通过
CSS
来访问
shadow DOM
子树。假设我想自定义我的
slider
。我想让它有一些样式,而不是系统原生的那样,像这样:
input[type=range].custom {
-webkit-appearance: none;
background-color: Red;
width: 200px;
结果如下:
很好,但是我怎样定义滑块的样式呢?我们已经知道,常规的
CSS
选择器并不能获取到
shadow DOM
子树。但事实上,这里有一些很方便的伪元素,可以取到
shadow DOM
子树中的元素。例如,
slider
中的滑块在
webkit
中可以这样访问:
input[type=range].custom::-webkit-slider-thumb {
-webkit-appearance: none;
background-color: Green;
opacity: 0.5;
width: 10px;
height: 40px;
样子如下:
很完美对不对?想想看,你可以为
shadow DOM
子树中的元素赋予样式,而不需要真的访问到这些元素。而这些
shadow DOM
的作者有了决定哪些部分可以被赋予样式的权利。如果你是作者,在做一些
UI widget toolkit
的时候,难道不想有这样的能力吗?
带有洞(
hole
)的
Shadow DOM
,无穷的想象力
讲完了这些令人惊叹的能力,我们想象一样,如果给一个有
shadown DOM
子树的元素插入子元素会怎样?我们来实验一下:
// Create an element with a shadow DOM subtree.
var input = document.body.appendChild(document.createElement('input'));
// Add a child to it.
var test = input.appendChild(document.createElement('p'));
// .. with some text.
test.textContent = 'Team Edward';
结果如下:
哇!欢迎来到
twilight DOM
的世界!它是文档(
document
)的一部分,可以被遍历到,但是不会渲染!它是不是很有用呢?不一定,但是如果你需要的话它确实就在那等你。
但是,如果我们真的有能力把元素的子元素放入
shadow DOM
子树中会怎么样?想象一下
shadow DOM
是一个模板,通过它的某个洞(
hole
)可以看到内部的子元素:
//
注意:这里只是模拟,不是真实的
API
var element = document.getElementById('element');
//
创建
shadow DOM
子树
element.shadow = document.createElement('div');
element.shadow.innerHTML = '<h1>Think of the Children</h1>' +
'<div class="children">{{children-go-here}}</div>';
// Now add some children.
var test = element.appendChild(document.createElement('p'));
test.textContent = 'I see the light!';
如果你去遍历
DOM
,你会看到这个:
<div id="element">
<p>I see the light</p>
但是像是这样渲染出来的:
<div id="element">
<div> <!-- shadow tree begins -->
<h1>Think of the Children</h1>
<div class="children"> <!-- shadow tree hole begins -->
<p>I see the light</p>
</div> <!-- shadow tree hole ends -->
</div> <!-- shadow tree ends -->
当你添加子元素的时候,从
DOM
树中看像一个正常的子元素,但是渲染的时候,他们从“洞(
hole
)”中进到了
shadow DOM
子树。
写到这里,你应该会承认,这真的很酷,也会问:
浏览器中什么时候才会有呢?