Web Components:前端原生组件化,你真的了解吗?
大家好,我是掘金上常写入门教程的全栈工程师。最近在带实习生时,发现很多人一提到“组件化”,第一反应就是 Vue、React 或 Angular。其实,在这些框架之前,浏览器早就悄悄支持了一种无需任何框架就能实现组件化的技术——Web Components。
我当初学的时候也误以为它只是“冷门实验性 API”,但后来在做微前端和跨框架复用组件时,才发现它的强大之处。今天这篇技术分享,就带你从零开始,用最简单的方式掌握 Web Components 的核心用法,并完成一个可运行的小项目。全程只用原生 JavaScript,不依赖任何构建工具!
为什么 Web Components 值得关注?
Web Components 是一套由 W3C 标准定义的浏览器原生 API,它让你能像使用 <div> 一样,自定义自己的 HTML 标签(比如 <my-button>),并封装内部结构、样式和逻辑。
它的三大核心技术是:
- Custom Elements(自定义元素):定义新标签
- Shadow DOM(影子 DOM):实现样式和 DOM 隔离
- HTML Templates(模板):声明可复用的 HTML 片段
✅ 优势:
- 无需框架,浏览器原生支持
- 真正的样式隔离(比 CSS Modules 更彻底)
- 跨框架复用(Vue/React/Angular 都能直接用)
- 轻量、无依赖,适合微前端或 UI 库开发
❌ 局限:
- IE 不支持(但现代项目基本已放弃 IE)
- 生态工具链不如 React/Vue 成熟
- 学习曲线略陡(尤其 Shadow DOM)
不过别担心,我会用最直白的方式带你上手。
开发环境准备(超简单!)
Web Components 是浏览器原生能力,所以你只需要:
- 一个现代浏览器(Chrome / Edge / Firefox / Safari)
- 任意代码编辑器(VS Code、Sublime、甚至记事本都行)
- 一个本地服务器(避免 file:// 协议下的 CORS 问题)
快速启动本地服务(3 种方式)
| 方法 | 命令 | 说明 |
|---|---|---|
| Python 3 | python -m http.server 8080 |
最快,适合临时测试 |
| Node.js (http-server) | npx http-server |
需安装 Node,但功能更强 |
| VS Code 插件 | 安装 "Live Server" 插件 | 点击右键 → Open with Live Server |
💡 新手建议:直接用 VS Code + Live Server,一键启动,自动刷新。
创建一个空文件夹,新建 index.html,我们就在这里开始编码!
核心概念详解(配代码示例)
第一步:定义你的第一个自定义元素
我们先不谈 Shadow DOM,先用最简单的 Custom Elements 创建一个组件。
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Web Components 入门</title>
</head>
<body>
<!-- 使用自定义标签 -->
<hello-world name="小明"></hello-world>
<script>
// 定义组件类
class HelloWorld extends HTMLElement {
constructor() {
super(); // 必须调用 super()
const name = this.getAttribute('name') || '陌生人';
this.innerHTML = `<p>你好,${name}!</p>`;
}
}
// 注册自定义元素
customElements.define('hello-world', HelloWorld);
</script>
</body>
</html>
打开浏览器,你会看到:你好,小明!
✅ 关键点:
- 组件类必须继承
HTMLElement - 通过
this.getAttribute()获取属性 - 用
customElements.define(tagName, Class)注册 - 标签名必须包含连字符
-(如my-button,不能是button)
⚠️ 常见错误:忘记
super()或标签名不符合规范,浏览器会报错。
第二步:加入 Shadow DOM 实现样式隔离
上面的例子有个问题:如果页面有全局 p { color: red; },我们的组件文字也会变红。为了解决这个问题,引入 Shadow DOM。
class HelloWorld extends HTMLElement {
constructor() {
super();
const name = this.getAttribute('name') || '陌生人';
// 创建 Shadow Root
const shadow = this.attachShadow({ mode: 'open' });
// 定义内部 HTML 和样式
shadow.innerHTML = `
<style>
p {
color: blue;
font-weight: bold;
background: #f0f0f0;
padding: 8px;
border-radius: 4px;
}
</style>
<p>你好,${name}!</p>
`;
}
}
customElements.define('hello-world', HelloWorld);
现在,无论页面全局样式如何,组件内的 <p> 永远是蓝色背景!
🔍
mode: 'open'表示可以通过 JS 访问 shadow root(如element.shadowRoot)。若设为'closed',则完全封闭,外部无法访问内部 DOM。
第三步:使用 <template> 提升性能
每次创建组件都拼接字符串效率低,且不利于维护。我们可以用 <template> 预定义结构。
<!-- 在 body 外定义模板 -->
<template id="hello-world-template">
<style>
p {
color: green;
font-family: monospace;
}
</style>
<p>你好,<span id="name"></span>!</p>
</template>
<script>
class HelloWorld extends HTMLElement {
constructor() {
super();
const template = document.getElementById('hello-world-template');
const shadow = this.attachShadow({ mode: 'open' });
// 克隆模板内容(注意:要 deep clone)
shadow.appendChild(template.content.cloneNode(true));
// 动态设置名字
const nameSpan = shadow.getElementById('name');
nameSpan.textContent = this.getAttribute('name') || '朋友';
}
}
customElements.define('hello-world', HelloWorld);
</script>
💡 为什么用
cloneNode(true)?
因为<template>的内容是“惰性”的,不会渲染。我们必须克隆一份副本插入到 Shadow DOM 中才能生效。
实战:做一个可交互的计数器组件
现在,我们综合运用以上知识,做一个 <counter-box> 组件,包含:
- 显示当前数字
- “+1” 和 “-1” 按钮
- 支持初始值属性
步骤 1:编写 HTML 模板
<template id="counter-template">
<style>
.counter {
display: inline-block;
padding: 10px;
border: 1px solid #ccc;
border-radius: 6px;
font-size: 18px;
}
button {
margin: 0 5px;
padding: 4px 8px;
cursor: pointer;
}
</style>
<div class="counter">
<button id="decrease">-</button>
<span id="value">0</span>
<button id="increase">+</button>
</div>
</template>
步骤 2:定义组件类
class CounterBox extends HTMLElement {
constructor() {
super();
const template = document.getElementById('counter-template');
this._shadow = this.attachShadow({ mode: 'open' });
this._shadow.appendChild(template.content.cloneNode(true));
// 获取 DOM 引用
this._valueEl = this._shadow.getElementById('value');
this._increaseBtn = this._shadow.getElementById('increase');
this._decreaseBtn = this._shadow.getElementById('decrease');
// 初始化计数值
this._count = parseInt(this.getAttribute('initial') || '0', 10);
this._updateValue();
// 绑定事件
this._increaseBtn.addEventListener('click', () => {
this._count++;
this._updateValue();
});
this._decreaseBtn.addEventListener('click', () => {
this._count--;
this._updateValue();
});
}
_updateValue() {
this._valueEl.textContent = this._count;
}
}
customElements.define('counter-box', CounterBox);
步骤 3:在页面中使用
<body>
<h2>我的计数器</h2>
<counter-box initial="5"></counter-box>
<counter-box initial="-2"></counter-box>
</body>
刷新页面,你会看到两个独立的计数器,互不影响!这就是组件化的魅力。
🎯 进阶思考:
如果你想让外部知道计数值变化(比如父组件监听),可以用dispatchEvent触发自定义事件。这属于“组件通信”范畴,后续可深入。
新手常见问题 & 解决方案
| 问题 | 原因 | 解决方法 |
|---|---|---|
| 自定义标签不显示 | 忘记注册或标签名不合法 | 检查 customElements.define() 和是否含 - |
| 样式没生效 | 未使用 Shadow DOM 或写在全局 | 把样式写进 Shadow DOM 内部 |
| 事件无法触发 | 事件绑定时机错误 | 在 constructor 或 connectedCallback 中绑定 |
| 属性更新无效 | 没监听属性变化 | 实现 static get observedAttributes() 和 attributeChangedCallback() |
| 模板找不到 | <template> 放在 <body> 内且 JS 执行太早 |
把 JS 放在 </body> 前,或用 DOMContentLoaded |
如何监听属性变化?
比如用户动态修改 <counter-box initial="10">,组件应响应更新:
class CounterBox extends HTMLElement {
// 声明需要监听的属性
static get observedAttributes() {
return ['initial'];
}
// 属性变化时触发
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'initial' && oldValue !== newValue) {
this._count = parseInt(newValue, 10);
this._updateValue();
}
}
// ...其余代码不变
}
下一步学习建议
Web Components 虽然原生,但实际项目中常配合工具使用:
Lit(推荐!)
Google 出品的轻量库,简化 Web Components 开发,支持响应式、模板语法等。GitHub 地址:https://github.com/lit/litStencil
由 Ionic 团队开发,可编译 Web Components 为高性能组件,并支持 SSR。与主流框架集成
- React:用
React.createElement包装 Web Component - Vue:直接当作普通标签使用(需注意属性命名转换)
- React:用
深入 Shadow DOM
了解 slot(插槽)、CSS 自定义属性(--var)穿透等高级特性。
结语
Web Components 不是取代 React/Vue 的方案,而是一种底层能力。掌握它,能让你更深入理解“组件化”的本质,也能在需要跨技术栈复用 UI 时游刃有余。
我建议所有前端开发者至少了解其基本用法。毕竟,这是浏览器给我们的“标准答案”。
📌 最后提醒:
别被“原生”吓到——它只是 JavaScript + HTML 的组合。多写几遍,你就会发现:原来组件化,本可以如此简单。
如果你觉得这篇技术分享对你有帮助,欢迎去我的 GitHub 主页(搜索用户名即可)看看其他入门教程。也欢迎在评论区留言交流!
Happy Coding!

评论 0