必须在 constructor 中调用 attachShadow(),因浏览器仅允许元素实例化瞬间创建 Shadow DOM;延迟至 connectedCallback 会因 light DOM 已含子节点而报错。

直接用 <template> 配合 Web Components 是最轻量、兼容性最好、不依赖构建工具的方案,但必须把模板克隆时机和 Shadow DOM 初始化顺序搞对,否则内容不渲染或样式泄漏。
为什么必须在 constructor 里调用 attachShadow()
浏览器只允许在元素实例化瞬间(即 constructor 执行时)调用 this.attachShadow()。如果拖到 connectedCallback 再执行,HTML 解析器早已把子节点(比如文本、<slot>)塞进 light DOM,此时再 attach 就会报错:Failed to execute 'attachShadow' on 'Element': Shadow root cannot be created on a host which already has children。
实操建议:
-
constructor中只做三件事:调用super()、this.attachShadow({ mode: 'open' })、设置默认属性(如this._size = 'medium') - 所有需要访问子节点、读取
textContent、计算尺寸、操作外部 DOM 的逻辑,一律移到connectedCallback中 - 避免在
constructor中写this.innerHTML = '...'—— 这会污染 light DOM,破坏封装性
<template> 克隆必须用 cloneNode(true)
直接 appendChild(template.content) 会导致模板被“搬走”,下次实例化时 template.content 为空;而 innerHTML 字符串拼接又无法绑定事件或复用 slot。
立即学习“前端免费学习笔记(深入)”;
实操建议:
- 模板定义必须带
id,例如<template id="my-button">... - 克隆代码固定为:
const tmpl = document.getElementById('my-button').content.cloneNode(true) - 插入前检查是否已存在 shadow root:
if (this.shadowRoot) this.shadowRoot.appendChild(tmpl),避免重复挂载 - 若模板含
<slot>,确保组件用法中提供对应命名 slot,否则内容不显示
observedAttributes 不声明就收不到属性变更
HTML 属性变化(如 <my-button disabled> → <my-button disabled="false">)不会自动同步到 JS 属性,也不会触发重绘。即使你在 attributeChangedCallback 里写了 this.disabled = newValue,若没在类里声明 static get observedAttributes(),回调根本不会执行。
常见错误现象:
- 修改
<my-button size="large">后样式没变,控制台也无报错 - 动态执行
el.setAttribute('size', 'small'),组件内部无响应
实操建议:
- 所有需响应的属性(
disabled、label、theme)必须显式列在observedAttributes数组中 -
attributeChangedCallback内优先判断oldValue !== newValue,避免无效更新 - 属性值始终是字符串,数值型需手动转换:
parseInt(newValue)或parseFloat(newValue)
自定义标签名必须含且仅含一个连字符
customElements.define() 对标签名有硬性校验:必须含且仅含一个短横线(-),不能以数字开头,不能是保留字(如 html、script)。写成 mybutton 或 MyButton 都会抛出 DOMException: Failed to execute 'define' on 'CustomElementRegistry': "mybutton" is not a valid custom element name。
实操建议:
- 命名采用
kebab-case,如c-button、data-grid、ui-modal - 避免使用
x-前缀(已被历史废弃)、html-(保留字冲突) - 注册前先检查是否已定义:
if (!customElements.get('c-button')) customElements.define('c-button', CButton)
真正容易被忽略的是:模板 ID 和自定义标签名之间没有强制关联,但一旦模板被多个组件复用,ID 冲突或未查到 getElementById 返回 null,就会静默失败——建议在克隆前加 console.warn 提示模板缺失。



















