是web components中唯一安全的html结构容器,因其不渲染、不执行脚本、不加载资源,仅在content.clonenode(true)时激活;必须用template.content获取内容,不可用innerhtml;connectedcallback是操作shadowroot的可靠时机;是运行时注入契约,需明确定义;html modules未落地,应通过domparser安全解析模板。

Web Components 中 <template></template> 是唯一安全的 HTML 结构容器
直接把 HTML 字符串拼进 innerHTML 或用 document.createElement('div').innerHTML = htmlStr 注入模板,等于把结构、样式、事件逻辑全暴露在全局作用域里——后续任何 JS 都能篡改它,也根本谈不上“分层”。<template></template> 标签天生不渲染、不执行脚本、不加载资源,它的内容只在被 content.cloneNode(true) 显式提取时才激活,这是浏览器原生提供的隔离边界。
常见错误是把 <template></template> 当成普通容器写样式或加事件:<template id="card"><div onclick="bad()">...</div></template>。onclick 会被忽略,但开发者常误以为“没生效是 JS 没绑好”,其实根本没解析到 DOM 树里。
-
<template></template>内部不能含<script></script>或<link rel="stylesheet">,它们不会执行也不会加载 - 必须用
template.content获取文档片段,不能用template.innerHTML—— 后者返回空字符串 - 若需动态插入子节点,应在 clone 后操作
shadowRoot或目标容器,而非直接修改template元素本身
用 customElements.define() 绑定模板时必须走 connectedCallback
很多人在 constructor 里就调用 this.attachShadow({ mode: 'open' }),结果发现 this.shadowRoot.querySelector('button') 总是 null。因为此时元素还没挂载到 DOM,shadowRoot 虽已存在,但内部节点尚未解析完成。
真正可靠的时机只有 connectedCallback:它保证元素已插入文档、shadowRoot 已就位、且模板内容可通过 querySelector 安全访问。
- 不要在
constructor或attributeChangedCallback里操作shadowRoot的子节点 -
connectedCallback可能被多次调用(如元素被移出又插入),需加防重逻辑,比如用if (this._rendered) return - 若模板依赖属性(如
data-title),应在attributeChangedCallback中触发重新渲染,而不是直接改shadowRoot内容
<slot></slot> 不是占位符,而是运行时内容注入契约
写 <my-card><h2>标题</h2></my-card> 时,如果组件内部没声明 <slot></slot>,那 <h2></h2> 就永远卡在 my-card 的 childNodes 里,既不显示也不参与样式继承。这不是“没生效”,而是浏览器明确拒绝了未声明的插槽注入。
使用 Puppeteer + Chrome 将 HTML 渲染为中文 PDF,自动处理图表等待、Tab 展开、动画、测高、白边消除、防分页,适用于看板、报表、网页和交互图表转 PDF。
更隐蔽的问题是命名插槽和默认插槽混用:<slot name="header"></slot> 只接收 <span slot="header"></span>,而 <span></span> 这类无 slot 属性的节点,只会落到第一个无 name 的 <slot></slot> 中——如果没写,默认插槽就丢弃所有内容。
- 每个
<slot></slot>必须有明确语义,比如<slot name="actions"></slot>,避免用slot="1"或slot="left"这类位置描述 - 若组件需要 fallback 内容,写在
<slot>默认文案</slot>里,而非靠 JS 判断assignedNodes().length === 0 -
<slot></slot>不支持 CSS 选择器穿透,my-card::part(header) { color: red }这种写法无效;真要样式控制,得用:host(> [slot="header"]) { ... }
HTML Modules 尚未落地,fetch() 加载模板必须手动处理解析
现在有人直接 fetch('card.html').then(r => r.text()).then(html => this.shadowRoot.innerHTML = html),这会绕过 <template></template> 的安全机制:内联脚本执行、CSS 注入、XSS 风险全开。浏览器不会帮你做转义或沙箱隔离。
正确做法是让服务端返回标准 HTML 片段(不含 、),然后用 DOMParser 解析并提取 <template></template> 内容:
const parser = new DOMParser();
const doc = parser.parseFromString(await res.text(), 'text/html');
const template = doc.querySelector('template');
if (template) {
this.shadowRoot.appendChild(template.content.cloneNode(true));
}
注意:DOMParser 对 malformed HTML 容错性差,服务端返回的 HTML 必须是严格格式化的片段,否则 querySelector('template') 可能返回 null。
- 不要信任第三方 HTML 片段,尤其来自 CMS 或用户输入的内容
- 若模板含
<style></style>,确保它被包裹在<template></template>内,否则DOMParser会立即执行样式规则 - 构建时可预编译模板为 JS 字符串(如
export const cardTemplate = `<template>...</template>`),避免运行时解析开销
最易被忽略的点是:解耦不是靠“把 HTML 文件拆开”,而是靠运行时是否允许外部脚本任意读写组件内部结构。哪怕所有模板都放在一个文件里,只要用了 customElements.define() + attachShadow() + <slot></slot>,就能切断耦合;反之,哪怕拆成 100 个文件,只要用 innerHTML 直接注入,就仍是裸奔状态。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!










