SSR 输出的自定义标签需在 HTML 解析前注册或显式升级,否则生成 HTMLUnknownElement;attributeChangedCallback 不触发 SSR 初始属性,须在 connectedCallback 中读取;Shadow DOM 内容应于 connectedCallback 或 queueMicrotask 中填充。

服务端渲染(SSR)输出的 HTML 中直接使用 <user-card> 这类自定义标签,浏览器不会自动升级——除非你提前注册、手动升级或规避解析时机问题。这不是“能不能用”的问题,而是“怎么让 SSR 输出的 HTML 在客户端正确激活”的问题。
customElements.define 必须在 HTML 解析前执行
浏览器解析 HTML 时,遇到未注册的 <user-card> 会创建 HTMLUnknownElement,之后调用 customElements.define() 不会 retroactively 升级已有节点。
- 错误做法:
<script src="components.js"></script>放在</body>底部;或用window.onload包裹注册逻辑 - 推荐做法:把
customElements.define('user-card', UserCard)写在内联<script>中,且该<script>必须出现在所有<user-card>标签之前 - 若 SSR 模板由 CMS 动态生成、无法控制顺序,必须在 DOM 加载后显式升级:
document.querySelectorAll('user-card').forEach(el => customElements.upgrade(el))
动态插入的自定义标签需手动 upgrade
SSR 返回的 HTML 是静态字符串,但后续 JS 可能通过 innerHTML = '<user-card name="Alice"></user-card>' 插入新节点——这些节点不会触发 connectedCallback,除非你主动升级。
- 每次插入后立即调用
customElements.upgrade(el),例如:el.innerHTML = str; el.querySelectorAll('user-card').forEach(customElements.upgrade); - 不要依赖 MutationObserver 自动监听并升级——它无法捕获
innerHTML批量写入中的未知元素,且性能开销大 - 若用框架(如 Vue/React)管理 DOM,确保其挂载逻辑不绕过
customElements.upgrade()调用
attributeChangedCallback 不处理 SSR 初始属性
SSR 渲染的 <user-card name="Alice" avatar="/a.jpg"></user-card> 中的 name 和 avatar 是静态 attribute,attributeChangedCallback 完全不会触发。
立即学习“前端免费学习笔记(深入)”;
- 必须在
connectedCallback中手动读取:const name = this.getAttribute('name'); - 避免在
constructor中读取——此时this.attributes可能尚未完全就位,尤其在 SSR 场景下 - 如果组件支持 JS 属性赋值(如
el.name = 'Bob'),需配套定义 setter 并同步调用this.setAttribute('name', value),才能触发attributeChangedCallback
Shadow DOM 内容填充时机要避开 DOM 尚未 ready
SSR 输出的自定义元素已存在于 DOM,但它的 Shadow Root 在 constructor 中创建后,内容不能立刻写入——因为此时 this.shadowRoot 已存在,但浏览器可能尚未完成父节点布局,直接写入易导致样式错乱或被覆盖。
- 内容填充应放在
connectedCallback中,或更稳妥地用queueMicrotask(() => { this.shadowRoot.innerHTML = ... }) - 不要在
constructor中操作this.shadowRoot.innerHTML——此时this.shadowRoot虽可访问,但 DOM 尚未稳定,部分浏览器会丢弃内容 - 若组件含异步数据(如 fetch 用户信息),务必用
AbortController绑定生命周期,避免disconnectedCallback触发时请求仍在进行
真正麻烦的不是注册或升级,而是 SSR 和 Custom Elements 的生命周期错位:服务器只管吐 HTML,浏览器却要分两阶段处理——先解析成未知节点,再靠 JS 补充行为。任何一步没对齐,就会出现“标签渲染了但没交互”“属性存在但没生效”这类静默失效问题。



















