标签本身不提供样式隔离或共享,仅是静态占位符;必须配合 attachshadow({ mode: 'closed' }) 和 document.importnode() 才能实现真正隔离与状态保留。

直接用 <template></template> 标签不会自动隔离或共享样式
很多人以为把子应用 HTML 写进 <template id="subapp"></template> 就能天然“模块化”,其实浏览器只是跳过解析、不渲染、不执行脚本——它既不隔离,也不共享,只是一个静态占位符。你手动 innerHTML = tmpl.innerHTML 插入后,所有 class、id、<style></style> 全部落在 light DOM 里,照样被主应用 CSS 穿透,第三方库(比如 Ant Design)的 .ant-btn 依然生效。
常见错误现象:
- 子应用模板里写
<div class="header">,结果被主应用的 <code>.header { margin-top: 20px }改了间距 - 多个子应用共用同一份
<template></template>,但各自data-bind填充后,id="modal"重复导致document.getElementById('modal')返回错对象 - 模板含
<script>console.log('init')</script>,克隆后插入 DOM,这段脚本根本不会执行 - 必须从
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或动态生成唯一标识(如data-id="${Date.now()}-${Math.random().toString(36).substr(2, 9)}") - 在自定义元素 constructor 中调用
this.attachShadow({ mode: 'closed' })—— 缺少这句,等于没建墙 - 克隆模板后,用
shadowRoot.appendChild(fragment)挂载,而非直接 append 到 light DOM - 所有子应用 CSS 必须显式注入:不能靠
<link rel="stylesheet">,要用CSSStyleSheetAPI 或adoptedStyleSheets(Chrome 73+/Safari 16.4+) - 避免在模板里写
<style></style>标签 —— 直接塞进shadowRoot.innerHTML会导致逃逸,必须提取内容转为CSSStyleSheet实例再注入 - 构建阶段用 PostCSS 插件(如
postcss-css-namespace)统一加前缀:把.btn编译成[data-subapp="login"] .btn,并确保所有子应用共用同一套配置 - 运行时由主应用拦截子应用的
<link rel="stylesheet">请求,返回重写后的 CSS 文本(含 data 属性前缀),再通过adoptedStyleSheets注入到对应 Shadow Root - 对
@import url(...)和url(./icon.png)这类相对路径资源,需额外做路径代理或构建时转为绝对 URL,否则前缀无效 - 纯
<template></template>方案不适合高频交互场景 —— 它没有生命周期钩子,状态更新只能靠手动 diff,复杂度很快超过 Web Component
document.importNode() 是安全克隆 <template></template> 的唯一可靠方式
cloneNode(true) 会丢掉表单控件状态(如 <input checked> 的选中态),而 document.importNode(tmpl.content, true) 能保留原生行为,兼容 Shadow DOM 场景,是微前端中复用模板的底线操作。
实操要点:
想让 <template></template> 内容真正隔离?必须配合 attachShadow({ mode: 'closed' })
<template></template> 本身无作用域,只有把它克隆后的 fragment 插入到 closed Shadow DOM 中,才能切断样式穿透和 DOM 查询链。否则,哪怕你用了 data-app="cart",主应用的 [data-app="cart"] .btn 也拦不住 Ant Design 的全局 .ant-btn 规则。
关键步骤:
共享逻辑别堆在模板里,用 data-* + 构建时前缀 配合运行时注入才可控
想让多个子应用共用一套按钮样式?别在每个 <template></template> 里复制粘贴 <style>.shared-btn { ... }</style>。这种写法维护成本高,且无法保证 scope 一致。
推荐组合方案:
真正的隔离不在模板语法里,而在你是否强制所有子应用走同一套构建规则、是否在挂载前校验 attachShadow 调用、是否拒绝任何 innerHTML = htmlString 的快捷写法。这些点漏掉一个,泄漏率就不是 0.3%,而是回到 64%。











