Shadow DOM 是唯一能真正隔离样式的原生机制,必须在 constructor() 中调用 attachShadow({ mode: 'closed' }) 创建隔离边界,否则所有封装均为假隔离;mode: 'open' 会暴露 shadowRoot 导致样式可被篡改,而 closed 模式虽返回 null 但 DevTools 仍可查看,CI/CD 应强制校验其使用。

Shadow DOM 是唯一能真正隔离样式的原生机制
不调用 attachShadow({ mode: 'closed' }),所有“模板封装”都是假隔离。浏览器只认 DOM 结构,customElements.define() 本身不创建样式边界——外部 button { color: red } 依然会穿透进来,.modal-overlay 冲突、z-index 错乱、全局 normalize.css 覆盖子应用字体,全因此而起。
必须在自定义元素的 constructor() 中显式执行:
class MyCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'closed' }); // ✅ 唯一有效起点
}
}
-
mode: 'open'等于放弃隔离:任何脚本都能通过el.shadowRoot修改内部样式或结构 -
mode: 'closed'下el.shadowRoot返回null,但 DevTools 可展开#shadow-root查看(需开启“Show user agent shadow DOM”) - CI/CD 应强制校验:所有
attachShadow调用必须含{ mode: 'closed' },否则阻断构建
不能用 shadowRoot.innerHTML = htmlString 注入子应用 HTML
直接赋值会绕过 Shadow DOM 封装机制:<style></style> 逃逸到全局作用域,<script></script> 在全局上下文中执行,既污染主应用样式,又带来 XSS 风险。
常见错误现象:
立即学习“前端免费学习笔记(深入)”;
- 子应用引入的 Ant Design 样式覆盖了主应用按钮颜色
- 控制台报错
Failed to execute 'querySelector' on 'Document': The provided selector is empty(因脚本在全局执行但 DOM 尚未挂载)
正确做法是用 DOMParser 解析后分治处理:
- 提取所有
<style></style>内容,转为CSSStyleSheet实例,再通过shadowRoot.adoptedStyleSheets = [sheet]注入(Chrome 73+ / Safari 16.4+ 支持) - 拦截
<script src=""></script>,fetch后在沙箱中执行(重绑定this/window/document到当前 shadow 上下文) - 剥离内联
<script></script>,避免意外执行
<template> 本身不提供任何样式隔离能力
很多人以为把子应用 HTML 写进 <template id="subapp"></template> 就能天然“模块化”,其实浏览器只是跳过解析、不渲染、不执行脚本——它既不隔离,也不共享,只是一个静态占位符。
你手动 innerHTML = tmpl.innerHTML 插入后,所有 class、id、<style></style> 全部落在 light DOM 里,照样被主应用 CSS 穿透,第三方库(比如 Ant Design)的 .ant-btn 依然生效。
实操要点:
- 必须从
tmpl.content克隆,不能用tmpl.innerHTML—— 后者会丢失select选项、textarea值、input的checked/disabled状态 - 克隆后需遍历
fragment.querySelectorAll('[data-bind]')手动填充,不要用正则替换{{title}}—— 容易 XSS,且无法处理属性绑定(如data-bind="src") - 挂载时用
container.appendChild(fragment),禁用innerHTML +=—— 否则已挂载的事件监听器会被销毁 - 模板内禁止硬编码
id,改用data-id或动态生成唯一标识
iframe 不是组件替代品,而是沙箱容器
<iframe> 拥有独立 document、window 和样式作用域,天然隔离,但它不是 HTML 组件的合理替代方案。
把它当组件用,等于把按钮、卡片这类轻量 UI 单元强行塞进另一个完整页面上下文中:
- 每次加载触发完整的 HTML 解析 + CSSOM 构建 + JS 引擎初始化,内存占用通常是 Shadow DOM 的 2.3 倍(实测 Chrome 124)
- 父子通信只能走
postMessage,无法直接访问 DOM、共享状态或事件冒泡 - iOS Safari 对
height: 100%渲染不稳定,常出现双滚动条、焦点丢失 - SEO 不友好,且无法继承父页的
<link rel="preload">等资源提示
选 <iframe> 当且仅当子应用是完全不可信的第三方服务(如嵌入银行支付页、广告 SDK),其余情况优先用 Shadow DOM + JS 沙箱组合。
真正容易被忽略的是:即使用了 adoptedStyleSheets,它也不支持 @import、字体声明、媒体查询回退;mode: 'closed' 下无法调试,但也不能靠 mode: 'open' 来“方便开发”——生产环境一旦放开,就等于主动交出样式控制权。



















