customElements.define 必须传入继承 HTMLElement 的类,不能传字符串;Shadow DOM 初始化必须在 constructor 中执行 attachShadow,属性监听需通过 static observedAttributes 声明并在 attributeChangedCallback 中手动同步,slot 仅透传 light DOM 内容。

customElements.define 只接受类,不接受 HTML 字符串
直接传字符串给 customElements.define 会报错 TypeError: Class constructor cannot be invoked without 'new'。这不是 API 设计疏漏,而是规范强制要求组件必须是可实例化的类——只有类才能响应生命周期、绑定属性、管理 Shadow DOM。
- 错误写法:
customElements.define('my-card', '<div>Hello</div>') - 正确路径:定义一个继承
HTMLElement的类,在constructor()中调用super()并创建shadowRoot,在connectedCallback()中渲染结构 - 模板建议用
<template id="my-card">预存,再通过document.getElementById('my-card').content.cloneNode(true)插入,避免重复解析字符串 - 若需动态注入后端返回的富文本 HTML,别用
shadowRoot.innerHTML = htmlStr——它会丢失事件绑定且不安全;改用DOMParser解析成DocumentFragment,再shadowRoot.appendChild()
Shadow DOM 初始化必须在 constructor 里完成
this.attachShadow({ mode: 'open' }) 必须在 constructor 中执行,否则后续所有样式隔离、slot 分发、querySelector 查找都会失效。常见错误是把它挪到 connectedCallback,此时元素已挂载,但 Shadow Root 尚未建立,导致样式泄漏或内容无法透传。
-
constructor是唯一能安全初始化 Shadow DOM 的时机;connectedCallback只负责“首次渲染”和“状态同步” - 别在
constructor里操作this.innerHTML或this.shadowRoot.innerHTML——前者绕过 Shadow DOM,后者此时shadowRoot还没创建(会报Cannot set property 'innerHTML' of null) - 如果组件依赖异步资源(如字体、图标),应在
connectedCallback中加载,并加防重逻辑,因为该钩子可能被多次触发(比如元素被移除又重新 append)
属性变更必须显式声明 observedAttributes
HTML 属性变化不会自动触发更新,必须靠 static get observedAttributes() 告诉浏览器哪些属性需要监听,再在 attributeChangedCallback 中手动同步。忽略这一步,my-card avatar="xxx" 改了也不会更新头像。
将调研报告、方案对比、数据分析、需求梳理、会议纪要、设计说明、代码评审、学习笔记等任务产物输出为可在浏览器直接打开的单文件 HTML,替代 Markdown 作为最终交付物。Markdown 在信息密度、视觉清晰度和可分享性上已跟不上 AI 产物的丰富度,HTML 能同时承载排版、表格、SVG 等。
- 只监听实际用到的属性,比如
['avatar', 'name', 'disabled'],不要写*或全量枚举,性能无益且易出错 -
attributeChangedCallback中注意区分oldValue和newValue类型:HTML 属性全是字符串,checked是"true"/null,不是布尔值 - 若属性值是 JSON 字符串(如
data='{"theme":"dark"}'),需在回调里JSON.parse(),并加 try/catch 防止解析失败中断流程 - 避免在该回调中直接操作
shadowRoot.innerHTML = ''清空再重写——这会销毁所有事件监听器;改用shadowRoot.replaceChildren(...)或逐个appendChild()
slot 内容透传只对 light DOM 有效
<slot> 只能分发用户写在组件标签内部的子节点(即 light DOM),它对 JavaScript 动态生成的内容或后端返回的 HTML 字符串完全无感。想让 CMS 返回的富文本进 <slot name="body">,得手动注入。
立即学习“前端免费学习笔记(深入)”;
- 典型场景:
<my-card><div slot="body">这里是用户写的 HTML</div></my-card>→ slot 自动生效;但fetch('/api/content').then(html => ...)得自己塞进shadowRoot.querySelector('[slot="body"]').appendChild(...) - 若要支持 HTML 字符串注入,推荐用
DOMParser:const frag = new DOMParser().parseFromString(htmlStr, 'text/html').body.children[0],再shadowRoot.querySelector('[slot="body"]').appendChild(frag) - 注意:slot 元素本身不参与渲染,它只是“出口”,真正渲染的是它包裹的内容;所以不要给
<slot>加样式,而应作用于其父容器或使用::slotted(*) - 如果组件内部有多个
<slot>,记得每个都设name,否则默认 slot 会吞掉所有未命名内容,造成意外交互
:host 或 ::slotted 显式控制。封装成本比 Lit 或 Vue 高,但换来的是零构建依赖、纯浏览器运行、以及真正的 DOM 语义一致性。


















