Web Component 开发入门:创建可复用的自定义 HTML 元素
Web Component 允许你创建自定义的 HTML 元素,它们样式封装、行为独立、可在任何项目中使用——无论项目使用 React、Vue 还是原生 HTML。它是一组浏览器原生 API,不需要编译、不需要引入框架运行时,写出来的组件天然跨项目复用。
如果你维护过多个技术栈的项目,一定体会过这种痛苦:同一个"按钮"在 React 项目里是一个组件、在 Vue 项目里是另一个组件、在服务端渲染的页面里还得手写一遍 HTML。Web Component 的思路完全不同——组件就是浏览器的一部分,谁都能直接用。
核心技术
Web Component 由三项核心技术组成:
- Custom Elements — 定义新的 HTML 元素
- Shadow DOM — 样式和行为封装
- HTML Templates — 定义可复用的 HTML 结构
Custom Elements 的生命周期
自定义元素有四个标准的生命周期回调,理解它们才能写出健壮组件:
| 回调 | 触发时机 | 典型用途 |
|---|---|---|
connectedCallback() |
元素被插入文档 | 绑定事件、发起数据请求 |
disconnectedCallback() |
元素被移出文档 | 解绑事件、清理定时器 |
attributeChangedCallback() |
监听属性变化 | 响应属性更新重新渲染 |
adoptedCallback() |
元素被移动到新文档 | 少见,跨文档迁移时用 |
注意,connectedCallback() 可能被多次调用(比如元素在文档里被搬来搬去),所以事件绑定要在里面做、解绑要在 disconnectedCallback() 里做,否则会重复绑定。
创建一个 Web Component
// 自定义按钮组件
class MyButton extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
static get observedAttributes() {
return ['variant', 'disabled'];
}
connectedCallback() {
this.render();
}
attributeChangedCallback() {
this.render();
}
render() {
const variant = this.getAttribute('variant') || 'primary';
const disabled = this.hasAttribute('disabled') ? 'disabled' : '';
this.shadowRoot.innerHTML = `
<style>
button {
padding: 10px 20px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
}
button.primary {
background: #4F46E5;
color: white;
}
button.primary:hover {
background: #4338CA;
}
button.outline {
background: transparent;
border: 2px solid #4F46E5;
color: #4F46E5;
}
button[disabled] {
opacity: 0.6;
cursor: not-allowed;
}
</style>
<button class="${variant}" ${disabled}>
<slot></slot>
</button>
`;
// 事件处理
this.shadowRoot.querySelector('button')
.addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('my-click', {
detail: { id: this.id }
}));
});
}
}
// 注册自定义元素
customElements.define('my-button', MyButton);
使用自定义元素
<my-button variant="primary" id="submit-btn">提交</my-button>
<my-button variant="outline">取消</my-button>
<my-button disabled>已禁用</my-button>
<script>
document.querySelector('#submit-btn')
.addEventListener('my-click', (e) => {
console.log('Button clicked:', e.detail.id);
});
</script>
更复杂的例子:计数器组件
class Counter extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.count = 0;
}
connectedCallback() {
this.render();
this.shadowRoot.querySelector('#inc')
.addEventListener('click', () => this.update(1));
this.shadowRoot.querySelector('#dec')
.addEventListener('click', () => this.update(-1));
}
update(amount) {
this.count += amount;
this.shadowRoot.querySelector('#value').textContent = this.count;
this.dispatchEvent(new CustomEvent('count-change', {
detail: { count: this.count }
}));
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host { display: inline-flex; align-items: center; gap: 12px; }
button {
width: 36px; height: 36px;
border: 1px solid #ccc;
border-radius: 50%;
background: white;
cursor: pointer;
font-size: 18px;
}
span { min-width: 30px; text-align: center; font-size: 20px; }
</style>
<button id="dec">-</button>
<span id="value">0</span>
<button id="inc">+</button>
`;
}
}
customElements.define('my-counter', Counter);
<my-counter></my-counter>
<my-counter></my-counter>
Web Component 的优势
| 特性 | 说明 |
|---|---|
| 框架无关 | 在任何项目中都能使用 |
| 样式隔离 | Shadow DOM 确保样式不冲突 |
| 原生支持 | 无需编译、无需 polyfill(现代浏览器) |
| 可复用 | 一次编写,处处使用 |
| 封装 | HTML、CSS、JS 打包在一个文件中 |
一个设计系统场景
假设公司有三套产品:管理后台用 React,官网用 Vue,营销页是直接渲染的 HTML。过去要做统一按钮样式,就得在三个技术栈里各维护一份;现在把 <my-button>、<my-counter> 这些组件封装成 Web Component 发布到内部 npm 包,三个项目只需要引入脚本标签:
<script type="module" src="/components/my-button.js"></script>
<my-button variant="primary">提交订单</my-button>
改样式只改组件一处,全站同步更新;升级组件版本也只需改一个版本号。这正是 Web Component 在大型组织中流行的核心原因——组件不再是"某个框架的东西",而是团队共同的基础设施。
如果你所在团队正打算统一多端组件,不妨先拿一个高频组件(比如按钮、弹窗)试点:在三个项目里替换掉各自的实现,跑一两个迭代再决定要不要全面铺开。风险小,收益可衡量。
什么时候用 Web Component
- 多框架团队共享组件库:React、Vue、Angular 并存时,用 Web Component 做共享 UI 层,各框架都能直接
<my-card>一样使用; - 长期维护的组件:框架更新换代快,但浏览器 API 很稳定,投资在 Web Component 上的组件十年后依然可用;
- 渐进增强:在服务端渲染页面里嵌入交互组件,不需要整个页面重构成 SPA。
不太适合的场景:重度依赖框架生态的组件(比如需要访问 React Context)、需要服务端渲染 SEO 的整页内容——这时 Web Component 的"运行时渲染"反而是短板。
兼容性与最佳实践
现代浏览器(Chrome、Edge、Firefox、Safari)都原生支持 Web Component,无需 polyfill。但要注意:
- 用
customElements.define()注册后,同一个名字不能重复注册,组件库要注意命名冲突; - 大量同步渲染大量组件时,考虑用
connectedCallback里的异步或requestAnimationFrame分批更新,避免首屏卡顿; - 给组件加
observedAttributes时,记得同步更新attributeChangedCallback里的渲染逻辑,否则属性变了界面不变; - 表单类组件要注意
formAssociated与原生表单联动,否则提交时拿不到值。
常见问题
Web Component 支持 SEO 吗? 组件内部通过 Shadow DOM 渲染的内容,搜索引擎的抓取能力在不断完善,但关键正文建议同时放在组件标签内或使用 declarative shadow DOM 服务端输出,避免完全依赖运行时渲染。
能在老浏览器用吗? IE 已退出历史舞台;如果你仍需要支持旧版 Edge 或很老的内核,可以引入 @webcomponents/webcomponentsjs polyfill,但绝大多数现代站点已不需要。
和 Vue/React 的组件冲突吗? 不冲突。框架组件是"框架内部的语言",Web Component 是"浏览器原生的元素",两者可以嵌套混用,这也是很多团队做"框架无关 UI 库"的底气。
16IDC 观察
Web Component 是最佳的前端组件封装方式——不依赖任何框架,可以在任何项目中复用。对于设计系统(Design System)和 UI 组件库来说,Web Component 是比框架特定组件更长寿的选择。如果你的团队需要维护多个不同技术栈的项目,Web Component 是一次编写、处处运行的最佳实践。