一. 定义
什么是Web Components?
Web Components 是一组用于创建可重用、封装的自定义 HTML 元素的标准技术,由 W3C 制定并维护,是现代 Web 开发中构建组件化 UI 的原生方式。它允许开发者创建类似 <my-button>
这样的自定义标签,这些标签拥有自己的样式和行为,而且可以在任何支持 Web Components 的项目中复用,无需依赖任何框架(如 React、Vue 等) 。
Web Components 包含三个主要技术:
二.详细解释组成
1. Custom Elements(自定义元素)
允许开发者定义自己的 HTML 元素,比如 <my-card>
,并控制它的创建、属性、事件等生命周期。
关键点:
- 通过继承
HTMLElement
类来定义新元素。 - 使用
customElements.define('my-element', MyElementClass)
注册自定义元素。
- 第一个参数为所创建元素的名称 (注:为了和原生的元素区分开,元素的名称不能是单个单词,且其中必须要有短横线,eg: user-card);
- 第二个参数为定义元素行为的类;
- 第三个参数为可选参数,是一个包含extends属性的配置对象,它指定所创建的元素继承自哪个内置元素,可以继承任何内置元素
- 支持生命周期回调:
constructor()
→ 创建元素实例时调用connectedCallback()
→ 元素被插入到 DOM 中时调用 (如果元素的某些属性后来被修改,比如通过 JS 设置element.setAttribute('disabled', 'true')
)attributeChangedCallback()
→ 属性变化时调用(如果元素从 DOM 中被移除,比如element.remove()
)disconnectedCallback()
→ 元素被移除时调用(如果元素被移动到另一个文档,比如跨 iframe)adoptedCallback()
→ 很少用到
简单例子:
javascript
class FancyDrawer extends AppDrawer {
constructor() {
super(); // always call super() first in the constructor. This also calls the extended class' constructor.
...
}
toggleDrawer() {
// Possibly different toggle implementation?
// Use ES2015 if you need to call the parent method.
// super.toggleDrawer()
}
anotherMethod() {
...
}
}
customElements.define('fancy-app-drawer', FancyDrawer);
在 HTML 中使用:
html
<fancy-app-drawer></fancy-app-drawer>
2. Shadow DOM(影子 DOM)
它提供了一种将组件的内部结构、样式和行为进行封装 的机制,使得组件的内部 DOM 树与外部完全隔离,不会与页面的其他部分发生样式或 DOM 结构上的冲突
关键点:
- 使用
this.attachShadow({ mode: 'open' })
创建 Shadow DOM。 - Shadow DOM 内部的样式不会泄漏到外部,外部的样式也不会影响到内部(除非特意设计)。
参数 mode
有两种选项:
模式 | 说明 |
---|---|
'open' |
Shadow DOM 可以通过 element.shadowRoot 从外部访问(推荐用于自定义元素) |
'closed' |
Shadow DOM 无法从外部访问(element.shadowRoot 返回 null ,更封闭) |
例子:
javascript
class ShadowElement extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
p { color: red; }
</style>
<p>这段文字是红色的,且样式不会影响外部!</p>
`;
}
}
customElements.define('shadow-element', ShadowElement);
3. HTML Templates(HTML 模板)
通过 <template>
和 <slot>
标签,可以定义可复用的 HTML 片段,不会在页面加载时渲染,但可以在运行时插入到 Shadow DOM 中。
关键点:
<template>
标签用于声明不会立即渲染的 HTML。<slot>
允许外部内容"注入"到组件中,类似于插槽(Vue 中的 slot / React 中的 children)。
例子:
html
<template id="user-card">
<style>
.card { border: 1px solid #ccc; padding: 16px; }
</style>
<div class="card">
<h2><slot name="name">默认名称</slot></h2>
<p><slot name="email">默认邮箱</slot></p>
</div>
</template>
<script>
class UserCard extends HTMLElement {
constructor() {
super();
const template = document.getElementById('user-card');
const content = template.content.cloneNode(true);
this.attachShadow({ mode: 'open' }).appendChild(content);
}
}
customElements.define('user-card', UserCard);
</script>
<!-- 使用自定义组件 -->
<user-card>
<span slot="name">张三</span>
<span slot="email">zhangsan@example.com</span>
</user-card>
三.一些进阶技巧
1.样式(CSS)的封装与控制
默认基本特性:
- Shadow DOM 内部的 CSS 样式默认不会泄漏到外部;
- 外部的 CSS 样式默认也不会影响到 Shadow DOM 内部;
- 这种隔离是 自动的、原生的,是 Shadow DOM 最重要的优势之一。
什么是 :host
?
- 在 Shadow DOM 的
<style>
中,:host
表示 当前自定义元素本身(即宿主元素) 。 - 它用于设置 该 Web Component 在页面中的外观,比如宽高、边距、布局等。
✅ 示例:
javascript
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
padding: 20px;
border: 2px solid #999;
border-radius: 10px;
background: #f9f9f9;
}
:host([disabled]) {
opacity: 0.6;
pointer-events: none;
}
p {
color: green;
}
</style>
<p>我是组件内容</p>
`;
使用方式:
html
<my-element></my-element> <!-- 正常显示 -->
<my-element disabled></my-element> <!-- 应用了 :host([disabled]) 样式 -->
🔍 说明:
:host
控制的是<my-element>
这个宿主元素自身的样式;:host([disabled])
是属性选择器,当你的组件使用了disabled
属性时,可以改变其外观;
2:使用 CSS 变量实现"样式定制"
这是非常常用的一种模式,可以让使用者从 外部传入样式变量,从而定制组件内部的样式,实现一定程度的样式开放。
✅ 示例:通过 CSS 变量定制颜色
javascript
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
padding: 20px;
border: 1px solid var(--border-color, #ccc); /* 默认灰色,可被外部覆盖 */
border-radius: 8px;
background: var(--bg-color, #fff); /* 默认白色 */
}
p {
color: var(--text-color, #333);
}
</style>
<p>我支持外部通过 CSS 变量定制样式</p>
`;
外部使用:
html
<my-styled-element style="
--border-color: red;
--bg-color: #f0f8ff;
--text-color: darkblue;
"></my-styled-element>
🔧 好处:
- 组件内部样式仍然被封装保护;
- 但允许外部通过 CSS 自定义属性(变量) 来调整部分样式;
- 非常灵活,是构建可定制组件库的推荐方式 ✅。
3:使用 ::slotted()
为插槽内容设置样式
如果你在 Shadow DOM 中使用了 <slot>
(内容分发),那么默认情况下:
外部传入到插槽的内容,它的样式不受 Shadow DOM 内部样式的影响,反之亦然。
但你可以使用 ::slotted()
伪元素选择器,来为插槽中的内容设置样式!
✅ 示例:
javascript
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
::slotted(p) {
color: purple;
font-weight: bold;
}
::slotted(h3) {
color: orange;
}
</style>
<div>
<slot></slot> <!-- 外部传入的内容会在这里显示 -->
</div>
`;
使用方式:
html
<my-slotted-component>
<p>这个段落会被 ::slotted(p) 样式影响,变成紫色粗体</p>
<h3>这个标题会被 ::slotted(h3) 影响,变成橙色</h3>
</my-slotted-component>
🔍 说明:
::slotted(p)
只作用于 插入到插槽中的<p>
元素;- 但注意:你不能深度控制插槽内容的内部结构 ,比如
::slotted(div p)
是不支持的; - 它只是给插槽的直接子元素设置样式 🎯。
4:使用 ::part
实现"受控的样式穿透"
这是 Shadow DOM 提供的一种官方机制,允许外部页面有选择地、可控地为 Shadow DOM 内部的某些部分设置样式,而不用完全打破封装。
基本用法:
- 在 Shadow DOM 内部,给某个元素添加
part="xxx"
属性; - 在外部,使用
::part(xxx)
选择器来设置该元素的样式;
✅ 示例:
组件内部(Shadow DOM):
javascript
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = `
<style>
.inner-box {
padding: 10px;
border: 1px solid #aaa;
border-radius: 4px;
}
</style>
<div class="inner-box" part="content-box">
我是 Shadow DOM 内部的一个盒子,但允许外部通过 part 设置样式
</div>
`;
外部 HTML:
html
<my-part-component style="
::part(content-box) {
background: yellow;
font-weight: bold;
border-color: red;
}
"></my-part-component>
🔓 说明:
- 只有你在 Shadow DOM 中显式地给元素添加了
part="xxx"
,外部才能通过::part(xxx)
去设置样式; - 这是一种 安全、可控的样式开放方式,比完全开放 Shadow DOM 更优雅 👍。
2.插槽
插槽(<slot>
)是 Shadow DOM 中的一个特殊 HTML 元素,它定义了一个"占位符",允许外部使用者向该组件的内部插入任意内容
在组件的 Shadow DOM 模板中,使用 <slot></slot>
来定义一个内容插槽。
示例:最基础的插槽
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>插槽 Slot 基础示例</title>
</head>
<body>
<!-- 自定义组件使用 -->
<my-card>
<h2>这是标题</h2>
<p>这是插槽传入的内容,会显示在组件的 <slot> 位置</p>
</my-card>
<script>
class MyCard extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
shadow.innerHTML = `
<style>
.card {
border: 1px solid #ccc;
padding: 20px;
border-radius: 8px;
font-family: Arial, sans-serif;
}
</style>
<div class="card">
<slot></slot> <!-- 这里是内容会被插入的位置 -->
</div>
`;
}
}
customElements.define('my-card', MyCard);
</script>
</body>
</html>
插槽相关用法:
类型 | 语法 | 说明 | 是否推荐 |
---|---|---|---|
默认插槽 | <slot></slot> |
接收所有未指定插槽的内容 | ✅ 推荐 |
具名插槽 | <slot name="xxx"></slot> |
接收 slot="xxx" 的内容 |
✅ 推荐(复杂组件常用) |
传入内容 | <div slot="name">内容</div> 或直接子节点 |
将内容插入插槽中 | ✅ |
默认内容 | 写在 <slot>默认文案</slot> 中 |
插槽无内容时显示 | ✅ 推荐 |
样式控制 | ::slotted(选择器) |
为插槽内容设置样式 | ✅ 推荐 |
作用 | 实现内容分发,让组件更灵活、可配置 | --- | ✅ 核心功能 |
✅ 结果:
- 你写在
<my-card>
标签内部的 HTML(如<h2>
和<p>
) ,会被自动"传送"到组件 Shadow DOM 中的<slot>
位置并渲染出来; - 组件的内部样式通过 Shadow DOM 完全封装,不会影响外部;
- 外部可以传入任意结构的内容,非常灵活 ✅。
3.通信
🧩 父 → 子 通信:通过属性(Attributes)或属性监听
这是最常见、最简单的通信方式,尤其适用于 父组件控制子组件行为或展示。
✅ 实现方式:
- 父组件通过 设置子组件的属性(attribute) 来传递数据或控制状态;
- 子组件通过
attributeChangedCallback
监听这些属性的变化并做出响应;
✅ 示例:父组件控制子组件的 disabled 状态
xml
html
<!-- 子组件:my-button -->
<script>
class MyButton extends HTMLElement {
static get observedAttributes() {
return ['disabled'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'disabled') {
this.style.opacity = newValue !== null ? '0.5' : '1';
this.style.pointerEvents = newValue !== null ? 'none' : 'auto';
}
}
}
customElements.define('my-button', MyButton);
</script>
<!-- 父组件 HTML 中使用 -->
<my-button disabled>点击我(被禁用)</my-button>
<my-button>点击我(可用)</my-button>
🔧 说明:
- 父组件通过设置
disabled
属性,控制子组件是否禁用; - 子组件通过
observedAttributes
+attributeChangedCallback
监听变化;
✅ 适用场景: 父组件需要控制子组件 UI 状态、配置等;
🧩 子 → 父 通信:通过事件(Custom Events)
子组件可以通过派发 自定义事件(CustomEvent) 来通知父组件发生了什么,比如点击、数据变更等。
✅ 实现方式:
- 子组件使用
this.dispatchEvent(new CustomEvent(...))
派发事件; - 父组件通过
addEventListener
监听这些事件;
✅ 示例:子组件按钮点击后通知父组件
xml
html
<script>
class MyNotifyButton extends HTMLElement {
connectedCallback() {
this.innerHTML = `<button>点击通知父组件</button>`;
this.querySelector('button').addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('button-clicked', {
detail: { message: '子组件被点击了!' },
bubbles: true // 允许事件冒泡,方便父组件监听
}));
});
}
}
customElements.define('my-notify-button', MyNotifyButton);
</script>
<!-- 父组件监听子组件事件 -->
<my-notify-button></my-notify-button>
<script>
document.querySelector('my-notify-button').addEventListener('button-clicked', (e) => {
alert(e.detail.message); // 接收到子组件消息
});
</script>
🔧 说明:
- 子组件通过
CustomEvent
派发事件,并可通过detail
传递数据; - 父组件通过监听该事件来响应子组件行为;
bubbles: true
是推荐做法,这样即使组件嵌套也能监听;
✅ 适用场景: 子组件需要通知父组件用户交互、状态变更等;
🧩 兄弟组件 / 远房组件通信:通过共同的父组件 或 全局事件 / 状态管理
如果两个组件没有直接的父子关系(比如兄弟组件、跨层级组件),常用的方式有:
方式 1:通过共同的父组件中转
- 子组件 A 向父组件派发事件;
- 父组件接收到后,调用子组件 B 的方法或修改其属性;
方式 2:使用全局事件(Event Bus 模式)
- 在
window
上派发和监听自定义事件,实现任意组件间通信; - 简单但松散耦合,适合中小型应用;
方式 3:使用状态管理库(如 Redux、MobX)或自定义全局状态
- 适用于大型应用,后面我们会提到更结构化的状态管理方案;
4.状态管理
方案 1:状态提升(Lifting State Up)------ 通过共同的父组件管理状态
方案 2:全局状态对象(Global State / Singleton)
- 创建一个 全局的 JavaScript 对象 / 模块,保存共享的状态;
- 所有组件通过导入或访问该对象来读取或监听状态变化;
✅ 示例:使用一个全局状态对象
javascript
// global-state.js
export const state = {
count: 0,
setCount(newCount) {
this.count = newCount;
// 可以在这里派发事件通知所有组件状态更新
window.dispatchEvent(new CustomEvent('state-changed', { detail: { count: newCount } }));
}
};
html
<script type="module">
import { state } from './global-state.js';
class CounterDisplay extends HTMLElement {
connectedCallback() {
this.render();
window.addEventListener('state-changed', this.render.bind(this));
}
render() {
this.innerHTML = `<p>当前计数: ${state.count}</p>`;
}
}
customElements.define('counter-display', CounterDisplay);
</script>
<script type="module">
import { state } from './global-state.js';
class CounterButton extends HTMLElement {
connectedCallback() {
this.innerHTML = `<button>增加</button>`;
this.querySelector('button').addEventListener('click', () => {
state.setCount(state.count + 1);
});
}
}
customElements.define('counter-button', CounterButton);
</script>
<counter-display></counter-display>
<counter-button></counter-button>
四.总结
Web Component 是一套由 W3C 标准化的原生 Web 技术,用于创建可复用、封装良好的自定义 HTML 元素,核心优势在于跨框架兼容性和样式隔离。以下是其关键信息:
-
核心技术:
- 自定义元素 :通过继承
HTMLElement
定义新标签,如<my-button>
,并注册到customElements
。 - Shadow DOM:封装组件内部结构与样式,避免全局污染,支持样式隔离。
- HTML 模板 :通过
<template>
和<slot>
定义可复用的 HTML 片段,动态插入内容。
- 自定义元素 :通过继承
-
核心优势:
- 高复用性:组件可在不同项目、框架中直接使用,减少代码冗余。
- 强封装性:样式和逻辑独立,避免组件间相互影响。
- 跨框架支持:可在 React、Vue、Angular 等框架中无缝集成。
- 原生兼容性:现代浏览器(Chrome、Firefox 等)原生支持,旧浏览器需通过 Polyfill 兼容。
-
应用场景:
- 组件库开发:构建企业级 UI 组件库(如按钮、表单、导航栏)。
- 微前端架构:拆分大型应用为独立微应用,通过组件化集成。
- 复杂 UI 构建:实现拖放界面、交互式图表等可维护性高的组件。
-
兼容性处理 :
通过引入
@webcomponents/webcomponentsjs
等 Polyfill 库,支持 IE 及旧版浏览器。