Web Components:原生组件化开发新趋势
作者:一位开源项目维护者,写过几十份技术文档,也带过不少零基础学员。今天,我想用最平实的语言,带你走进 Web Components 的世界——一个被低估却极其强大的前端原生能力。
为什么我要写这篇教程?
几年前,我刚开始接触前端框架时,被 Vue、React 的“组件化”概念震撼得不行:原来页面可以像搭积木一样拼起来!但后来我才意识到——其实浏览器早就支持组件化了,只是我们太依赖框架,忘了原生的力量。
如今,越来越多的大型项目(比如 GitHub、Salesforce)开始在生产环境中使用 Web Components。它不依赖任何框架,是浏览器原生支持的标准,这意味着:你写的组件,未来十年都不会过时。
如果你刚入门 JavaScript,或者想理解“组件化”到底是什么,那么这篇《实践导向》的教程就是为你量身打造的。我会从零开始,手把手教你写出第一个 Web Component,并解释每一步背后的逻辑。
一、Web Components 是什么?能用来做什么?
简单说:Web Components 是一套浏览器原生的 API,让你能创建可复用、封装良好的自定义 HTML 元素。
想象一下,你想在网页里放一个“用户卡片”,传统做法可能是这样:
<div class="user-card">
<img src="avatar.jpg" />
<h3>Alice</h3>
<p>Frontend Developer</p>
</div>
但如果你有 100 个用户,就要复制粘贴 100 次。而且样式、行为都散落在 CSS 和 JS 文件里,很难维护。
而用 Web Components,你可以定义一个 <user-card> 标签:
<user-card name="Alice" role="Frontend Developer" avatar="avatar.jpg"></user-card>
这个标签:
- 自带样式(不会被外部 CSS 干扰)
- 自带交互逻辑(比如点击弹出详情)
- 可以在任何项目中直接使用(甚至不用 React/Vue)
这就是组件化的魅力:高内聚、低耦合、一次编写,到处使用。
二、环境准备:5 分钟搭建开发环境
好消息是:你不需要安装任何工具!Web Components 是浏览器原生支持的功能,现代浏览器(Chrome、Firefox、Edge、Safari)都已全面支持。
步骤 1:创建项目目录
打开终端(或命令提示符),执行:
mkdir my-web-components
cd my-web-components
touch index.html
步骤 2:编写基础 HTML
用任意文本编辑器(VS Code、记事本都行)打开 index.html,输入:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>我的第一个 Web Component</title>
</head>
<body>
<h1>欢迎来到 Web Components 世界!</h1>
<!-- 我们的组件将在这里渲染 -->
<script>
// JavaScript 代码将写在这里
</script>
</body>
</html>
💡 小贴士:我当初学的时候,总以为要装一堆工具才能开始。其实对于 Web Components,一个
.html文件就够了!先跑起来,再优化。
三、核心概念:三大支柱
Web Components 由三个核心技术组成:
| 技术 | 作用 | 类比 |
|---|---|---|
| Custom Elements(自定义元素) | 定义新 HTML 标签 | 就像发明一个新单词 |
| Shadow DOM(影子 DOM) | 封装样式和结构,隔离外部影响 | 给组件加个“玻璃罩子” |
| HTML Templates(模板) | 声明可复用的 HTML 片段 | 提前准备好“零件图纸” |
下面我们逐个击破。
3.1 Custom Elements:定义你的第一个组件
我们要创建一个叫 <hello-world> 的组件。
步骤 1:定义组件类
在 <script> 标签中添加:
class HelloWorld extends HTMLElement {
constructor() {
super(); // 必须调用 super()
this.innerHTML = '<p>Hello, 代码人生!</p>';
}
}
HelloWorld是一个普通的 JavaScript 类- 它继承自
HTMLElement(所有 HTML 元素的基类) constructor是构造函数,组件创建时自动运行
步骤 2:注册组件
customElements.define('hello-world', HelloWorld);
customElements.define()是浏览器提供的 API- 第一个参数是标签名(必须包含短横线
-) - 第二个参数是对应的类
步骤 3:在 HTML 中使用
在 <body> 中添加:
<hello-world></hello-world>
完整代码如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Web Components 入门</title>
</head>
<body>
<hello-world></hello-world>
<script>
class HelloWorld extends HTMLElement {
constructor() {
super();
this.innerHTML = '<p>Hello, 代码人生!</p>';
}
}
customElements.define('hello-world', HelloWorld);
</script>
</body>
</html>
保存文件,用浏览器打开 index.html,你会看到:
Hello, 代码人生!
🎉 恭喜!你已经写出了第一个 Web Component!
⚠️ 常见错误:标签名必须包含短横线(如
my-button),不能是单个单词(如button),这是浏览器强制规定的。
3.2 Shadow DOM:实现样式隔离
现在有个问题:如果我在外面写 CSS:
<style>
p { color: red; }
</style>
你会发现 <hello-world> 里的文字也变红了!这违背了“组件封装”的初衷。
解决方案:使用 Shadow DOM。
修改 constructor:
constructor() {
super();
// 创建 Shadow Root
const shadow = this.attachShadow({ mode: 'open' });
// 在 Shadow DOM 中添加内容
shadow.innerHTML = `
<style>
p {
color: blue;
font-weight: bold;
}
</style>
<p>Hello, 代码人生!</p>
`;
}
关键点:
this.attachShadow({ mode: 'open' })创建一个 Shadow Root- 所有内容(包括
<style>)都放在shadow.innerHTML里 - 外部 CSS 无法影响 Shadow DOM 内部
现在即使外部有 p { color: red },组件内的文字依然是蓝色。
💡 mode: 'open' vs 'closed':
open:允许通过 JavaScript 访问 Shadow DOM(element.shadowRoot)closed:完全封闭,外部无法访问(一般用open即可)
3.3 HTML Templates:复用结构
如果组件结构复杂,把 HTML 写在字符串里会很乱。这时可以用 <template>。
在 <body> 开头添加:
<template id="hello-template">
<style>
p { color: green; font-size: 18px; }
</style>
<p>Hello from template!</p>
</template>
然后在 JS 中使用:
class HelloWorld extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
// 获取模板
const template = document.getElementById('hello-template');
// 克隆模板内容(深拷贝)
const instance = template.content.cloneNode(true);
// 添加到 Shadow DOM
shadow.appendChild(instance);
}
}
这样,HTML 结构就和 JS 逻辑分离了,更清晰。
四、实战项目:做一个可配置的用户卡片组件
现在,我们来做一个实用的 <user-card> 组件,支持传入 name、role、avatar 属性。
步骤 1:定义 HTML 结构
在 <body> 中使用组件:
<user-card
name="张三"
role="JavaScript 工程师"
avatar="https://via.placeholder.com/60"
></user-card>
步骤 2:编写组件类
class UserCard extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
// 获取属性
const name = this.getAttribute('name') || '匿名用户';
const role = this.getAttribute('role') || '未知角色';
const avatar = this.getAttribute('avatar') || 'https://via.placeholder.com/60?text=No+Avatar';
shadow.innerHTML = `
<style>
.card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 16px;
max-width: 300px;
font-family: Arial, sans-serif;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.avatar {
width: 60px;
height: 60px;
border-radius: 50%;
margin-right: 12px;
}
.info {
display: flex;
align-items: center;
}
h3 { margin: 0; font-size: 18px; }
p { margin: 4px 0 0; color: #666; }
</style>
<div class="card">
<div class="info">
<img class="avatar" src="${avatar}" alt="${name}" />
<div>
<h3>${name}</h3>
<p>${role}</p>
</div>
</div>
</div>
`;
}
}
customElements.define('user-card', UserCard);
步骤 3:测试多个实例
在 HTML 中添加多个卡片:
<user-card
name="李四"
role="前端讲师"
avatar="https://via.placeholder.com/60?text=L"
></user-card>
<user-card
name="王五"
role="开源贡献者"
></user-card>
你会看到三个风格一致、互不干扰的用户卡片。
✅ 关键知识点:
- 使用
this.getAttribute('xxx')获取 HTML 属性- 提供默认值(
|| '默认值')避免空值- 所有样式都封装在 Shadow DOM 中,不会污染全局
五、新手常见问题解答(FAQ)
Q1:Web Components 和 React/Vue 组件有什么区别?
| 对比项 | Web Components | React/Vue 组件 |
|---|---|---|
| 依赖 | 无(浏览器原生) | 需要框架运行时 |
| 学习成本 | 仅需 HTML/CSS/JS | 需学习框架语法 |
| 性能 | 轻量,无虚拟 DOM | 有额外开销 |
| 生态 | 较小 | 丰富 |
| 跨框架使用 | ✅ 可在任何框架中使用 | ❌ 通常绑定特定框架 |
💡 建议:如果你做微前端、UI 库、或希望组件长期可用,Web Components 是绝佳选择。
Q2:如何响应属性变化?
目前我们的组件只在初始化时读取属性。如果后续动态修改属性(如 card.setAttribute('name', '新名字')),界面不会更新。
解决方案:使用 observedAttributes + attributeChangedCallback
class UserCard extends HTMLElement {
// 声明需要监听的属性
static get observedAttributes() {
return ['name', 'role', 'avatar'];
}
// 属性变化时触发
attributeChangedCallback(name, oldValue, newValue) {
if (oldValue !== newValue) {
this.render(); // 重新渲染
}
}
constructor() {
super();
this.attachShadow({ mode: ' open' });
this.render(); // 初始渲染
}
render() {
const name = this.getAttribute('name') || '匿名用户';
const role = this.getAttribute('role') || '未知角色';
const avatar = this.getAttribute('avatar') || 'https://via.placeholder.com/60?text=No+Avatar';
this.shadowRoot.innerHTML = `
<!-- 同上,略 -->
<h3>${name}</h3>
<p>${role}</p>
<!-- ... -->
`;
}
}
现在,动态修改属性也能实时更新 UI 了!
Q3:能传递复杂数据(如对象、数组)吗?
HTML 属性只能是字符串。如果要传对象,有三种方案:
传 JSON 字符串(简单场景):
<my-component data='{"name":"Alice","age":25}'></my-component>JS 中用
JSON.parse(this.getAttribute('data'))解析。通过 JavaScript 设置属性(推荐):
const comp = document.querySelector('my-component'); comp.userData = { name: 'Alice', age: 25 };使用事件通信(高级用法,后续可学)
六、学习建议与下一步
✅ 你已经掌握了:
- 创建自定义元素
- 使用 Shadow DOM 隔离样式
- 通过属性传递数据
- 响应属性变化
🔜 下一步可以学:
- 生命周期方法:
connectedCallback(插入 DOM 时)、disconnectedCallback(移除时) - 插槽(Slots):让组件支持嵌套内容,如
<my-modal><p>这里是内容</p></my-modal> - 与框架集成:在 React/Vue 中使用 Web Components
- 构建工具:用 Lit(轻量库)简化开发
🚫 避坑指南:
- 不要用 Web Components 做整个应用(它适合 UI 组件,不适合状态管理)
- 避免在
constructor中操作 DOM(此时元素还未挂载) - 移动端兼容性良好,但 IE 不支持(需 polyfill)
结语:代码人生,从原生开始
我当初学前端时,沉迷于各种框架的“魔法”,却忽略了浏览器本身的能力。直到后来维护一个跨团队项目,才体会到 Web Components 的价值——无需协商技术栈,一个 <date-picker> 标签,所有人都能用。
Web Components 不是银弹,但它代表了一种理念:尊重标准,拥抱原生,写可持续的代码。
希望这篇教程能成为你“代码人生”中的一块基石。记住:最好的学习方式,就是动手写一个组件,然后不断改进它。
现在,打开你的编辑器,写下你的第一个 <my-first-component> 吧!

评论 0