自定义元素要真正降低耦合,必须用 customelements.define() 收口通信契约、attributechangedcallback 响应属性、attachshadow 隔离样式、css 自定义属性定制主题、slot 分发内容,并统一注册时机与命名规范。

自定义元素不是“多写点 JS 就能封装”的玩具,它要真正降低耦合,必须放弃 class + data-* 的旧习惯,转而用 customElements.define() 强制收口通信契约——否则只是把耦合从 CSS 类名挪到 JS 变量名上而已。
自定义元素必须用 attributeChangedCallback 响应属性,而不是读 dataset
很多人把 data-* 当成“传参通道”,结果在组件里反复调用 el.getAttribute('data-id') 或 el.dataset.id。这看似解耦,实则埋雷:属性变更不会自动触发重渲染,JS 逻辑得自己监听、自己 diff、自己更新 DOM。
-
attributeChangedCallback是浏览器原生提供的响应式入口,只有把它加进observedAttributes列表的属性变更才会触发 - 属性值始终是字符串,别传对象或函数:
<user-card user-id="123"></user-card>✅,<user-card config='{"theme":"dark"}'></user-card>❌ - 初始化时属性已存在,但
constructor里拿不到this.shadowRoot,也别在这儿操作 DOM;等connectedCallback再挂载内容
Shadow DOM 不是默认开启的,不手动 attach 就等于没隔离
只定义类、调用 customElements.define(),不创建 shadowRoot,那组件样式和事件照样会泄漏、被外部 CSS 覆盖、被全局事件监听器捕获——和一堆 <div class="card"> 没区别。<ul>
<li>必须在 <code>constructor 或 connectedCallback 中显式调用 this.attachShadow({ mode: 'open' })
this.shadowRoot 下,不能直接操作 this(它还是 light DOM 节点):host { --user-card-bg: #f8f9fa; },别写 :host(.dark) .name 这种穿透式状态类slot 是唯一合法的内容分发机制,别用 innerHTML 或 template.innerHTML 注入
用 this.innerHTML = '<h3>' + title + '</h3>' 或 template.content.cloneNode(true) 动态塞内容,等于把父级结构硬编码进组件内部,下游开发者无法替换标题文案、无法加图标、无法控制语义层级。
生成Claude风格的精美单页HTML汇报文件。当用户需要生成"汇报"、"周报"、"月报"、"项目进度"、"复盘"、"演示"、"slide deck"、"状态报告"、"工作总结"时触发。支持6种模板:周报(weekly)、项目进度(project)、月度总结(monthly)、复盘报告(postmortem)、演示文稿(slid
- 组件模板里只写
<slot name="title"></slot>和<slot></slot>,把“挖洞权”完全交给使用者 - 父级使用时必须用
slot="title"显式声明归属,否则内容不会被分发:<user-card><span slot="title">张三</span></user-card> - slot 内容变更会触发
slotchange事件,可用于响应式更新,但它不冒泡,监听必须挂在this.shadowRoot.querySelector('slot')上
注册时机和标签命名必须统一,否则团队协作立刻失效
一个项目里出现 <date-picker></date-picker>、<datepicker></datepicker>、<my-date-picker></my-date-picker> 三种写法,或者有人在模块顶部注册、有人在页面末尾注册、有人用动态 import 延迟注册——这些都会导致组件未定义错误、SSR 渲染失败、测试环境 inconsistent。
- 所有自定义元素必须在应用启动早期(如入口 JS 第一行)同步注册,避免条件判断或异步加载
- 标签名必须全小写、含短横线(hyphenated),且与类名严格对应:
class DatePicker extends HTMLElement→customElements.define('date-picker', DatePicker) - 禁止在组件内部调用
customElements.define(),注册逻辑必须外置,由构建工具或主应用统一管理
最常被忽略的一点:自定义元素的“低耦合”不来自技术本身,而来自团队是否愿意放弃自由发挥——比如坚持用 <div class="btn primary"> 而不用 <code><action-button variant="primary"></action-button>,哪怕后者需要多写几行定义代码。一旦开始妥协,耦合就从 DOM 层悄悄爬回 JS 层。










