HTML 模板与 Web Components:原生组件化方案

CLARA轻量论坛系统
CLARA轻量论坛系统 星耀SVIP管理员 黑卡会员
发布于 2026-09-20 02:44 ·3 浏览 ·0 回复

看完这篇你就能从零写一个不用任何框架、浏览器原生支持的 `<user-card>` 组件,并且知道 template、Shadow DOM、自定义元素、slot 这四个零件各自负责什么。

第一步:先认清四个零件

Web Components 不是一门新语言,是四个已有 API 的组合,缺一个都不完整:

  • 自定义元素(`customElements.define`):把 `<user-card>` 这种带短横线的标签注册成真标签。
  • Shadow DOM:给组件一个独立 DOM 树,外部样式进不来,内部样式出不去。
  • `<template>`:写结构但不渲染,需要时克隆一份,避免在 JS 里拼字符串。
  • `<slot>`:内容分发,让使用者往组件里塞自己的 HTML。

注意:自定义元素名字必须包含短横线(`user-card` 合法,`usercard` 会直接抛 `SyntaxError`),且不能和已有标签重名。

第二步:用 template 写结构

把模板放在 HTML 里,而不是写在 JS 字符串中,编辑器能高亮、能格式化:

<template id="user-card-tpl">
  <style>
    :host { display:block; border:1px solid #ddd; border-radius:8px; padding:12px; }
    :host([hidden]) { display:none; }
    .name { font-weight:700; color: var(--card-name-color, #222); }
  </style>
  <div class="name"></div>
  <slot name="desc">暂无简介</slot>
</template>

`:host` 指组件自身,`var(--card-name-color, #222)` 是留给外部改样式的接口。

第三步:注册自定义元素并挂 Shadow DOM

class UserCard extends HTMLElement {
  static get observedAttributes() { return ['name']; }

  constructor() {
    super();
    const tpl = document.getElementById('user-card-tpl');
    this.attachShadow({ mode: 'open' })
        .appendChild(tpl.content.cloneNode(true));   // 必须 clone,否则只能插一次
    this._nameEl = this.shadowRoot.querySelector('.name');
  }

  connectedCallback() { this._render(); }
  attributeChangedCallback() { this._render(); }

  _render() {
    // 用 textContent,不要用 innerHTML 拼用户数据
    this._nameEl.textContent = this.getAttribute('name') ?? '';
  }
}
customElements.define('user-card', UserCard);

注意:`tpl.content` 是文档片段,直接 append 会被“搬走”,第二次就空了,所以每次都要 `cloneNode(true)`。

注意:初始属性存在时,`attributeChangedCallback` 会在 `constructor` 之后、`connectedCallback` 之前触发,所以 `_render()` 里访问的元素必须在 constructor 阶段就缓存好。

第四步:用 slot 让使用者塞内容

<user-card name="阿乐">
  <span slot="desc">Clara BBS 作者</span>
</user-card>

模板里的 `<slot name="desc">` 会接收这个 `slot="desc"` 的元素;如果使用者没传,就显示“暂无简介”这个默认内容。

第五步:对外通信——事件和样式两条路

组件往外发消息用自定义事件:

this.dispatchEvent(new CustomEvent('greet', {
  detail: { name: this.getAttribute('name') },
  bubbles: true,
  composed: true      // 关键:不加这个,事件穿不出 Shadow DOM
}));

注意:`composed: true` 是跨 Shadow 边界冒泡的必要条件,忘了它,外面 `document.addEventListener('greet', ...)` 永远收不到。

样式想留可改的口子,就用 `part` + `::part()`:

<!-- 模板内 -->
<div class="name" part="name"></div>
/* 组件外部 */
user-card::part(name) { letter-spacing: 1px; }

注意:外部普通的 `user-card .name { }` 是无效的,选择器进不了 Shadow DOM。改样式只有两条正规路径:CSS 自定义属性、`::part()`。

第六步:检查兼容与常见错误

  • 现代 Chrome / Edge / Firefox / Safari 均已原生支持,IE 不支持。
  • 同名重复 `define` 会抛 `NotSupportedError`;注册后再改原型也不生效,务必在类写完整后再注册。
  • 组件未 upgrade 前,自定义标签里的内容会先按普通元素显示一下(FOUC)。给 `:host { display:block }` 配合 CSS 里的 `user-card:not(:defined) { visibility:hidden }` 可以规避。

小结

  1. 四个零件分工明确:`<template>` 存结构、Shadow DOM 做隔离、自定义元素做注册、`<slot>` 做分发。
  2. 组件名必须带短横线;模板必须 `cloneNode`;事件跨边界必须 `composed: true`。
  3. 样式隔离是双向的,对外只留 CSS 自定义属性和 `::part()` 两个口子。
  4. 给用户数据赋值一律用 `textContent`,别用 `innerHTML` 拼字符串。
  5. 生命周期按 `constructor → attributeChangedCallback → connectedCallback` 的顺序走,缓存 DOM 引用要在 constructor 里做。
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-535.html
转载请注明出处,版权归原作者所有。

全部回复 0

还没有回复,来抢沙发~