Shadow DOM 提供的是边界而非私有性,其结构保护依赖设计契约而非技术封锁;closed 模式无法真正阻止探测,事件、样式、API 暴露等仍可间接推断内部结构。

Shadow DOM 本身不提供“私有性”,它提供的是 边界 —— 一个浏览器强制执行的 DOM 查询与样式作用域分隔线。所谓“保护结构”,本质是让外部 JS 无法通过常规方式访问或修改 shadow 内部节点,但这个效果取决于你是否选对了 mode、是否暴露了不该暴露的接口。
attachShadow({ mode: 'closed' }) 并不能真正阻止探测
很多人以为 mode: 'closed' 就等于“彻底锁死”,其实不是:
-
shadowRoot属性确实返回null,但事件监听、getComputedStyle()、甚至原型重写(如劫持HTMLElement.prototype.attachShadow)仍可能间接推断内部结构 - 无障碍工具、DevTools 扩展、或某些分析 SDK 可能绕过限制读取渲染树(尤其在 Chromium 126+ 中已有相关 bypass 报告)
- 一旦组件响应外部属性变更(比如
@prop或attributeChangedCallback),其 DOM 响应逻辑本身就构成了可被逆向的“行为指纹”
结构不被查到 ≠ 结构不可推断
即使 document.querySelectorAll('my-button input') 查不到 shadow 内的 <input>,以下方式仍可能暴露它:
- 组件对外 dispatch 的事件中携带
composed: true,且未过滤 detail 字段 → 外部可通过监听input事件反推存在输入控件 -
:host(:focus-within)样式触发时,配合getBoundingClientRect()测量布局变化,可推测内部 focusable 元素数量与位置 - 若组件暴露了方法如
focusInput(),调用后检查document.activeElement是否为宿主元素 → 可确认内部有可聚焦节点
真正有效的结构保护靠设计契约,而非技术封锁
与其依赖 closed 模式制造虚假安全感,不如用明确的封装契约降低攻击面:
- 所有交互入口必须走
@prop/@method/CustomEvent,禁止在文档中直接querySelector子组件再调用其内部方法 - 避免在 shadow 内使用
id="inner-input"后又在宿主上暴露getInputEl()这类 API —— 这等于开门放人 - 如果必须透传结构能力(如表单提交),用
<slot name="form-control"></slot>让使用者自己提供可控的 input,而不是在 shadow 里硬编码一个 - 对第三方 SDK(如 Sentry、Hotjar),显式禁用 DOM 捕获:在初始化时设置
denyUrls: [/shadow-root/]或拦截mutationobserver回调
结构“私有”的关键不在 shadow root 是否可访问,而在于你是否把内部实现细节当作公共契约来维护。哪怕用了 open 模式,只要不暴露内部节点引用、不依赖具体 class 名做逻辑分支、不把 shadow 内 DOM 当作 API 返回值,外部代码就很难稳定地耦合你的结构。

















