Web Components:原生组件化开发新趋势
作者:一位维护过多个开源项目的前端工程师
写在前面:我当初学前端时,被 React、Vue 的生态包围,以为“组件化”只能靠框架实现。直到某天在阅读 W3C 标准时,才惊讶地发现——浏览器早已原生支持组件化!今天这篇教程,就是想带零基础的你,亲手用 纯 JavaScript 构建可复用的 Web 组件,不依赖任何框架。
一、什么是 Web Components?
Web Components 是一组浏览器原生支持的 API 集合,它让你能创建自定义 HTML 元素(比如 <my-button>、<user-card>),并封装其内部结构、样式和行为。这些元素就像 <div> 或 <input> 一样,可以在任何项目中直接使用。
为什么值得学习?
- 无需框架:不依赖 React、Vue,纯 JS 即可实现组件化
- 高度封装:内部 DOM 和样式与页面隔离(通过 Shadow DOM)
- 跨项目复用:写一次,用在任何支持现代浏览器的项目中
- 未来趋势:被 Google、Microsoft 等大厂用于构建 Design System(如 Shoelace、Lion)
📚 推荐书籍:《Web Components in Action》(英文)或《深入理解 Web Components》(中文)对原理有更系统讲解。
二、环境准备:5 分钟搭建开发环境
好消息是:你不需要安装任何构建工具! 只需一个现代浏览器(Chrome、Edge、Firefox 最新版)和一个代码编辑器(如 VS Code)。
步骤清单:
- 新建一个空文件夹,例如
web-components-demo - 在其中创建
index.html文件 - 打开浏览器,直接拖拽
index.html进去即可运行
💡 我当初学的时候,总以为要配 webpack、Babel 才能写组件。其实 Web Components 的最大优势之一,就是开箱即用!
三、核心概念:三大支柱
Web Components 由三个核心技术组成:
| 技术 | 作用 | 是否必须 |
|---|---|---|
| Custom Elements | 定义自定义 HTML 标签 | ✅ 必须 |
| Shadow DOM | 封装组件内部 DOM 和样式 | ✅ 必须 |
| HTML Templates | 声明可复用的 DOM 模板 | ❌ 可选(但推荐) |
下面逐个解释,并附上代码示例。
3.1 Custom Elements:定义你的专属标签
你想创建一个 <hello-world> 标签?只需两步:
步骤 1:定义一个类(继承 HTMLElement)
class HelloWorld extends HTMLElement {
constructor() {
super(); // 必须调用 super()
this.textContent = "Hello, Web Components!";
}
}
步骤 2:注册这个类到浏览器
customElements.define('hello-world', HelloWorld);
⚠️ 注意:自定义标签名必须包含连字符
-(如my-button),这是浏览器区分原生标签和自定义标签的规则。
完整示例(放入 index.html):
<!DOCTYPE html>
<html>
<head>
<title>Web Components 入门</title>
</head>
<body>
<hello-world></hello-world>
<script>
class HelloWorld extends HTMLElement {
constructor() {
super();
this.textContent = "Hello, Web Components!";
}
}
customElements.define('hello-world', HelloWorld);
</script>
</body>
</html>
打开浏览器,你会看到页面显示:Hello, Web Components!
3.2 Shadow DOM:真正的封装隔离
现在问题来了:如果我在页面里写 h1 { color: red; },会不会影响组件内部的标题?
不会! 因为 Shadow DOM 创建了一个独立的 DOM 树,与主文档完全隔离。
如何启用 Shadow DOM?
在 constructor 中调用 this.attachShadow({ mode: 'open' }):
class MyCard extends HTMLElement {
constructor() {
super();
// 创建 Shadow Root
const shadow = this.attachShadow({ mode: 'open' });
// 向 Shadow DOM 添加内容
shadow.innerHTML = `
<style>
h2 { color: blue; }
</style>
<h2>这是一个卡片</h2>
`;
}
}
customElements.define('my-card', MyCard);
现在,即使你在页面全局写 h2 { color: green !important; },卡片内的标题依然是蓝色。
🔒
mode: 'open'表示外部 JS 可以通过element.shadowRoot访问内部;'closed'则完全封闭(一般用'open'即可)。
3.3 HTML Templates:提升性能与可读性
上面我们用 innerHTML 拼接字符串,但内容复杂时容易出错。更好的方式是使用 <template> 标签。
使用模板的步骤:
- 在 HTML 中定义
<template> - 在 JS 中克隆模板内容,插入 Shadow DOM
<template id="my-card-template">
<style>
.card { border: 1px solid #ccc; padding: 16px; }
h3 { margin: 0; color: #333; }
</style>
<div class="card">
<h3>用户信息卡</h3>
<p>欢迎使用 Web Components!</p>
</div>
</template>
<script>
class UserCard extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
// 获取模板并克隆
const template = document.getElementById('my-card-template');
const instance = template.content.cloneNode(true);
shadow.appendChild(instance);
}
}
customElements.define('user-card', UserCard);
</script>
<user-card></user-card>
✅ 优势:模板只解析一次,多次使用时性能更高;HTML 结构清晰,便于维护。
四、实战项目:构建一个可复用的“计数器按钮”
现在,让我们整合所学知识,做一个真正有用的组件:点击按钮,数字递增。
功能需求:
- 显示当前计数值
- 点击按钮 +1
- 支持通过属性设置初始值(如
<counter-button start="5">)
第一步:编写 HTML 模板
<template id="counter-template">
<style>
button {
padding: 8px 16px;
font-size: 16px;
background: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #45a049;
}
span {
margin-left: 8px;
font-weight: bold;
}
</style>
<button>点击计数</button>
<span>0</span>
</template>
第二步:定义组件类
class CounterButton extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
// 克隆模板
const template = document.getElementById('counter-template');
const instance = template.content.cloneNode(true);
shadow.appendChild(instance);
// 获取内部元素
this.button = shadow.querySelector('button');
this.countSpan = shadow.querySelector('span');
// 初始化计数值
const start = parseInt(this.getAttribute('start') || '0', 10);
this.count = start;
this.countSpan.textContent = this.count;
// 绑定点击事件
this.button.addEventListener('click', () => {
this.count++;
this.countSpan.textContent = this.count;
});
}
}
customElements.define('counter-button', CounterButton);
第三步:在页面中使用
<counter-button></counter-button>
<br><br>
<counter-button start="10"></counter-button>
✅ 你会看到两个独立的计数器,互不影响!这就是组件化的威力。
💡 实战经验:永远在 Shadow DOM 内部绑定事件,避免污染全局。我早期犯过的错误就是在
document上监听,导致多个组件互相干扰。
五、新手常见问题解答(FAQ)
Q1:Web Components 能和 React/Vue 一起用吗?
完全可以! 你可以把 Web Component 当作普通 HTML 标签嵌入 React/Vue 项目。例如在 Vue 中:
<template>
<div>
<counter-button start="5"></counter-button>
</div>
</template>
注意:React 对自定义属性(props)的支持稍弱,需用
ref手动设置属性。
Q2:如何向组件传递数据?
主要有两种方式:
| 方式 | 适用场景 | 示例 |
|---|---|---|
| HTML 属性(attributes) | 传递简单字符串/数字 | <my-comp count="5"> |
| JavaScript 属性(properties) | 传递对象、函数等复杂数据 | compInstance.data = { name: 'Tom' } |
📌 属性(attribute)会反映在 HTML 上;属性(property)是 JS 对象的字段,不会显示在 DOM 中。
Q3:如何让组件响应属性变化?
使用 observedAttributes 静态方法 + attributeChangedCallback:
class DynamicTitle extends HTMLElement {
static get observedAttributes() {
return ['title']; // 监听 title 属性变化
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'title') {
this.shadowRoot.querySelector('h2').textContent = newValue;
}
}
}
这样,当 <dynamic-title title="新标题"></dynamic-title> 的 title 改变时,组件会自动更新。
Q4:浏览器兼容性如何?
| 浏览器 | 支持情况 |
|---|---|
| Chrome | ✅ 完全支持(v54+) |
| Firefox | ✅ 完全支持(v63+) |
| Safari | ✅ 完全支持(v10.1+) |
| Edge | ✅ 完全支持(Chromium 版) |
老旧浏览器(如 IE)不支持,但可通过 polyfill 降级(不推荐用于新项目)。
六、学习建议与下一步
6.1 避坑指南
- 不要忘记
super():在constructor第一行必须调用 - 标签名必须带
-:否则注册会失败 - 样式只作用于 Shadow DOM:想全局覆盖?需用 CSS 变量(
--primary-color) - 避免在 connectedCallback 做 heavy work:它可能被多次调用
6.2 推荐学习路径
- 动手改写现有 UI:把项目中的按钮、卡片改造成 Web Component
- 学习 CSS 变量:实现主题定制(如
:host { --color: red; }) - 探索 slots:实现内容分发(类似 Vue 的 slot)
- 阅读开源项目:如 Shoelace(纯 Web Components 组件库)
6.3 进阶资源
- 书籍:《Web Components in Action》(Manning 出版社)
- 官方文档:MDN Web Components Guide
- 工具库:Lit(简化 Web Components 开发,由 Google 维护)
结语
Web Components 不是“取代 React/Vue”的技术,而是补充前端生态的底层能力。它让你在不引入框架的情况下,也能写出高内聚、低耦合的代码。
我维护的几个开源项目,正是靠 Web Components 实现了“一次编写,多端复用”——既能在传统网站用,也能嵌入 Electron 或微前端架构中。
希望这篇教程能帮你迈出原生组件化开发的第一步。记住:最好的学习方式,就是立刻打开编辑器,敲下你的第一个 <my-first-component>!
Happy Coding! 🎉

评论 0