template标签是结构预定义工具,须用cloneNode(true)或importNode深克隆后插入才生效;直接innerHTML写字符串会导致插槽失效、XSS风险且难维护。

用 template + content 做运行时组件挂载
浏览器原生支持的 <template> 标签不会被渲染,但能安全存 HTML 片段,配合 document.importNode 和 <slot> 可实现无框架的轻量组件化。它比字符串拼接安全,比 Web Components 更低侵入性。
常见错误是直接 innerHTML 插入未克隆的 template.content,导致多次挂载时节点复用、事件丢失或 DOM 移动异常。
- 每次使用前必须调用
document.importNode(template.content, true)深克隆 -
<slot>仅在挂载后生效,需确保父容器已插入文档流再执行appendChild - 不支持动态响应式更新,适合静态结构 + 一次性初始化场景(如表单控件、弹窗模板)
用 customElements.define 注册极简自定义元素
如果你需要生命周期钩子(如 connectedCallback)和属性监听,又不想引入 Lit 或 Stencil,原生 Custom Elements API 就够用。它的体积接近零,兼容 Chrome 67+、Firefox 63+、Safari 10.1+。
容易踩的坑是把逻辑写在构造函数里就操作 this.shadowRoot,此时根节点尚未创建;或者在 attributeChangedCallback 中直接修改自身属性,触发无限循环。
立即学习“前端免费学习笔记(深入)”;
- 只在
connectedCallback中初始化 DOM 和事件绑定 - 在
static get observedAttributes()中声明要监听的属性名,且属性值变更必须通过this.setAttribute()触发 - 若需 Shadow DOM,用
this.attachShadow({ mode: 'closed' }),避免外部样式穿透干扰
用 data- 属性驱动模板参数化
没有 JSX 或模板引擎时,data- 属性是最自然的“props”传递方式。组件初始化时读取 this.dataset,转换为配置对象,再决定渲染逻辑或默认值。
注意 Safari 对 dataset 的大小写处理更严格:HTML 中写 data-api-url,JS 中读作 this.dataset.apiUrl(驼峰),但 IE 不支持此映射,所以纯轻量方案建议避开连字符,改用 data-apiurl 直接读 this.dataset.apiurl。
- 数值类属性(如
data-limit="5")需手动parseInt,dataset总是返回字符串 - 布尔类行为(如
data-disabled)建议统一用存在性判断:'disabled' in this.dataset,而非比较值 - 避免在
data-中传复杂 JSON 字符串,解析成本高且易 XSS,优先拆成多个原子属性
为什么不用 innerHTML 拼接模板字符串?
看似最简单,实则隐患最多:XSS 风险、HTML 转义遗漏、事件监听器无法自动绑定、嵌套层级深时难以维护。尤其当模板中含用户输入内容(如 ${user.name}),不加过滤直接插入就是漏洞。
更关键的是,字符串模板无法复用浏览器的解析逻辑——比如你写了 <my-button></my-button>,但没注册该元素,它只是普通标签,不会触发任何生命周期;而 <template> 或自定义元素能真正参与 DOM 构建流程。
- 若真要用字符串,至少封装一层
el.innerHTML = escapeHtml(str),其中escapeHtml替换<、>、&、" - 永远不要信任
JSON.stringify输出作为 HTML 内容,它不转义 HTML 特殊字符 - 服务端渲染同构场景下,客户端应跳过已存在的 DOM 初始化,否则会重复挂载
真正轻量的核心不是代码行数少,而是边界清晰:不接管状态、不代理事件、不重写渲染逻辑。一旦开始补响应式、diff、异步加载,就该换方案了。



















