<template>不能直接当组件用,因其内容被浏览器跳过解析、不渲染、不执行脚本、不加载资源,仅作声明式“休眠区”;必须通过document.importNode(tmpl.content, true)克隆并手动填充数据后插入DOM。

为什么不能直接当组件用
因为浏览器遇到<template>会跳过解析,里面的内容既不渲染、也不执行<script>、更不加载图片或字体——它只是个“休眠区”。常见错误是把<template>当成 Vue 的<template>或 Web Component 的<slot>,结果页面空着,控制台也没报错,纯静默失效。
直接innerHTML = tmpl.innerHTML看似能插入内容,但会丢掉<input checked>的选中态、<select>的当前选项、<textarea>的用户输入值;用cloneNode(true)也一样,表单状态全归零。
-
<template>里写{{title}}?浏览器不认识,必须手动替换 - 含
<style>或<script>?克隆后不会自动生效,得额外处理 - 硬编码
id="card-1"?多次复用就冲突,document.getElementById()随机返回
怎么安全克隆并填充内容
唯一可靠路径是document.importNode(tmpl.content, true):第一个参数必须是tmpl.content(不是tmpl本身),第二个参数true表示深克隆,能保留表单控件原生状态,也兼容 Shadow DOM 场景。
填充逻辑要手动遍历绑定点,别用正则替换字符串——XSS 风险高,且无法处理属性级绑定(比如data-bind="src"对应<img src="">)。
立即学习“前端免费学习笔记(深入)”;
- 查节点:
fragment.querySelectorAll('[data-bind]') - 填文本:
node.textContent = data[bindAttr](防 XSS) - 填 HTML:
node.innerHTML = sanitize(data[bindAttr])(必须过滤) - 挂载:
container.appendChild(fragment),禁用innerHTML +=——否则已绑定的事件监听器全丢
如何让组件支持属性传参和复用
纯<template>没有属性监听、没有生命周期,所谓“传参”就是调用 JS 工厂函数时传入一个对象,再映射到 DOM 节点。它适合静态内容或低频交互,不适合高频响应式场景。
关键设计约束比语法更重要:
- 用
data-bind="author-name"代替class="author"——语义清晰,后期改名不连带改 CSS - 工厂函数返回
DocumentFragment,不是字符串——避免每次调用都重新解析 HTML - 若需更新,缓存首次生成的节点,只改
textContent或dataset值,不重复克隆 - 模板内禁用
id,改用data-id或动态生成唯一标识(如data-id="${Date.now()}-${Math.random().toString(36).substr(2, 9)}")
想真正隔离?必须配合attachShadow({ mode: 'closed' })
<template>本身零隔离能力。哪怕你把子应用模板塞进<template id="cart">,手动克隆插入后,所有 class、style、id 全落在 light DOM,照样被主应用 CSS 穿透——.btn会被 Ant Design 的.ant-btn覆盖,[data-app="cart"] .modal照样匹配到其他模块的 modal。
真正切断样式和查询链,只有一步:this.attachShadow({ mode: 'closed' })。缺这句,等于没建墙。
- 克隆后的
fragment必须挂载到shadowRoot下,不是直接插到 body -
mode: 'closed'阻止外部 JS 访问 shadowRoot,防止意外篡改 - 注意 iOS Safari 对
adoptedStyleSheets支持不全,<style>标签仍需 inline 或预注入
复杂点不在怎么写模板,而在什么时候放弃模板——当需要样式隔离、属性响应、焦点管理或 SSR 支持时,<template>就得让位给 Custom Elements 或框架组件。



















