shadow DOM是什么
<video controls autoplay name="media">
<source id="mp4" src="trailer.mp4" type="video/mp4">
</video>
上面這是最簡單的視屏標(biāo)簽,里面有默認的音量等按鍵。在源代碼中根本沒有一點痕跡。那這些節(jié)點是從哪里來的?
??這就是shadow DOM,視屏的控件在瀏覽器中真實面目如下:

發(fā)現(xiàn)#shadow-root是灰色的,這是瀏覽器為了表明在shadow DOM中,代表頁面其他的部分的內(nèi)容不會對內(nèi)部產(chǎn)生影響(可用特定方式穿透,后文會說到)
內(nèi)容具體指css選擇器和javascript代碼
簡而言之,Shadow DOM 是一個 HTML 的新規(guī)范,其允許開發(fā)者封裝HTML組件(類似vue組件,將html,css,js獨立部分提?。?。
為什么要使用shadow DOM
Bootstrap的名字你一定不陌生,代碼一般如下:
<ul class="media-list">
<li class="media">
<div class="media-left">
<a href="#">

</a>
</div>
<div class="media-body">
<h4 class="media-heading">Media heading</h4>
</div>
</li>
</ul>
非常的簡單好用,但是這些東西你并沒有深入了解,往往結(jié)構(gòu)變復(fù)雜之后,都是一堆模板,修改是一個很難的問題,牽一發(fā)而動全身。
??這種情況下shadow DOM的優(yōu)勢十分巨大,你可以這么寫模板
// 我是一個簡潔的模板
<bootstrap-media-list>
<a href="#">

</a>
<h4 class="media-heading">Media heading</h4>
</bootstrap-media-list>
當(dāng)然想實現(xiàn)這么寫還需要一些js,css配合才行
如何使用
先運行一個例子
<div class="widget">Hello, world!</div>
<script>
var host = document.querySelector('.widget');
var root = host.createShadowRoot();
root.textContent = '我在你的 div 里!';
</script>

??首先我們指定一個宿主節(jié)點(shadow host)然后創(chuàng)建影子根(shadow root)為它添加一個文本節(jié)點,結(jié)果宿主中的內(nèi)容未被渲染。
如何渲染宿主節(jié)點中的內(nèi)容
只渲染影子根中的內(nèi)容基本沒有實用的地方,但能自由的渲染宿主節(jié)點中的內(nèi)容的話就可以讓頁面展現(xiàn)更靈活。我們需要content標(biāo)簽
<div class="pokemon">胖丁</div>
<template class="pokemon-template">
<h1>一只野生的<content></content>出現(xiàn)了!</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>

??
<content>標(biāo)簽創(chuàng)建了一個**插入點 **將.pokemon里面的文本投影出來,多個內(nèi)容匹配時可以實用select屬性指定
<div class="host">
<p>大慈大悲,由諸葛亮進化而來。</p>
<span class="name">觀音姐姐獸</span>
</div>
<template class="root-template">
<dl>
<dt>名字</dt>
<dd><content select=".name"></content></dd>
</dl>
<p><content select=""></content></p>
</template>
<script>
var host = document.querySelector('.host');
var root = host.createShadowRoot();
var template = document.querySelector('.root-template');
root.appendChild(template.content);
</script>

??可以使用
select屬性類似選擇器的形式渲染宿主節(jié)點中匹配元素的投影。這種形式不但可以改變DOM流的順序也可以讓布局變得靈活。??在模板的最后
<content select=""></content>是一種貪心匹配,把宿主節(jié)點中所有未被匹配的內(nèi)容全部投影。需要注意的是把貪心匹配放在最前面會把所有的節(jié)點投影并且之后的select不會再獲取到被其投影的內(nèi)容。??以下都是等效的:
<content></content><conent select=""></conent><content select="*"></content>
樣式渲染與封裝
先看一個簡單的例子
<style>
button {
font-size: 18px;
font-family: '華文行楷';
}
</style>
<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>影子按鈕</button>';
</script>

??在影子節(jié)點中存在邊界使
shadow DOM樣式和正常DOM流中的樣式不相互干擾。這是一種作用域化的體現(xiàn),不用再擔(dān)心樣式的相互沖突。
(:host)選擇器
:host是偽類選擇器選擇宿主節(jié)點,我們可以擴展一下上面的例子
<style>
p {
font-size: 12px;
}
</style>
<p>我的文本</p>
<button>我的按鈕</button>
<template class="shadow-template">
<style>
:host(p) {
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('button').createShadowRoot();
var template = document.querySelector('.shadow-template');
root1.appendChild(document.importNode(template.content, true));
root2.appendChild(document.importNode(template.content, true));
</script>

??這個例子有幾個點:
- p標(biāo)簽字體大小是12px = 影子樣式的優(yōu)先級不如頁面樣式
-
:host選擇器中可以使用任意合法選擇器,*應(yīng)用于所有 - 通過掛載不同宿主渲染出不同的內(nèi)容,可以實現(xiàn)主題化
??上面的主題化并不完全,只根據(jù)掛載元素進行選擇也就是說.parent > .child,但是我們還能通過:host-context實現(xiàn).parent < .child如下
<div class="serious">
<p class="serious-widget">
serious-widget
</p>
</div>
<div class="playful">
<p class="playful-widget">
playful-widget
</p>
</div>
<template class="widget-template">
<style>
:host-context(.serious) {
width: 250px;
height: 50px;
background: tomato;
}
:host-context(.playful) {
width: 250px;
height: 50px;
background: deepskyblue;
}
</style>
<content></content>
</template>
<script>
var root1 = document.querySelector('.serious-widget').createShadowRoot();
var root2 = document.querySelector('.playful-widget').createShadowRoot();
var template = document.querySelector('.widget-template');
root1.appendChild(document.importNode(template.content, true));
root2.appendChild(document.importNode(template.content, true));
</script>

上面的效果就非常不錯了,可以進行動態(tài)組件構(gòu)建
ps: 偽類,偽元素選擇器也可以直接使用,效果和正常節(jié)點中一致
(::content)選擇器
在使用 shadow DOM 的時候應(yīng)該確保內(nèi)容和表現(xiàn)的分離,也就是說文本應(yīng)該來自頁面而不是埋在 shadow DOM 的模板里。所以我們需要在模板中對分布式節(jié)點進行渲染。
<div class="widget">
<button>分布節(jié)點碉堡啦!</button>
</div>
<template class="widget-template">
<style>
::content > button {
color: white;
background: tomato;
border-radius: 10px;
border: none;
padding: 10px;
}
</style>
<content select=""></content>
</template>
<script>
var root = document.querySelector('.widget').createShadowRoot();
var template = document.querySelector('.widget-template');
root.appendChild(document.importNode(template.content, true));
</script>

打破作用域(::shadow)
我們可以在掛載節(jié)點中使用::shadow,比如
<style>
.sign-up::shadow #username{
font-size: 20px;
border: 1px solid red;
}
</style>
<div class="sign-up"></div>
<template class="sign-up-template">
<style>
#username{
font-size: 12px;
}
</style>
<div>
<input type="text" id="username" placeholder="用戶名">
</div>
</template>
<script>
var root = document.querySelector('.sign-up').createShadowRoot();
var template = document.querySelector('.sign-up-template');
root.appendChild(document.importNode(template.content, true));
</script>

