怎样用JavaScript使用ShadowDOM?
Shadow DOM(影子 DOM)是 Web Components 规范的核心技术之一,它允许开发者将 DOM 树封装在自定义元素内部,从而实现样式隔离、DOM 封装和局部作用域。通过 Shadow DOM,你可以在不影响外部全局样式的情况下构建可复用的组件。本文将从基础概念出发,结合 JavaScript 示例,逐步讲解如何创建、使用和操作 Shadow DOM。
Shadow DOM 的基本概念
在深入代码之前,先了解几个关键术语:
- Shadow Host:挂载 Shadow DOM 的普通 DOM 元素,通常是一个自定义元素或任意 HTML 元素(如 <div>、<span>)。
- Shadow Root:Shadow DOM 的根节点,是一个特殊的文档片段(DocumentFragment),附着在 Shadow Host 上。
- Shadow Tree:以 Shadow Root 为根的 DOM 子树,其内容与主 DOM 树隔离。
- 模式(Mode):创建 Shadow Root 时指定的
open或closed模式,决定外部能否通过element.shadowRoot访问内部结构。
创建 Shadow DOM
使用 JavaScript 创建 Shadow DOM 非常简单,只需调用宿主元素的 attachShadow() 方法,并传入一个配置对象,指定 mode 属性。下面的示例演示如何为一个 <div> 元素附加一个开放模式的 Shadow Root,并在其中插入一段简单的文本。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Shadow DOM 示例</title>
</head>
<body>
<div id="myHost"></div>
<script>
// 获取宿主元素
const host = document.getElementById('myHost');
// 创建 Shadow Root,模式为 open
const shadowRoot = host.attachShadow({ mode: 'open' });
// 在 Shadow DOM 中插入内容
shadowRoot.innerHTML = '<p style="color:red;font-weight:bold;">这是 Shadow DOM 中的文本</p>';
// 可以通过 shadowRoot 属性访问内部
console.log(host.shadowRoot); // 输出 Shadow Root 对象
</script>
</body>
</html>在上面的例子中,attachShadow({ mode: 'open' }) 返回一个 Shadow Root 对象。通过设置 innerHTML 属性,我们向 Shadow Tree 中添加了一个 <p> 元素。注意,外部样式(如页面中的全局样式)不会影响到 Shadow DOM 内部的元素,反之亦然。
样式封装与 :host 伪类
Shadow DOM 的一大优势是样式隔离。你可以在 Shadow Root 内添加 <style> 标签,这些样式只作用于 Shadow Tree 中的元素。此外,使用 :host 伪类可以设置宿主元素本身的样式。下面示例创建了一个带有阴影边框的自定义按钮卡片。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>样式封装示例</title>
<style>
/* 全局样式不影响 Shadow DOM */
.card { border: 2px solid blue; }
</style>
</head>
<body>
<div class="card" id="cardHost">外部卡片</div>
<script>
const host = document.getElementById('cardHost');
const shadow = host.attachShadow({ mode: 'open' });
// 在 Shadow DOM 中添加样式和结构
shadow.innerHTML = `
<style>
/* :host 设置宿主元素本身的样式 */
:host {
display: inline-block;
padding: 10px;
background: #f0f0f0;
border: 2px dashed green;
}
.inner {
color: red;
}
</style>
<div class="inner">这是 Shadow DOM 内部的内容</div>
`;
</script>
</body>
</html>运行后你会发现,外部定义的蓝色边框样式并没有覆盖 Shadow DOM 内部的绿色虚线边框。因为 :host 定义的样式拥有更高优先级(实际上等同于宿主元素的最高特异性),而外部全局样式根本无法穿透 Shadow 边界。
使用模板(<template>)和插槽(<slot>)
在复杂组件中,通常使用 <template> 元素来定义 Shadow DOM 的结构,并通过 <slot> 实现内容投影(Content Projection),允许外部向组件的特定位置插入内容。插槽使组件更具灵活性和可组合性。
下面的例子创建一个自定义的“alert-box”组件,它包含一个标题插槽和一个内容插槽。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>模板与插槽示例</title>
</head>
<body>
<!-- 使用自定义组件 -->
<alert-box>
<span slot="title">注意</span>
<p>这是重要的提示信息。</p>
</alert-box>
<template id="alertTemplate">
<style>
.alert {
border: 1px solid #ffc107;
background: #fff3cd;
padding: 10px;
border-radius: 4px;
}
.title {
font-weight: bold;
margin-bottom: 5px;
}
</style>
<div class="alert">
<div class="title">
<!-- 具名插槽:名为 title -->
<slot name="title">默认标题</slot>
</div>
<slot></slot> <!-- 默认插槽 -->
</div>
</template>
<script>
class AlertBox extends HTMLElement {
constructor() {
super();
// 获取模板内容
const template = document.getElementById('alertTemplate');
const content = template.content.cloneNode(true);
// 附加 Shadow DOM 并插入模板
this.attachShadow({ mode: 'open' }).appendChild(content);
}
}
// 注册自定义元素
customElements.define('alert-box', AlertBox);
</script>
</body>
</html>在这个示例中,外部元素通过 slot="title" 属性指定内容应投射到名称为 “title” 的 <slot> 中,而普通的 <p> 元素则进入默认插槽。<slot> 元素本身在渲染时会被替换成用户提供的内容,未提供内容时则显示插槽的默认文本(如“默认标题”)。
事件处理与事件重定向
Shadow DOM 中的事件处理需要特别注意:默认情况下,Shadow Tree 内部发生的事件会向外冒泡,但事件目标(event.target)会被重定向为 Shadow Host,以保持封装性。如果你想获取真正的内部元素,可以通过 event.composedPath() 方法获取事件传播路径,或者设置事件为 composed: true 来穿透 Shadow 边界。
下面的例子展示了点击内部按钮时如何获取事件信息。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>事件重定向示例</title>
</head>
<body>
<div id="host"></div>
<script>
const host = document.getElementById('host');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = `<button id="innerBtn">点我</button>`;
// 监听宿主上的点击事件(事件冒泡到宿主)
host.addEventListener('click', (event) => {
console.log('事件目标 (重定向后):', event.target); // 输出 host 元素
console.log('实际点击的元素 (composedPath):', event.composedPath()[0]); // 输出 button#innerBtn
});
</script>
</body>
</html>对于需要穿透 Shadow DOM 的自定义事件,可以在 new CustomEvent() 中设置 composed: true。
高级用法:结合自定义元素创建完整组件
更常见的实践是将 Shadow DOM 与自定义元素(Custom Elements)结合,构建功能完备的 Web 组件。下面的代码实现了一个简单的星级评分组件,内部使用 Shadow DOM 封装结构和样式,并暴露 value 属性供外部使用。
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>星级评分组件</title>
</head>
<body>
<star-rating value="3"></star-rating>
<script>
class StarRating extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
// 定义模板
const template = document.createElement('template');
template.innerHTML = `
<style>
:host { display: inline-block; cursor: pointer; }
.star { font-size: 2rem; color: #ccc; transition: color 0.2s; }
.star.active { color: gold; }
</style>
<div id="stars">
<span class="star" data-index="1">★</span>
<span class="star" data-index="2">★</span>
<span class="star" data-index="3">★</span>
<span class="star" data-index="4">★</span>
<span class="star" data-index="5">★</span>
</div>
`;
shadow.appendChild(template.content.cloneNode(true));
// 初始渲染
this._updateStars(parseInt(this.getAttribute('value')) || 0);
}
// 观察 value 属性变化
static get observedAttributes() { return ['value']; }
attributeChangedCallback(name, oldVal, newVal) {
if (name === 'value') {
this._updateStars(parseInt(newVal) || 0);
}
}
// 更新星星高亮
_updateStars(value) {
const stars = this.shadowRoot.querySelectorAll('.star');
stars.forEach(star => {
const index = parseInt(star.dataset.index);
star.classList.toggle('active', index <= value);
});
}
// 设置点击事件
connectedCallback() {
this.shadowRoot.addEventListener('click', (e) => {
const star = e.target.closest('.star');
if (star) {
const newVal = parseInt(star.dataset.index);
this.setAttribute('value', newVal);
// 触发自定义事件
this.dispatchEvent(new CustomEvent('rating-change', {
detail: { value: newVal },
bubbles: true,
composed: true
}));
}
});
}
}
customElements.define('star-rating', StarRating);
</script>
</body>
</html>这个组件在 Shadow DOM 中渲染了五个星星,通过 attributeChangedCallback 响应属性变化,并派发可冒泡且穿透 Shadow DOM 的自定义事件。外部可以通过监听 rating-change 事件获取点击结果。
总结
通过以上示例,你已经掌握了在 JavaScript 中使用 Shadow DOM 的常见方法:创建 Shadow Root、隔离样式、使用模板和插槽、处理事件,以及将其集成到自定义元素中。Shadow DOM 的核心价值在于封装与复用,合理利用它可以构建出健壮且易于维护的组件化 Web 应用。在实际项目开发中,建议优先使用开放模式(open)以便调试,并遵循 Web Components 标准,让组件在不同框架之间也能无缝协作。