是浏览器原生惰性容器,内容不解析、不执行、不渲染,需通过content.clonenode(true)显式克隆复用;而属非标准hack,html解析器会错误修复结构且可能预加载资源,导致xss风险与dom失真。

为什么直接用 <template></template> 而不是 <script type="text/html"></script>
因为 <template></template> 是浏览器原生支持的惰性容器,内容不会被解析、执行或渲染,直到你显式调用 content.cloneNode(true)。而 <script type="text/html"></script> 只是 hack——浏览器把它当脚本忽略,但 HTML 解析器仍会尝试修复嵌套错误(比如把 <tr> 丢到 <code><div> 里),导致结构失真。<p>常见错误现象:<code><template></template> 里写了 <tr><td>xxx</td></tr>,但没包裹在 <tbody> 中,结果 clone 后变成孤立 <code><tr>,插入表格时被浏览器自动甩到 DOM 外;<code><script type="text/html"></script> 里写 <img src="x" onerror="alert(1)">,哪怕没插入,某些浏览器也会预加载并触发 onerror。
- 必须把完整结构(包括必要父容器)写进
<template></template>,例如表格行模板要带<tbody>,列表项要带 <code><ul></ul>或<ol></ol> - 不要在
<template></template>内写内联脚本或事件属性(如onclick),它们不会绑定,且可能被 sanitizer 移除 - 若需动态插值,用占位符如
{{title}},由 JS 替换后再 append,而不是依赖 innerHTML + 字符串拼接(易 XSS)
customElements.define() 里怎么安全地复用 <template></template> 内容
不能在 connectedCallback 里每次重复 document.querySelector('#my-card-template').content.cloneNode(true)——模板查询是全局操作,多实例并发时可能查错节点;更糟的是,如果模板被移除或重命名,整个组件就静默失败。
实操建议:把模板提取为静态属性,在类定义时就绑定,避免运行时查找:
class MyCard extends HTMLElement {
static template = document.getElementById('my-card-template').content;
<p>connectedCallback() {
if (!this.shadowRoot) {
this.attachShadow({ mode: 'open' });
this.shadowRoot.appendChild(MyCard.template.cloneNode(true));
}
}
}</p>
注意点:
-
document.getElementById()必须在customElements.define()前执行,否则返回 null;推荐把模板放在或顶部,JS 在<script></script>中同步执行 - 若模板跨 HTML 文件(如通过
<link rel="import">,已废弃),改用fetch()+DOMParser加载,但需处理 CSP 和缓存 - 不要用
innerHTML动态生成模板字符串再 parse——无法保证 HTML 结构合法性,且丢失原生元素生命周期
大规模项目中模板与 Shadow DOM 的协作边界
<template></template> 只负责结构分发,不解决样式污染;Shadow DOM 才提供作用域隔离。两者必须配合,否则模板内容一插入就受全局 CSS 影响。
典型翻车场景:
文章转信息图。将文章/笔记转化为手机可读的 HTML 信息图,自动匹配视觉风格。触发场景:文章转图、笔记转图、信息图、转小红书图、做张图、可视化这篇文章、文生图。
- 模板里写
<h2 class="title">{{text}}</h2>,但全局有h2 { margin-top: 0; },导致所有卡片标题间距消失 - 用
slot透传用户内容,但没在 shadowRoot 样式里重置all: initial或inherit,父级字体颜色/行高直接穿透进来 - 多个组件共用同一份模板 HTML,但各自 Shadow DOM 的
:host样式冲突(比如都设:host { display: block; })
关键约束:
- 每个自定义元素的模板应专属,避免跨组件复用同一
<template id="shared"></template>—— 看似省事,实则让样式和逻辑耦合变隐性 - Shadow DOM 内部样式必须包含重置:至少声明
* { box-sizing: border-box; }和:host { all: initial; }(除非明确需要继承) - 若需主题切换,不要靠外部 class 切换,而应在模板中预留
data-theme属性,由组件 JS 控制shadowRoot.querySelector('[data-theme]').setAttribute
模板加载失败时怎么降级和调试
模板缺失不会抛错,只会让 content 为空,组件渲染成空白——这比报错更难排查。
简单但有效的检测方式:
- 在 define 前加断言:
if (!document.getElementById('my-card-template')) throw new Error('Template #my-card-template missing'); - 开发环境启用
console.warn提示:if (!MyCard.template?.firstChild) console.warn('MyCard template empty') - 给模板加
data-debug属性,构建工具扫描它生成缺失报告(适合 CI 流程)
真正麻烦的是异步加载模板(如微前端场景):
- 不能等
fetch完再 define 组件——会导致 HTMLUnknownElement 无法升级 - 正确做法:先注册空骨架组件,用
not(:defined)显示 loading 占位,待模板加载完再调用customElements.upgrade()强制刷新 - 避免在模板加载回调里反复调用
upgrade(),应节流或用MutationObserver监听特定容器
最常被忽略的一点:模板的 id 必须全局唯一,且不能和组件名同名(比如组件叫 my-card,模板 id 也叫 my-card),否则 getElementById 可能返回组件实例而非 template 元素。