不過缺點是只能穿透一層,但我們還有一個神器!
多層穿透(/deep/)
<style>
#foo /deep/ button {
color: red;
}
</style>
<div id="foo"></div>
<template>
<div id="bar"></div>
</template>
<script>
var root1 = document.querySelector('#foo').createShadowRoot();
var template = document.querySelector('template');
root1.appendChild(document.importNode(template.content, true));
var root2 = root1.querySelector('#bar').createShadowRoot();
root2.innerHTML = '<button>點我點我</button>';
</script>

javascript的區(qū)別
- 數(shù)據(jù)并沒有塊級化,仍掛載在
window上
- 事件重定向(原來綁定在 shadow DOM 節(jié)點中的事件被重定向了,所以他們看起來像綁定在宿主節(jié)點上一樣)
<input id="normal-text" type="text" value="I'm normal text">
<div id="host"></div>
<template>
<input id="shadow-text" type="text" value="I'm shadow text">
</template>
<script>
var root = document.querySelector('#host').createShadowRoot();
var template = document.querySelector('template');
root.appendChild(document.importNode(template.content, true));
document.addEventListener('click', function(e) {
console.log(e.target.id + ' clicked!');
});
</script>

可以看到在影子節(jié)點的事件被宿主節(jié)點代理。
事件阻塞
在監(jiān)聽以下事件時會被阻塞在影子節(jié)點的根:
aborterrorselectchangeloadresetresetresizescrollselectstar
<input id="normal-text" type="text" value="I'm normal text">
<div id="host">
<input id="distributed-text" type="text" value="I'm distributed text">
</div>
<template>
<div><content></content></div>
<div>
<input id="shadow-text" type="text" value="I'm shadow text">
</div>
</template>
<script>
var root = document.querySelector('#host').createShadowRoot();
var template = document.querySelector('template');
root.appendChild(document.importNode(template.content, true));
document.addEventListener('select', function(e) {
console.log(e.target.id + ' text selected!');
});
</script>

事件影子節(jié)點的根上被阻止,無法冒泡到ducoment,所以無法監(jiān)聽。
分布節(jié)點
分布節(jié)點指之前通過<content>標(biāo)簽將宿主節(jié)點的內(nèi)容投影,分布節(jié)點不會發(fā)生上面的阻塞情況,因為這個只是一個投影實際的內(nèi)容還是掛載在宿主節(jié)點上。