<template>是结构预定义工具,非Shadow DOM替代品;须用cloneNode(true)克隆插入才生效,直接innerHTML写字符串会导致插槽失效、XSS风险且难维护。

用 <template> 定义可复用结构,别直接写死在 DOM 里
直接把 header、card 这类结构硬编码进主页面,等于放弃模块化。真正可拆解的前提是:结构必须先“隐身”。<template> 就是干这事的——它不渲染、不执行脚本、不加载图片,纯粹存档 HTML 片段。
常见错误是把 <template> 放在 <body> 深处,导致 JS 查找困难;或给多个模板用相同 id,克隆时互相覆盖。
- 每个模板必须有唯一
id,比如<template id="product-card"> - 模板内避免使用全局 CSS 类名(如
header),优先用带前缀的命名(ui-header)防止样式污染 - 如果模板含表单控件,克隆后需重置
name和id属性,否则提交时字段冲突
通过 customElements.define() 注册自定义标签,实现语义化调用
写 <nav-bar></nav-bar> 比写一堆 <div class="nav"> 更接近“模块”本质——但光写标签没用,必须注册才能被浏览器识别。
容易踩的坑是:在 DOM 加载完成前就调用 customElements.define(),导致报错 Failed to execute 'define' on 'CustomElementRegistry': the name "nav-bar" has already been used;或者继承 HTMLElement 但漏写 super(),构造函数直接崩溃。
立即学习“前端免费学习笔记(深入)”;
- 注册必须在
DOMContentLoaded之后,或确保脚本放在<body>底部 - 类定义中不要在
constructor里操作 DOM,只做初始化;真实渲染逻辑放connectedCallback里 - 属性传值要用
static get observedAttributes()声明,否则attributeChangedCallback不触发
用 <slot> 实现内容分发,而不是拼接字符串
模块不是“固定内容”,而是“可填空的容器”。比如一个卡片组件,标题、描述、操作按钮位置固定,但具体内容由使用者决定——这靠 <slot>,不是靠 JS 字符串替换。
典型误用是嵌套多层 <slot> 却没设 name,结果所有内容全塞进第一个默认插槽;或在 Shadow DOM 外部写 <div slot="actions">,但组件内部没声明对应 <slot name="actions">,内容直接消失。
- 默认插槽(无
name)只能有一个,且必须放在 Shadow DOM 内容最外层 - 具名插槽要严格匹配
name属性,大小写敏感,空格也不行 - 如果组件可能不传内容,记得在
<slot>里写 fallback,比如<slot>暂无描述</slot>
构建阶段合并 vs 运行时加载,选错路径会卡死开发流
静态站点用 fetch('header.html') 动态加载,开发时若直接双击打开 HTML 文件,会因 file:// 协议触发 CORS,报错 TypeError: Failed to fetch;而用 Webpack/Vite 构建时硬套 SSI 指令 <!--#include file="footer.html" -->,又会在浏览器里原样显示注释,毫无作用。
根本区别在于:构建阶段合并是编译期行为(输出一个 HTML),运行时加载是客户端行为(多次 HTTP 请求)。混用等于让工具链互相打架。
- 纯静态项目 + 本地调试 → 必须起服务(
npx serve或python3 -m http.server),再用fetch - Webpack/Vite 项目 → 用
html-webpack-plugin或vite-plugin-html插件做预编译包含,别碰 SSI - 服务端渲染环境(如 Express/Nginx)→ 才考虑 SSI,且主文件扩展名得是
.shtml,配置必须开Options +Includes
<user-card> 组件,可能在开发时走 <template> 克隆,上线时被构建工具打包进主 HTML,而 SSR 场景下又由后端吐出完整 DOM。模块化真正的成本,是保持这三层路径的数据结构和生命周期一致。



















