attachShadow() 是创建 Shadow DOM 封装边界的唯一入口,必须显式调用才能生成隔离的 shadow tree;未调用则无边界,且 scoped 属性已被主流浏览器移除。

attachShadow() 是创建封装边界的唯一入口,不是可选开关
Shadow DOM 的封装与隐藏不是靠属性或配置开关实现的,必须显式调用 attachShadow() 才能生成隔离的 shadow tree。没这一步,就根本没有边界——scoped 属性早已被 Chrome 120+ 和 Firefox 115+ 彻底移除,BEM 类名、CSS-in-JS 或 !important 都无法阻止全局 button { color: red } 覆盖你的按钮。
常见错误包括:
- 对
<img>、<p>、<input>等原生替换元素调用attachShadow(),抛出DOMException: This element does not support shadow roots - 在元素尚未挂载到 DOM 前就调用(比如在
document.createElement('div')后立刻调用),导致失败 - 重复调用
attachShadow()到同一元素,触发InvalidStateError
安全做法是:只对 <div>、<span> 或自定义元素(如 <my-card>)调用;在自定义元素的 constructor() 中调用最稳妥;始终传入 { mode: 'open' } —— 'closed' 会让 element.shadowRoot 永远为 null,DevTools 查不到,调试基本瘫痪。
样式必须进 CSSOM,不能拼进 innerHTML
把 <style>.btn { color: blue }</style> 直接写进 shadowRoot.innerHTML 是最常见也最危险的误操作。浏览器会把它当普通 HTML 解析,<style> 标签不会进入 CSSOM,后续 JS 修改 style.textContent 完全无效,甚至引发 FOUC(Flash of Unstyled Content)。
立即学习“前端免费学习笔记(深入)”;
正确注入方式只有两种:
const style = document.createElement('style'); style.textContent = ':host { font-family: inherit; } .btn { color: blue; }'; shadowRoot.appendChild(style);- 更现代方案:
const sheet = new CSSStyleSheet(); sheet.replaceSync('.btn { color: blue; }'); shadowRoot.adoptedStyleSheets = [sheet];(注意 Safari 16.4+、Chrome 73+ 支持,IE/旧 Edge 不支持)
别忘了显式继承基础样式::host { font-family: inherit; color: inherit; },否则 <body style="font-family: system-ui"> 的字体根本透不进去。
:host 和 ::slotted() 是唯一合法的“穿透通道”,其余全是幻觉
Shadow DOM 的边界是硬隔离,但 W3C 规范只开放两个可控出口::host 响应宿主自身状态,::slotted() 有限透传可继承样式到 slot 内容。除此之外所有“穿透”方式都不可靠或已被移除。
典型误用:
-
::slotted(*):部分浏览器行为不一致,且破坏 slot 设计本意;只对顶层 slot 子节点生效,嵌套结构需显式写::slotted(.header) -
:host-context(.dark):Chrome/Firefox 已标记为 deprecated,应改用:host(.dark)+ 外部 class 控制宿主 -
div > p匹配 slot 里的<p>:无效,::slotted(p)才合法,且仅支持color、font-family、line-height、background-color等可继承属性
布局类属性(margin、display、width)永远不能靠 ::slotted() 控制——那是使用者的责任,组件只负责样式继承链。
closed 模式不等于“绝对隐藏”,只是切断标准访问路径
mode: 'closed' 下 element.shadowRoot 永远返回 null,这是规范强制设计,不是 bug。它阻断的是 element.shadowRoot.querySelector() 这类标准 JS 访问路径,但无法防御 DevTools 查看(开启 “Show user agent shadow DOM” 后仍可见)、事件监听(composed: true 的事件照样能冒泡出来)、或 HTML 注入后的样式逃逸。
真正安全的封装需要组合策略:
- 用
mode: 'closed'防止误操作和脚本遍历 - 对外暴露方法时手动控制逻辑(如
focus()内部调用this.shadowRoot.querySelector('input')?.focus()) - slot 内容必须经过 HTML 净化(避免
<script>执行),<link rel="stylesheet">必须代理并重写作用域 - 关键状态变更通过
CustomEvent发出,且设composed: false阻断跨 boundary 传播
最关键的细节往往被忽略:Shadow DOM 的隔离是浏览器在样式计算阶段就跳过跨 boundary 匹配,不是优先级博弈——!important 在这里毫无意义,它连参选资格都没有。



















