原生自定义元素是最轻量解耦的前端组件化方案,但需严格遵循命名含连字符、区分autonomous与built-in扩展、dom操作延至connectedcallback、shadow dom手动透传slot/事件、ssr时提前注册并降级兜底等规范。

原生自定义元素(customElements.define)是当前最轻量、最解耦的 HTML 前端组件化方案,无需框架、不侵入构建流程,但集成时容易在命名、生命周期、DOM 访问时机上出错。
自定义元素命名必须含连字符,且不能用 is 属性混用
浏览器靠连字符区分原生标签和自定义标签:my-button 合法,mybutton 或 MyButton 会直接抛错 Failed to execute 'define' on 'CustomElementRegistry': The name must contain a dash。内置扩展(如继承 HTMLButtonElement)必须用 is 属性写法:<button is="loading-button"></button>,不能写成 <loading-button></loading-button> —— 后者是 autonomous 元素,两者注册方式、使用语法、继承链完全不同。
- autonomous 元素:继承
HTMLElement,独立实现全部行为,使用<my-card></my-card> - built-in 扩展:继承
HTMLButtonElement等,复用原生语义,必须带is属性 - 同一项目中混用两类时,需严格区分注册调用:前者传两个参数,后者必须加
{ extends: 'button' }第三个参数
constructor 里只能调 super() 和初始化状态,DOM 操作要等 connectedCallback
很多开发者在 constructor 中尝试读取 this.getAttribute('src') 或操作 this.innerHTML,结果取不到值或报错。因为此时元素尚未插入文档,属性可能未解析,shadow DOM 也未挂载。正确做法是:
- 在
constructor中只做最小初始化:调super()、声明私有字段(如this.count = 0)、绑定方法(this.handleClick = this.handleClick.bind(this)) - 所有 DOM 相关动作(创建 shadow root、渲染模板、添加事件监听、读取属性)统一移到
connectedCallback - 若需响应属性变更,需显式声明静态属性
static observedAttributes = ['value', 'disabled'],并在attributeChangedCallback中处理
Shadow DOM 是样式隔离关键,但需手动透传 slot 和事件
用 this.attachShadow({ mode: 'open' }) 创建影子根后,内部样式不会泄漏,外部样式也无法穿透——这是优点,也是坑点。常见疏漏包括:
- 忘记在 shadowRoot 中写
<slot></slot>,导致用户写的子内容(如<my-button>点击我</my-button>)完全不显示 - 内部按钮点击事件不往外冒泡,外部监听
click拿不到;需手动this.dispatchEvent(new Event('click', { bubbles: true })) - 想让外部 CSS 控制字体大小?得在 shadow 内部用
css vars接口,比如font-size: var(--my-button-font-size, 14px);,再由外部设置:host { --my-button-font-size: 16px; }
动态创建与 SSR 兼容性常被忽略
服务端渲染(SSR)或静态 HTML 预生成时,customElements.define 还没执行,浏览器会把 <user-card></user-card> 当作未知标签,降级为 HTMLUnknownElement,样式丢失、逻辑不触发。解决方案不是“等 JS 加载完再渲染”,而是:
- 在 HTML 文档开头就注册(如
<script>customElements.define(...)</script>放在或顶部) - 对关键组件做“降级兜底”:在
constructor中先设this.innerHTML = 'Loading...',避免白屏 - 避免依赖
document.currentScript动态加载资源——它在模块脚本中不可用,改用import('./my-element.js')+define配合if (!customElements.get('my-element'))守卫
真正难的不是写一个能跑的 my-input,而是在多团队协作、多环境部署、跨框架嵌入时,保证它的属性名不冲突、事件语义一致、样式边界清晰——这些细节不在 API 文档里,但在每次 connectedCallback 被跳过、每次 slot 消失、每次 SSR 页面闪动时,都会露出来。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











