customElements.define()用于注册符合命名规则(含短横线、全小写)的自定义元素类,必须继承HTMLElement且在DOM加载后调用,否则<my-tab>将无效;注册后方可使用,类中需通过attachShadow创建影子DOM并封装激活逻辑与slot内容投影。

用 customElements.define() 注册选项卡自定义元素
浏览器原生支持自定义元素,但必须满足命名规则(含短横线),且不能复用已存在标签名。直接写 <my-tab> 会报错,因为没注册;必须先调用 customElements.define() 才能生效。
常见错误是把类定义写在 <script> 外部或未等 DOM 加载完成就注册。推荐放在 DOMContentLoaded 事件后,或使用 defer 属性加载脚本。
- 类必须继承
HTMLElement,不能是普通函数 - 构造函数里不能操作
this.innerHTML,需用this.attachShadow({ mode: 'open' })创建影子 DOM - 注册名必须全小写+短横线,如
'tab-container'合法,'TabContainer'或'tabcontainer'都非法
影子 DOM 中如何同步激活状态与内容显示
纯 HTML + JS 实现选项卡时靠 class 切换控制显隐,但在自定义元素里,影子 DOM 是隔离的,外部 CSS 无法穿透。所以激活逻辑必须封装在组件内部,不能依赖外部 .active 类手动干预。
典型做法是在 attributeChangedCallback 监听 active 属性变化,或提供 setActive(index) 方法触发内部更新。注意:不要在 connectedCallback 里直接写 display: block,应统一由属性变更驱动渲染。
立即学习“前端免费学习笔记(深入)”;
- 用
getBoundingClientRect()检测 tab 标题是否溢出,再动态加滚动按钮——这只能在影子 DOM 内做,外部无法获取内部尺寸 - 避免在
render()中重复创建 DOM 节点,应复用已有节点或用innerHTML = ''清空后重写 - 若支持键盘导航(Tab/ArrowKeys),焦点管理必须在影子 DOM 内实现,
document.activeElement拿不到影子内的元素
处理 slot 分发与内容投影的边界情况
自定义选项卡常通过 <slot name="title"> 和 <slot name="panel"> 接收标题和面板内容。但 slot 不是“实时绑定”,它只在首次渲染时建立映射;后续动态增删子元素,slot 不会自动更新内容。
比如用户 JS 动态插入新 <div slot="panel">,影子 DOM 里不会自动显示。要支持动态内容,得监听 slotchange 事件,并在回调中手动刷新渲染逻辑。
-
slot.assignedNodes({ flatten: true })才能拿到所有实际投射进来的节点,包括跨层级嵌套的 - 多个同名
slot会合并内容,但顺序按 HTML 中出现位置,不是按 JS 插入顺序 - 若未提供任何
slot内容,影子 DOM 中对应位置为空白,不会 fallback 到默认文本(除非用<slot>默认内容</slot>)
兼容性与降级策略不能只靠 CSS
Edge 18 及更早版本、IE 完全不支持自定义元素和影子 DOM。仅用 @supports (display: contents) 或检测 customElements 是否存在还不够,因为部分安卓 WebView 会返回存在但功能异常。
真正可靠的降级是:检测失败时,让自定义标签退化为普通 <div>,并用外部 JS 补齐交互逻辑。例如,若 customElements.define 抛错,就执行一段轻量级事件委托代码来模拟切换行为。
- 不要在组件内用
try/catch包裹整个定义逻辑,捕获后不处理等于掩盖问题 - SSR 场景下,服务端无法运行影子 DOM,需提前判断环境,对服务端渲染输出扁平 HTML 结构
- Vue/React 项目中混用自定义元素时,注意框架可能劫持
slot行为,需用defineCustomElement(Vue 3.2+)或createCustomElement(React)桥接



















