Web Components 实践指南:如何优雅使用 Shadow DOM?

作者:Russell Beswick

原文地址:Smashing Magazine

翻译:掘金安东尼


在构建现代 Web 应用时,我们越来越倾向于组件化,而 Web Components 正是原生支持这一理念的标准。你也许已经听说过它的三大核心能力:自定义元素(Custom Elements)、HTML 模板(HTML Templates)和影子 DOM(Shadow DOM)。

在这篇文章中,我们将聚焦其中最神秘却也最强大的部分 ------ Shadow DOM,深入剖析它的动机、原理、使用方式以及最佳实践。


什么是 Web Components?

我们常见的 Web Components 示例通常只关注自定义元素(Custom Elements),比如 <my-component> 这样的标签。但实际上,Web Components 是一组可以搭配使用、也可以独立使用的浏览器原生 API,包括:

  • 自定义元素(Custom Elements)
  • HTML 模板(HTML Templates)
  • 影子 DOM(Shadow DOM)

这意味着:你可以只用自定义元素而不必引入 Shadow DOM,但一旦你需要封装样式、避免冲突、提高组件的复用性,Shadow DOM 就会变得不可或缺。


为什么需要 Shadow DOM?

现代 Web 应用由各种来自不同库、框架甚至第三方的组件构成,DOM 元素之间极容易产生冲突。CSS 污染、全局选择器、类名冲突、脚本注入......都是典型的老问题。

比如你可能看到这样的"div soup":

xml 复制代码
<div id="my-custom-app-framework-landingpage-header" class="my-custom-app-framework-foo">
  <div><div><div><div><div><div>etc...</div></div></div></div></div></div>
</div>

Shadow DOM 的设计初衷就是要解决这些问题。它可以把 HTML、CSS 甚至 JavaScript 封装在组件内部,创建一个完全隔离的子树,就像 <video><details> 元素那样,避免外部影响。


哪些元素可以使用 Shadow DOM?

虽然 Shadow DOM 通常和自定义元素搭配使用,但它其实也可以附着在很多原生 HTML 元素上,比如:

css 复制代码
<aside> <blockquote> <body> <div> <footer> <h1>--<h6> <header> <main> <nav> <p> <section> <span>

但有些元素像 <input><select>,虽然内置了 shadow root,却不允许通过脚本访问,需要在 DevTools 中打开"显示用户代理 Shadow DOM"选项才能查看。


如何创建 Shadow DOM?

我们可以通过两种方式创建 Shadow Root:

1. 命令式创建

ini 复制代码
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<p>Hello from the Shadow DOM!</p>';
document.body.appendChild(host);

如果使用 { mode: 'open' },你可以通过 host.shadowRoot 访问其内部内容;但如果设为 closed,这个属性会返回 null,外部无法访问。这种模式更适合处理敏感信息,比如银行账户组件。

✅ 推荐默认使用 closed 模式,除非你明确需要调试或数据暴露。


2. 声明式创建

无需 JavaScript,也可以使用 HTML 模板声明 Shadow Root:

xml 复制代码
<my-widget>
  <template shadowrootmode="closed">
    <p>Declarative Shadow DOM content</p>
  </template>
</my-widget>

对于一些纯静态组件,这是更安全、更兼容 SEO 的方式。如果你注册了这个自定义元素,还可以通过 ElementInternals 访问影子根:

scala 复制代码
class MyWidget extends HTMLElement {
  #internals = this.attachInternals();
  #shadowRoot = this.#internals.shadowRoot;

  connectedCallback() {
    const p = this.#shadowRoot.querySelector('p');
    console.log(p.textContent);
  }
}
customElements.define('my-widget', MyWidget);

Shadow DOM 的高级配置项

除了 modeattachShadow 还支持以下三个选项:

1. clonable: true

在传统 DOM 中使用 cloneNode(true) 克隆一个附有 Shadow DOM 的元素时,默认不会拷贝影子内容。但现在你可以这样做:

xml 复制代码
<template shadowrootmode="closed" shadowrootclonable>
  <p>This is a test</p>
</template>

这样克隆出来的元素会包含 Shadow DOM 内容,非常适合组件复用。


2. serializable: true

启用后可以用 getHTML() 获取当前 Shadow DOM 的字符串表示,用于缓存或复制到另一个元素上。不过一定要小心,避免意外泄露敏感数据。

ini 复制代码
const html = element.getHTML({ serializableShadowRoots: true });

你还可以使用 setHTMLUnsafe() 动态注入:

arduino 复制代码
this.#shadow.setHTMLUnsafe(`
  <template shadowrootmode="open" shadowrootserializable>
    <p>Deep Shadow DOM Content</p>
  </template>
`);

3. delegatesFocus: true

这个选项让宿主元素像 <label> 一样自动把焦点传递给内部的 <input>

ini 复制代码
<template shadowrootmode="closed" shadowrootdelegatesfocus>
  <input type="text" />
</template>

适合表单组件,但需要注意:默认情况下,Shadow DOM 内的字段不会自动参与表单提交或验证,需要搭配 ElementInternals 使用。


使用 <slot> 进行内容投递

Shadow DOM 并不是完全封闭的。你可以使用 <slot> 定义插槽,实现内容的注入与分发:

xml 复制代码
<my-widget>
  <template shadowrootmode="closed">
    <h2><slot name="title"><span>Fallback Title</span></slot></h2>
    <slot name="description"><p>A placeholder description.</p></slot>
    <ol><slot></slot></ol>
  </template>

  <span slot="title">A Slotted Title</span>
  <p slot="description">Example description</p>
  <li>Foo</li><li>Bar</li><li>Baz</li>
</my-widget>

每个 <slot> 都可以监听 slotchange 事件,并通过 assignedNodes() 获取内容:

ini 复制代码
slot.addEventListener('slotchange', () => {
  const nodes = slot.assignedNodes();
});

这些插入的 DOM 节点仍属于宿主元素的"轻量级 DOM",可以直接用 document.querySelector 访问。


总结:从神秘到掌控

现在你应该已经理解:

  • Shadow DOM 是什么?
  • 什么时候应该用?
  • 怎么用才安全高效?

它不仅是样式封装的利器,也是组件安全的基础设施。未来我们还会深入探讨 Shadow DOM 中的样式作用域、变量穿透、CSS scoping 等高级技巧。

相关推荐
haruma sen5 分钟前
Spring面试
java·spring·面试
素界UI设计16 分钟前
开源网页生态掘金:从Bootstrap二次开发到行业专属组件库的技术变现
前端·开源·bootstrap
潘小安18 分钟前
【译】六个开发高手使用的 css 动画秘诀
前端·css·性能优化
前端开发爱好者28 分钟前
尤雨溪官宣:Vite 历史性的一刻!超越 Webpack!
前端·javascript·vite
前端开发爱好者32 分钟前
Vue3 "抛弃" Axios !用上了 专属请求库!
前端·javascript·vue.js
前端开发爱好者32 分钟前
"Lodash" 的终极版!Vue、React 通杀!
前端·javascript·全栈
前端开发爱好者33 分钟前
TanStack:不止于 Vue!一个库,真·通杀所有框架!
前端·javascript·vue.js
WBluuue35 分钟前
数据结构与算法:哈希函数的应用及一些工程算法
c++·算法·面试·哈希算法
curdcv_po1 小时前
Three.js,给纹理,设颜色空间
前端
站大爷IP1 小时前
HTTPS代理抓包完全攻略:工具、配置与高级技巧
前端