H5 Web Components 组件化方案

举报
yd_290101916 发表于 2026/09/28 09:06:41 2026/09/28
【摘要】 H5 Web Components 组件化方案摘要:Web Components 是浏览器原生的组件化技术,基于 Custom Elements、Shadow DOM、HTML Templates 三件套,无需框架即可创建可复用组件。本文系统讲解核心 API、生命周期、样式隔离与实战模式。 一、为什么用 Web Components主流框架(React/Vue)组件不能跨框架复用:Reac...

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 语法)作为上层封装,享受类型与性能的同时保留互操作性。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。