H5 Web Components 组件化方案
H5 Web Components 组件化方案
摘要:Web Components 是浏览器原生的组件化技术,基于 Custom Elements、Shadow DOM、HTML Templates 三件套,无需框架即可创建可复用组件。本文系统讲解核心 API、生命周期、样式隔离与实战模式。
一、为什么用 Web Components
主流框架(React/Vue)组件不能跨框架复用:React 组件无法直接用在 Vue 项目里。Web Components 是浏览器标准,框架无关,一次编写到处可用。适合:
- 跨框架的设计系统组件库。
- 微前端架构中的独立模块。
- 第三方嵌入式 widget(评论、播放器)。
- 需要强样式隔离的组件。
代价是 API 偏底层,状态管理、列表渲染需手写或配合轻量库。
二、三件套
Custom Elements
定义自定义元素:
class TravelCard extends HTMLElement {
constructor() {
super();
this._data = null;
}
connectedCallback() {
this.render();
}
render() {
this.innerHTML = `<h3>${this._data?.title ?? "未命名"}</h3>`;
}
set data(val) {
this._data = val;
this.render();
}
get data() {
return this._data;
}
}
customElements.define("travel-card", TravelCard);
使用:
<travel-card></travel-card>
Shadow DOM
提供样式与 DOM 隔离:
class TravelCard extends HTMLElement {
constructor() {
super();
const shadow = this.attachShadow({ mode: "open" });
shadow.innerHTML = `
<style>
:host { display: block; border: 1px solid #ccc; padding: 16px; }
h3 { color: #B91C1C; }
</style>
<h3>标题</h3>
<slot></slot>
`;
}
}
:host 选择宿主元素,<slot> 接收外部传入的子节点。外部样式无法穿透 Shadow DOM,组件样式也不会泄漏。
HTML Templates
<template id="card-tmpl">
<style>
.card { padding: 16px; }
.title { font-weight: bold; }
</style>
<div class="card">
<div class="title"></div>
<div class="desc"></div>
</div>
</template>
const tmpl = document.getElementById("card-tmpl").content;
const root = tmpl.cloneNode(true);
root.querySelector(".title").textContent = "故宫";
shadow.appendChild(root);
<template> 内容不渲染,可多次克隆,比字符串拼模板更安全。
三、生命周期
class MyEl extends HTMLElement {
connectedCallback() { /* 插入 DOM */ }
disconnectedCallback() { /* 移除 DOM */ }
attributeChangedCallback(name, oldVal, newVal) { /* 属性变化 */ }
adoptedCallback() { /* 移到新 document */ }
static get observedAttributes() {
return ["title", "open"];
}
}
observedAttributes 声明要监听的属性,变化时触发 attributeChangedCallback。
四、属性与特性
HTML 属性(attribute)是字符串,DOM 属性(property)可是任意类型。约定:两者同步。
class TravelCard extends HTMLElement {
static get observedAttributes() { return ["title"]; }
attributeChangedCallback(name, oldVal, newVal) {
if (name === "title") {
this._title = newVal;
this.render();
}
}
set title(val) { this.setAttribute("title", val); }
get title() { return this.getAttribute("title") ?? ""; }
}
布尔属性需特殊处理:
get open() { return this.hasAttribute("open"); }
set open(val) {
val ? this.setAttribute("open", "") : this.removeAttribute("open");
}
五、事件
组件内部事件用 CustomEvent 抛出,bubbles 与 composed 让事件穿透 Shadow DOM:
this.dispatchEvent(new CustomEvent("card-click", {
detail: { id: this._data.id },
bubbles: true,
composed: true
}));
外部监听:
card.addEventListener("card-click", e => {
console.log("点击了", e.detail.id);
});
六、样式隔离与穿透
外部控制宿主
travel-card { border-radius: 8px; } /* 外部样式作用于宿主 */
Shadow 内部用 :host 接收:
:host { border-radius: 8px; }
:host([featured]) { border-color: gold; }
CSS 自定义属性穿透
CSS 变量可穿透 Shadow DOM,是主题化的主要手段:
travel-card {
--card-bg: #fff;
--card-fg: #333;
}
/* Shadow 内 */
:host {
background: var(--card-bg, #fff);
color: var(--card-fg, #333);
}
CSS Parts
用 ::part() 暴露内部元素给外部样式:
shadow.innerHTML = `<div part="title">标题</div>`;
travel-card::part(title) { font-size: 20px; }
adoptStyleSheets
共享样式表,性能优于多个 <style>:
const sheet = new CSSStyleSheet();
sheet.replaceSync(`.card { padding: 16px; }`);
shadow.adoptedStyleSheets = [sheet];
七、Slot 与组合
shadow.innerHTML = `
<div class="header"><slot name="icon"></slot><slot name="title"></slot></div>
<div class="body"><slot></slot></div>
`;
<travel-card>
<span slot="icon">🏛️</span>
<span slot="title">故宫</span>
<p>明清皇家宫殿</p>
</travel-card>
具名 slot 填对应位置,默认 slot 接收未命名的子节点。监听 slotchange 可在内容变化时响应。
八、表单集成
实现 ElementInternals 让自定义元素参与表单校验:
class RatingInput extends HTMLElement {
static formAssociated = true;
constructor() {
super();
this._internals = this.attachInternals();
}
connectedCallback() {
this.addEventListener("change", () => {
this._internals.setFormValue(this.value);
});
}
get value() { return this._value; }
set value(v) {
this._value = v;
this._internals.setFormValue(v);
}
}
可配合 setValidity() 做自定义校验。
九、实战:可复用卡片
class TravelCard extends HTMLElement {
static get observedAttributes() { return ["title", "tag", "heritage"]; }
constructor() {
super();
const shadow = this.attachShadow({ mode: "open" });
shadow.innerHTML = `
<style>
:host { display: block; }
.card { border: 1px solid #e5e5e5; border-radius: 12px; overflow: hidden; }
.tag { background: #B91C1C; color: #fff; padding: 4px 8px; border-radius: 4px; }
.heritage { color: #D4A84B; }
.title { font-size: 18px; font-weight: bold; }
slot { display: block; padding: 16px; }
</style>
<div class="card">
<div class="meta">
<span class="tag"></span>
<span class="heritage"></span>
</div>
<div class="title"></div>
<slot></slot>
</div>
`;
}
connectedCallback() { this._render(); }
attributeChangedCallback() { this._render(); }
_render() {
const root = this.shadowRoot;
root.querySelector(".tag").textContent = this.getAttribute("tag") ?? "";
root.querySelector(".title").textContent = this.getAttribute("title") ?? "";
root.querySelector(".heritage").textContent =
this.hasAttribute("heritage") ? "世界遗产" : "";
}
}
customElements.define("travel-card", TravelCard);
使用:
<travel-card title="故宫博物院" tag="宫殿" heritage>
红墙黄瓦,金碧辉煌。
</travel-card>
十、与框架协作
React 19+ 原生支持 Web Components,可直接传属性:
<travel-card title={title} heritage={isHeritage}>{desc}</travel-card>
旧版 React 对自定义属性需用 ref 设置。Vue 2.7+ 与 Vue 3 原生支持。框架无关正是 Web Components 的核心价值。
十一、调试与避坑
- Chrome DevTools 元素面板会显示 Shadow DOM(需开启设置)。
mode: "closed"会让外部无法访问shadowRoot,但调试困难,慎用。- 升级:
customElements.define前若已有<travel-card>在 DOM 中,会异步升级,connectedCallback会触发。 - 避免在
constructor中访问子节点或属性,此时还未连接。 - 大列表渲染性能不如框架的虚拟列表,必要时配合 lit-html 等轻量库。
十二、总结
Web Components 提供浏览器原生的组件化能力,强项是跨框架复用与样式隔离。掌握 Custom Elements 生命周期、Shadow DOM 隔离、Slot 组合、CSS 变量主题化四块即可构建可复用组件库。若嫌 API 偏底层,可上 Lit(响应式属性 + 模板)或 Stencil(带编译器,类 React 语法)作为上层封装,享受类型与性能的同时保留互操作性。
- 点赞
- 收藏
- 关注作者
评论(0)