前言
需求
在编写本站的友链页面,实现可插入html的展示区时,遇到了一个问题:如果在里面写CSS,是可以覆盖全局样式的!
1
2
3
4
| a {
color: #8ec6ff;
text-decoration: underline;
}
|

这就很尴尬了。一刀切限制用户使用类型选择器不仅增加了开发成本还要人工审核,毕竟风格统一的页面匹配类型进行修改样式的操作还是挺常用的。那么有没有什么好方法能做到CSS只能在某个作用域下生效呢?当然是使用Shadow DOM啦。
使用传统的Shadow DOM
如果你在搜索引擎查找“Shadow DOM”,那么大概会搜到如下的使用方法:
(使用影子 DOM - Web API | MDN)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| <div class="shadow-host">
<template class="shadow-template">
<style>
/* 这里是样式 */
</style>
<!-- ... -->
</template>
</div>
<script>
(function attachShadowDOM(root) {
// 1. 找到所有的template
const templates = document.querySelectorAll('template.shadow-template');
templates.forEach(template => {
// 2. 创建Shadow DOM并挂载
const host = template.parentElement;
const shadow = host.attachShadow({
// 3. 设置模式 (open表示外部可以通过shadowRoot属性访问Shadow DOM,closed表示不可以)
mode: 'open'
});
// 4. 将template内容插入shadow root
shadow.innerHTML = template.innerHTML;
});
})(document);
</script>
|
运行一下,确实可以用。那就写点东西吧…
1
2
3
4
5
6
7
8
9
| <template class="shadow-template">
<style>
/* 这里是样式 */
</style>
<script>
alert("success!");
</script>
</template>
|
运行。呃,为什么没弹窗?按F12查看元素,发现确实是成功创建了Shadow DOM。

