Web Components是浏览器原生支持的组件化标准,由Custom Elements、Shadow DOM和HTML Templates三部分组成。与React/Vue等框架组件不同,Web Components不依赖任何运行时库,浏览器直接解析执行。在需要跨框架复用组件、构建与框架无关的设计系统、或开发嵌入式微件时,Web Components提供了一条技术中立路径。
Custom Elements定义与生命周期回调
自定义元素通过customElements.define注册,类继承HTMLElement并实现生命周期回调:
class RatingStars extends HTMLElement {
constructor() {
super();
this._value = 0;
this._max = 5;
}
// 元素插入DOM时调用
connectedCallback() {
this.render();
this.attachEvents();
}
// 元素移除DOM时调用
disconnectedCallback() {
this.cleanup();
}
// 监听的属性变化
static get observedAttributes() {
return ['value', 'max', 'readonly'];
}
// 属性变化时调用
attributeChangedCallback(name, oldVal, newVal) {
if (name === 'value') {
this._value = Number(newVal) || 0;
this.updateDisplay();
}
if (name === 'max') {
this._max = Number(newVal) || 5;
this.render();
}
}
render() {
// 渲染逻辑
}
attachEvents() {
// 事件绑定
}
cleanup() {
// 清理定时器、事件监听等
}
}
customElements.define('rating-stars', RatingStars);
生命周期回调的执行顺序:constructor(元素创建时)-> connectedCallback(插入DOM时)-> attributeChangedCallback(属性变化时)-> disconnectedCallback(移除时)。observedAttributes声明需要追踪的属性,未声明的属性变化不会触发回调。
使用方式与原生HTML元素一致:
<rating-stars value="3" max="5"></rating-stars>
Shadow DOM封装与样式隔离
Shadow DOM创建独立的DOM子树,外部CSS选择器和JavaScript无法穿透Shadow边界,实现真正的样式隔离。在constructor中开启Shadow DOM:
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const style = document.createElement('style');
style.textContent = `
:host {
display: inline-flex;
gap: 4px;
}
.star {
font-size: 24px;
color: #ccc;
cursor: pointer;
transition: color 0.2s;
}
.star.active {
color: #f5a623;
}
.star:hover {
color: #f5a623;
}
`;
const container = document.createElement('div');
container.className = 'container';
shadow.appendChild(style);
shadow.appendChild(container);
this._container = container;
}
:host选择器匹配自定义元素本身,相当于Shadow DOM内的this。Shadow DOM内的样式不会泄漏到外部,外部全局CSS的类名也不会影响Shadow DOM内的元素。
mode: 'open'允许外部通过element.shadowRoot访问Shadow DOM,便于调试。mode: 'closed'则禁止外部访问,安全性更高但调试不便。生产环境推荐open模式配合文档说明。
HTML Templates与slot插槽机制
使用<template>定义可复用的DOM结构,配合<slot>实现内容分发:
<template id="card-template">
<style>
.card {
border: 1px solid #e0e0e0;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 4px rgba(0,0,0,0.08);
}
.card-header {
font-weight: 600;
margin-bottom: 8px;
}
::slotted([slot="title"]) {
color: #1a1a1a;
}
</style>
<div class="card">
<div class="card-header">
<slot name="title">默认标题</slot>
</div>
<div class="card-body">
<slot>默认内容</slot>
</div>
</div>
</template>
<script>
class InfoCard extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: 'open' });
const template = document.getElementById('card-template');
shadow.appendChild(template.content.cloneNode(true));
}
}
customElements.define('info-card', InfoCard);
</script>
<!-- 使用 -->
<info-card>
<span slot="title">用户信息</span>
<p>这是卡片内容</p>
</info-card>
::slotted选择器样式化被插入到slot中的元素。cloneNode(true)深拷贝template内容,每次实例化都获得独立的DOM副本。具名slot(name="title")匹配slot="title"的子元素,默认slot(无name属性)接收未指定slot的子元素。
与React和Vue集成互操作
Web Components可以被任何框架直接使用。在React中使用时,需注意React对自定义元素属性传递的限制——React不会自动将非标准属性映射到DOM属性:
// React封装组件
function RatingInput({ value, max, onChange }) {
const ref = useRef(null);
useEffect(() => {
const el = ref.current;
el.addEventListener('rating-change', (e) => {
onChange(e.detail.value);
});
return () => {
el.removeEventListener('rating-change', () => {});
};
}, [onChange]);
return (
<rating-stars
ref={ref}
value={value}
max={max}
/>
);
}
// React 19+ 直接支持自定义元素属性
// React 18及以下需要手动设置属性
useEffect(() => {
if (ref.current) {
ref.current.value = value;
ref.current.max = max;
}
}, [value, max]);
自定义元素通过CustomEvent向外发送事件,React通过addEventListener监听。Vue中使用更简洁,@rating-change可直接绑定:
<template>
<rating-stars
:value="rating"
:max="5"
@rating-change="onRatingChange"
/>
</template>
<script setup>
const rating = ref(3);
function onRatingChange(e) {
rating.value = e.detail.value;
}
</script>
Vue的v-model无法直接绑定自定义元素,需要通过:value和@event手动实现双向绑定。Vue 3.3+的defineCustomElement可将Vue组件编译为Web Components:
import { defineCustomElement } from 'vue';
const MyButton = defineCustomElement({
props: ['label', 'type'],
emits: ['click'],
template: `<button :class="'btn-' + type" @click="$emit('click')">{{ label }}</button>`
});
customElements.define('my-button', MyButton);
浏览器兼容与Polyfill策略
Web Components的核心API在Chrome、Firefox、Safari和Edge中已全面支持。对于需要兼容旧浏览器的项目,使用@webcomponents/webcomponentsjspolyfill:
<script type="module">
if (!customElements || !Element.prototype.attachShadow) {
await import('@webcomponents/webcomponentsjs/webcomponents-bundle.js');
await customElements.whenDefined('rating-stars');
}
</script>
polyfill仅在不支持时按需加载,减少现代浏览器的额外开销。customElements.whenDefined返回Promise,确保元素注册完成后再执行依赖逻辑。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/webcomponents-zi-ding-yi-yuan-su-shi-zhan-shadowdom-yu-kua/