innerHTML = htmlString 一定会污染样式,因为浏览器解析时绕过沙箱和 Shadow DOM 封装,style/script 直接全局生效;唯一安全方式是用 DOMParser 解析后手动处理资源并注入 closed shadowRoot。

直接 innerHTML = htmlString 为什么一定会污染样式
因为浏览器对 innerHTML 的解析完全绕过沙箱和 Shadow DOM 封装逻辑。<style> 标签内容被当作普通全局样式注入 document,<script> 直接在当前 window 上执行——哪怕你已经挂载到 shadowRoot 里,只要用的是 shadowRoot.innerHTML = htmlString,就等于主动打开逃逸通道。
常见错误现象包括:normalize.css 重置了整个页面的 body 字体大小、子应用定义的 .modal-overlay 覆盖了主应用的 z-index、两个子应用都用了 .btn 类但样式互相覆盖。
- 不能靠加 wrapper div 解决:后代选择器(如
#app .btn)仍会穿透作用 -
@scope规则目前无任何主流浏览器支持,别写进生产代码 - Vue 的
<style scoped>只保护组件内静态样式,对子应用动态插入的<style>无效
DOMParser 是唯一安全的 HTML 解析入口
必须用 new DOMParser().parseFromString(html, 'text/html') 获取标准 Document 对象,才能逐节点控制资源分发。它返回的 doc.body 是干净 DOM 片段,不含原始上下文;doc.head 里的 <link rel="stylesheet"> 和 <style> 也未执行,可被拦截重写。
实操时要提取并分类处理:
立即学习“前端免费学习笔记(深入)”;
- 所有
<script src="xxx">:用new URL(scriptSrc, response.url)补全绝对路径,再交由沙箱(如 qiankun 的execScripts)加载 - 所有内联
<script>:必须用Function构造器,在重绑定this/window/document的环境中执行 - 所有
<style>和<link rel="stylesheet">:提取内容或 URL,后续统一注入shadowRoot.adoptedStyleSheets或 light DOM 容器内
Shadow DOM 的 mode 必须设为 closed
attachShadow({ mode: 'closed' }) 是生产环境唯一可行方案。mode: 'open' 等于放弃隔离——外部脚本能直接读写 el.shadowRoot.styleSheets 或调用 el.shadowRoot.querySelector,样式和 DOM 都形同虚设。
注意点:
-
el.shadowRoot在closed模式下返回null,但 DevTools 里仍可通过console.dir(el)查看#shadow-root节点 -
adoptedStyleSheets必须手动注入,不能依赖<link>或<style>直接写入 light DOM - React/Vue 渲染必须显式指定
rootNode或调用createRoot(shadowRoot).render(),否则默认挂载到document.body会逃逸
动态样式注入必须走主应用统一接口
子应用运行时调用 document.head.appendChild(style) 或第三方库(如 antd Modal)默认挂载到 document.body,这些样式完全脱离容器,卸载时无法回收——这是切换子应用后样式残留的根本原因。
正确做法是强制约束:
- 禁用所有直接操作
document.head或document.body的样式注入方式 - 提供主应用级
injectStyle(text)接口,所有动态样式必须经此入口,确保挂载在子应用根节点下 - 封装第三方 UI 组件(如 AntdModal),强制
getContainer={() => subAppRoot},避免样式逃逸 - 检查
document.styleSheets列表,卸载前确认没有不属于当前子应用的CSSStyleSheet
最易被忽略的点是:DOM 移除能清掉挂载在容器内的样式,但清不掉那些“偷偷”插到 document.head 里的——它们不会随子应用卸载而消失,只会越积越多。



