把Shadow DOM挂到body下也没用。好吧,看来是不能在Shadow DOM里面插入代码。那咋办呢?
使用声明式Shadow DOM
打开上面MDN的链接,把语言切换到英语,你会发现Creating a shadow DOM中多了一段Declaratively with HTML:
Creating a shadow DOM via JavaScript API might be a good option for client-side rendered applications. For other applications, a server-side rendered UI might have better performance and, therefore, a better user experience. In such cases, you can use the <template> element to declaratively define the shadow DOM. The key to this behavior is the enumerated shadowrootmode attribute, which can be set to either open or closed, the same values as the mode option of attachShadow() method.
― Using shadow DOM - Web APIs | MDN
好吧。。中文页面万年不更新。
根据文中所说的,我们把他改成声明式的试试看: 1
2
3
4
5
6
7
8
9
10
11
12
13
| <div class="shadow-host">
<template class="shadow-template" shadowrootmode="open">
<style>
/* 这里是样式 */
</style>
<script>
alert("success!");
</script>
</template>
</div>
<!-- 注意:attachShadowDOM方法已被移除 -->
|
打开网页…弹窗居然生效了?!没错,仅仅是改成声明式Shadow DOM就已经做到了执行script。至此,简介中的问题已经找到答案了。
兼容性
但是,写前端有一个脑子里一定要有的问题:这个功能能否在大部分浏览器工作?
根据Declarative Shadow DOM | Can I use…,声明式Shadow DOM的兼容性如下:
可以看到覆盖率还是挺高的。如果你的网站比较现代,完全不用考虑polyfill!
兼容Chrome 90-110
先来挑个软柿子捏吧。Can I use的Partial Support描述是Uses an older non-standard attribute called shadowroot instead of the standardized shadowrootmode attribute. 那我们直接给模板加上旧参数:
1
| <template class="shadow-template" shadowrootmode="open" shadowroot="open"></template>
|
打开Chrome 90测试一下,成功了。
兼容Chrome 53-89
再往下的版本就得回滚到Shadow DOM v1了。
可以看到,最低可以支持到Chrome 53。先把之前的代码还原: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| <script>
(function attachShadowDOM(root) {
// 1. 找到所有的template
const templates = document.querySelectorAll('template.shadow-template');
templates.forEach(template => {
// 2. 创建Shadow DOM并挂载
const host = template.parentElement;
const shadow = host.attachShadow({
// 3. 设置模式 (open表示外部可以通过shadowRoot属性访问Shadow DOM,closed表示不可以)
mode: 'open'
});
// 4. 将template内容插入shadow root
shadow.innerHTML = template.innerHTML;
});
})(document);
</script>
|
因为是polyfill,我们需要加入判断。只有在浏览器不支持shadowRootMode或shadowRoot属性的时候才运行代码:
1
2
3
4
5
| const supportsDeclarativeShadowDOM = HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode') || HTMLTemplateElement.prototype.hasOwnProperty('shadowRoot');
if (!supportsDeclarativeShadowDOM) {
console.log('Polyfilling Shadow DOM for friends cards');
...
}
|
现在,在Chrome 89及以下应该也能正常加载到DOM了。但是还有一个遗留问题我们没有解决:<script>块还是被忽略的。在解决这个问题之前,我们先看看脚本在成功加载的声明式Shadow DOM中是什么表现:
1
2
3
4
5
6
7
8
| <div id="test">Shadow DOM Content</div>
<div class="shadow-host">
<template shadowrootmode="open">
<script>
document.getElementById('test').style.color = 'red';
</script>
</template>
</div>
|
打开浏览器,可以看到:
Shadow DOM Content
这说明Shadow DOM里面拿到的document和外部是一样的,无需做任何封装。那就好办了,直接把里面的所有script都挂载到真实DOM:
1
2
3
4
5
6
7
8
9
10
11
| const scripts = shadow.querySelectorAll("script");
if (!scripts) {
return;
}
// To execute scripts inside non-declarative shadow DOM, we need to move them to light DOM
scripts.forEach(shadowScript => {
const script = document.createElement("script");
script.textContent = shadowScript.textContent;
template.parentElement.appendChild(script);
});
|
这里为什么不直接shadowScript.cloneNode(true)后挂载呢?因为我试了不行。尝试寻找答案无果,就先这样吧。
差点忘了,我们还需要支持外部script的加载和module、defer和async等参数。那就再加上几行:
1
2
3
4
5
6
7
| if (shadowScript.src) script.src = shadowScript.src;
if (shadowScript.integrity) script.integrity = shadowScript.integrity;
if (shadowScript.crossOrigin) script.crossOrigin = shadowScript.crossOrigin;
if (shadowScript.type) script.type = shadowScript.type;
if (shadowScript.defer) script.defer = shadowScript.defer;
if (shadowScript.async) script.async = shadowScript.defer;
|
测试一下,没有任何问题了!
完整代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
| <div class="shadow-host">
<template class="shadow-template" shadowrootmode="open" shadowroot="open">
<style>
/* 这里是样式 */
</style>
<script>
alert("success!");
</script>
</template>
</div>
<script>
(function attachShadowDOM(root) {
const supportsDeclarativeShadowDOM = HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode') || HTMLTemplateElement.prototype.hasOwnProperty('shadowRoot');
if (!supportsDeclarativeShadowDOM) {
console.log('Polyfilling Shadow DOM');
// 1. 找到所有的template
const templates = document.querySelectorAll('template.shadow-template');
templates.forEach(template => {
// 2. 创建Shadow DOM并挂载
const host = template.parentElement;
const shadow = host.attachShadow({
// 3. 设置模式 (open表示外部可以通过shadowRoot属性访问Shadow DOM,closed表示不可以)
mode: 'open'
});
// 4. 将template内容插入shadow root
shadow.innerHTML = template.innerHTML;
// 5. 为了了执行Shadow DOM内的脚本,我们需要将它们移动到真实 DOM
const scripts = shadow.querySelectorAll("script");
if (!scripts) {
return;
}
scripts.forEach(shadowScript => {
const script = document.createElement("script");
script.textContent = shadowScript.textContent;
// 支持外部脚本
if (shadowScript.src) script.src = shadowScript.src;
if (shadowScript.integrity) script.integrity = shadowScript.integrity;
if (shadowScript.crossOrigin) script.crossOrigin = shadowScript.crossOrigin;
// 支持module, defer和async
if (onloadElement.type) script.type = onloadElement.type;
if (onloadElement.defer) script.defer = true;
if (onloadElement.async) script.async = true;
template.parentElement.appendChild(script);
});
});
}
})(document);
</script>
|
剩下的工作?
实际上,到这一步还是不能实现需求。回想一下,我们需要在Shadow DOM内写JS,目的就是为了控制里面的元素对吧?然而我们
刚刚测试的时候只是控制了外面的元素。我们可以试着把<div id=“test”>放到Shadow DOM内,试一下还能不能实现:
1
2
3
4
5
6
7
8
| <div class="shadow-host">
<template shadowrootmode="open" shadowroot="open">
<div id="test">Shadow DOM Content</div>
<script>
document.getElementById('test').style.color = 'red';
</script>
</template>
</div>
|
测试一下:
Shadow DOM Content
哈哈,果然爆炸了。
打开F12,发现报错为Uncaught TypeError: Cannot read properties of null (reading 'style')。
可能你会说,改成document.querySelector('.shadow-host').shadowRoot.getElementById('test')不就好了?确实,你说得对,但是这样做十分麻烦!想象一下,当你编写一个组件,却需要不断查看它的父布局实现细节…这真的耦合过头了!我们还是希望能够提供一个API,使得组件能在内部直接访问到shadowRoot。但截至2025/12/11日笔者写下这段话时,还没有任何能做到在声明式Shadow DOM中直接获取shadowRoot的浏览器API。我们需要想办法了。
那么,这个问题就留到下一篇博客解决吧。