引言
Web Components 是一组原生浏览器 API,允许开发者创建可复用的自定义元素。然而,在实际项目中,我们往往需要在 React、Vue 或 Angular 等框架中使用这些组件。由于框架的虚拟 DOM 和事件系统与原生元素存在差异,直接使用可能会遇到数据绑定失效、事件丢失等问题。本文将手把手教你如何正确集成 Web Components 与三大框架,并分享最佳实践。
1. 创建一个简单的 Web Component
首先,我们创建一个名为 的 Web Component,用于展示用户信息。
// user-card.js
class UserCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._name = '';
this._age = 0;
}
static get observedAttributes() {
return ['name', 'age'];
}
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'name') this._name = newValue;
if (name === 'age') this._age = newValue;
this.render();
}
connectedCallback() {
this.render();
}
render() {
this.shadowRoot.innerHTML = `
<style>
.card { border: 1px solid #ccc; padding: 10px; border-radius: 8px; }
.name { font-weight: bold; }
.age { color: #666; }
</style>
<div class="card">
<div class="name">${this._name}</div>
<div class="age">${this._age} years old</div>
<button id="greet">Greet</button>
</div>
`;
this.shadowRoot.querySelector('#greet').addEventListener('click', () => {
this.dispatchEvent(new CustomEvent('greet', { detail: this._name }));
});
}
}
customElements.define('user-card', UserCard);
该组件接收 name 和 age 属性,并触发 greet 自定义事件。
2. 在 React 中使用 Web Components
React 对 Web Components 的支持较为友好,但需要注意属性传递和事件处理。
2.1 安装与引入
npm install --save user-card
# 或直接引入脚本
在 React 组件中引入:
// 在入口文件或组件中引入
import 'user-card';
2.2 使用组件
import React, { useRef, useEffect } from 'react';
function App() {
const cardRef = useRef(null);
useEffect(() => {
const card = cardRef.current;
const handleGreet = (e) => {
alert(`Hello, ${e.detail}!`);
};
card.addEventListener('greet', handleGreet);
return () => card.removeEventListener('greet', handleGreet);
}, []);
return (
<div>
{/* 注意:React 中属性名需使用驼峰,但 Web Components 属性名是 kebab-case */}
<user-card ref={cardRef} name="Alice" age={25} />
</div>
);
}
export default App;
注意:
- 属性
name和age直接作为 HTML 属性传递,React 会自动处理字符串和数字。 - 事件监听必须使用原生
addEventListener,因为 React 的合成事件无法直接捕获 Web Component 的自定义事件。 - 如果属性是对象或数组,需要通过
JSON.stringify转为字符串,或在attributeChangedCallback中解析。
2.3 传递复杂数据
<user-card user={JSON.stringify({name: 'Bob', age: 30})} />
在 Web Component 中解析:
attributeChangedCallback(name, oldValue, newValue) {
if (name === 'user') {
const user = JSON.parse(newValue);
this._name = user.name;
this._age = user.age;
}
this.render();
}
3. 在 Vue 中使用 Web Components
Vue 3 对 Web Components 有很好的支持,但需要配置 isCustomElement 或使用 vue-web-component-wrapper(不推荐)。
3.1 配置 Vue 忽略自定义元素
在 vite.config.js 中:
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: {
isCustomElement: (tag) => tag.startsWith('user-'),
},
},
}),
],
});
3.2 使用组件
<template>
<div>
<user-card :name="userName" :age="userAge" @greet="handleGreet" ref="cardRef" />
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import 'user-card';
const userName = ref('Alice');
const userAge = ref(25);
const cardRef = ref(null);
const handleGreet = (e) => {
alert(`Hello, ${e.detail}!`);
};
// 注意:Vue 3 的 @greet 可以直接绑定自定义事件,但需要组件触发 CustomEvent
// 如果事件名是 kebab-case,Vue 会自动转换
</script>
注意:
- Vue 3 支持在模板中直接使用
@greet监听自定义事件,但事件名必须全部小写(因为 HTML 属性不区分大小写)。 - 属性绑定使用
:prop语法,Vue 会自动将 camelCase 转为 kebab-case(例如:userName变为user-name属性)。
3.3 传递对象属性
<template>
<user-card :user="userObject" />
</template>
<script setup>
const userObject = { name: 'Bob', age: 30 };
</script>
Vue 会将对象转为 JSON 字符串传递给属性,Web Component 需解析。
4. 在 Angular 中使用 Web Components
Angular 使用 Zone.js 和自定义事件处理,需要配置 CUSTOMELEMENTSSCHEMA。
4.1 配置模块
// app.module.ts
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA], // 允许自定义元素
bootstrap: [AppComponent],
})
export class AppModule {}
4.2 使用组件
// app.component.ts
import { Component, ElementRef, ViewChild, AfterViewInit, OnDestroy } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<user-card
[attr.name]="userName"
[attr.age]="userAge"
#cardRef
></user-card>
`,
})
export class AppComponent implements AfterViewInit, OnDestroy {
userName = 'Alice';
userAge = 25;
@ViewChild('cardRef', { static: true }) cardRef!: ElementRef;
private listener!: (e: Event) => void;
ngAfterViewInit() {
this.listener = (e: Event) => {
const customEvent = e as CustomEvent;
alert(`Hello, ${customEvent.detail}!`);
};
this.cardRef.nativeElement.addEventListener('greet', this.listener);
}
ngOnDestroy() {
this.cardRef.nativeElement.removeEventListener('greet', this.listener);
}
}
注意:
- 属性绑定必须使用
[attr.name]语法,否则 Angular 会尝试进行双向绑定。 - 事件监听必须通过原生 API,因为 Angular 的
(greet)语法无法直接识别自定义事件(除非使用@Output装饰器包装,但 Web Component 不是 Angular 组件)。 - 如果属性是对象,可以使用
[attr.user]="userObject | json"但需注意 JSON 序列化。
5. 最佳实践与常见坑
5.1 属性命名
Web Components 使用 kebab-case 属性名,而 React 和 Vue 推荐 camelCase。在框架中传递时,框架会自动转换,但需注意:
- React:直接写 kebab-case 属性名,如
user-name。 - Vue:使用
:userName会转换为user-name。 - Angular:使用
[attr.user-name]。
5.2 事件处理
- 所有框架中,自定义事件都建议使用原生
addEventListener监听,因为框架的合成事件系统可能无法正确处理。 - Vue 3 的
@语法可以工作,但事件名必须全部小写。 - Angular 可以通过
@HostListener或addEventListener。
5.3 生命周期
Web Components 的 connectedCallback 对应框架的挂载阶段,disconnectedCallback 对应卸载。确保在卸载时移除事件监听,避免内存泄漏。
5.4 样式隔离
Web Components 的 Shadow DOM 提供样式隔离,但框架的全局样式无法穿透。如果需要覆盖样式,可以使用 CSS 自定义属性(CSS Variables)或 ::part 伪元素。
user-card {
--card-border-color: blue;
}
在 Web Component 中使用:
.card {
border-color: var(--card-border-color, #ccc);
}
总结
本文通过一个完整的 user-card 示例,演示了 Web Components 与 React、Vue 和 Angular 的集成方法。核心要点包括:
- 属性传递:使用原生属性,复杂数据需序列化。
- 事件监听:优先使用原生
addEventListener。 - 生命周期管理:注意在卸载时清理。
- 样式隔离:利用 CSS 自定义属性进行主题定制。
Web Components 与框架的集成并不复杂,但需要理解两者之间的差异。掌握这些技巧后,你可以在项目中无缝使用任何 Web Component,享受跨框架复用的便利。
延伸阅读
- MDN Web Components 文档
- lit-html 库 – 简化 Web Components 开发
- Stencil 框架 – 生成高性能 Web Components