Web Components原生组件Custom Element与Shadow DOM封装实践
Web Components是浏览器原生支持的组件化标准,由Custom Element、Shadow DOM、HTML Template三个核心API组成。与React/Vue等框架组件不同,Web Components不依赖任何框架运行时,可直接在任何HTML页面中使用。本文从Custom Element生命周期钩子、Shadow DOM样式隔离到表单集成和框架兼容,给出完整的开发实战指南。
Custom Element生命周期钩子与属性响应
Custom Element通过customElements.define注册后,浏览器在解析到对应标签时自动实例化。关键生命周期回调:
class TooltipBox extends HTMLElement {
constructor() {
super();
this._visible = false;
this._content = '';
}
connectedCallback() {
this.render();
this.addEventListener('mouseenter', this._show);
this.addEventListener('mouseleave', this._hide);
}
disconnectedCallback() {
this.removeEventListener('mouseenter', this._show);
this.removeEventListener('mouseleave', this._hide);
}
static get observedAttributes() {
return ['content', 'position'];
}
attributeChangedCallback(name, oldVal, newVal) {
if (oldVal === newVal) return;
switch (name) {
case 'content':
this._content = newVal;
this._updateContent();
break;
case 'position':
this._updatePosition(newVal);
break;
}
}
_show = () => {
this._visible = true;
this._toggle();
};
_hide = () => {
this._visible = false;
this._toggle();
};
_toggle() {
const tip = this.shadowRoot.querySelector('.tip');
if (tip) tip.style.display = this._visible ? 'block' : 'none';
}
render() {
this.innerHTML = '<slot></slot>';
}
}
customElements.define('tooltip-box', TooltipBox);
使用方式:
<tooltip-box content="提示文字" position="top">悬停查看</tooltip-box>
注意点:自定义标签名必须包含连字符(-),这是规范强制要求;connectedCallback可能被多次触发(如DOM移动),需处理重复绑定问题。
Shadow DOM样式隔离与CSS自定义属性穿透
Shadow DOM创建独立的DOM子树,外部CSS无法穿透到内部,内部样式也不会泄漏到外部。这是Web Components样式封装的核心机制。
class CardItem extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
connectedCallback() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
border-radius: 8px;
overflow: hidden;
background: var(--card-bg, #ffffff);
border: 1px solid var(--card-border, #e0e0e0);
box-shadow: var(--card-shadow, 0 2px 8px rgba(0,0,0,0.1));
}
.header {
padding: 16px;
font-size: 18px;
font-weight: 600;
border-bottom: 1px solid var(--card-border, #e0e0e0);
}
.body { padding: 16px; }
::slotted(h3) {
margin: 0;
color: var(--card-title-color, #1a1a1a);
}
</style>
<div class="header">
<slot name="header"></slot>
</div>
<div class="body">
<slot></slot>
</div>
`;
}
}
customElements.define('card-item', CardItem);
使用方式及外部样式穿透:
<style>
card-item {
--card-bg: #f8f9fa;
--card-border: #dee2e6;
--card-title-color: #212529;
}
</style>
<card-item>
<span slot="header">标题区域</span>
<p>内容区域</p>
</card-item>
:host选择器匹配组件宿主元素,::slotted选择器匹配插槽中分发进来的内容。CSS自定义属性是Shadow DOM内外通信的唯一样式通道。
表单集成与ElementInternals API
Web Components默认不参与表单提交,需通过ElementInternals API将自定义元素注册为表单控件:
class RatingInput extends HTMLElement {
static formAssociated = true;
constructor() {
super();
this.attachShadow({ mode: 'open' });
this._value = 0;
}
connectedCallback() {
this.shadowRoot.innerHTML = `
<style>
.star { cursor: pointer; font-size: 24px; display: inline-block; }
.star.active { color: #ffc107; }
.star.inactive { color: #ddd; }
</style>
<div class="stars"></div>
`;
this._renderStars();
}
get internals() {
return this.attachInternals();
}
set value(val) {
this._value = val;
this.internals.setFormValue(String(val));
this.internals.setValidity({});
this._renderStars();
}
get value() { return this._value; }
formResetCallback() { this.value = 0; }
formDisabledCallback(disabled) {
this.shadowRoot.querySelectorAll('.star').forEach(s => {
s.style.pointerEvents = disabled ? 'none' : 'auto';
s.style.opacity = disabled ? '0.5' : '1';
});
}
_renderStars() {
const container = this.shadowRoot.querySelector('.stars');
container.innerHTML = '';
for (let i = 1; i <= 5; i++) {
const star = document.createElement('span');
star.className = 'star ' + (i <= this._value ? 'active' : 'inactive');
star.textContent = '\u2605';
star.addEventListener('click', () => { this.value = i; });
container.appendChild(star);
}
}
}
customElements.define('rating-input', RatingInput);
使用方式:
<form id="reviewForm">
<label>评分:<rating-input name="rating"></rating-input></label>
<button type="submit">提交</button>
</form>
<script>
document.getElementById('reviewForm').addEventListener('submit', (e) => {
e.preventDefault();
const fd = new FormData(e.target);
console.log('Rating:', fd.get('rating'));
});
</script>
框架兼容性与打包发布策略
Web Components在React中使用有一个已知问题:React 18及之前版本不会将自定义属性传递给自定义元素。解决方案是使用@lit/react包生成React绑定:
import { createComponent } from '@lit/react';
import React from 'react';
import { CardItem } from './card-item.js';
export const CardItemReact = createComponent({
tagName: 'card-item',
elementClass: CardItem,
react: React,
eventName: { onCardClick: 'card-click' },
});
Vue 3对Web Components有原生支持,通过compilerOptions.isCustomElement配置即可:
// vite.config.js
export default defineConfig({
vue: {
template: {
compilerOptions: {
isCustomElement: (tag) => tag.includes('-'),
},
},
},
});
打包发布时,每个Web Component建议独立输出为一个JS文件,通过ES Module按需加载。组件样式内联在Shadow DOM中,无需外部CSS文件,这天然避免了样式冲突问题。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/webcomponents-yuan-sheng-zu-jian-customelement-yu-shadowdom/