Web 组件化开发

2026-06-22 · 6 阅读 · 587字
HTMLJavaScript

Web 组件化开发

什么是 Web Components

Web Components 是一组 W3C 标准,允许开发者创建可复用的自定义 HTML 元素。与 React 组件或 Vue 组件不同,它们是浏览器原生支持的,不依赖任何框架。

Web Components 由三项核心技术组成:

  1. Custom Elements:自定义元素
  2. Shadow DOM:影子 DOM,封装样式和结构
  3. HTML Templates:HTML 模板

Custom Elements

创建自定义元素

class MyButton extends HTMLElement {
  constructor() {
    super();
    this.addEventListener('click', () => {
      alert('按钮被点击!');
    });
  }

  // 监听属性变化
  static get observedAttributes() {
    return ['disabled', 'variant'];
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'disabled') {
      this.style.opacity = newValue !== null ? '0.5' : '1';
      this.style.cursor = newValue !== null ? 'not-allowed' : 'pointer';
    }
  }

  // 元素被插入 DOM
  connectedCallback() {
    this.render();
  }

  render() {
    this.textContent = this.getAttribute('label') || '按钮';
  }
}

// 注册自定义元素
customElements.define('my-button', MyButton);
<!-- 使用 -->
<my-button label="提交" disabled></my-button>
<my-button label="取消" variant="outline"></my-button>

生命周期回调

回调 触发时机
constructor 元素实例化时
connectedCallback 元素被插入 DOM 时
disconnectedCallback 元素从 DOM 移除时
attributeChangedCallback 监听属性变化时
adoptedCallback 元素被移动到新文档时

Shadow DOM

Shadow DOM 提供样式封装,组件内部的样式不会泄露到外部,外部样式也不会影响内部。

class MyCard extends HTMLElement {
  constructor() {
    super();
    // 创建影子 DOM
    this.attachShadow({ mode: 'open' }); // 或 'closed'
  }

  connectedCallback() {
    this.shadowRoot.innerHTML = `
      <style>
        .card {
          border: 1px solid #ddd;
          border-radius: 8px;
          padding: 16px;
          font-family: sans-serif;
        }
        .card-title {
          font-size: 1.2em;
          font-weight: bold;
          margin-bottom: 8px;
        }
        /* 这些样式不会影响外部元素 */
      </style>
      <div class="card">
        <div class="card-title">
          <slot name="title">默认标题</slot>
        </div>
        <div class="card-body">
          <slot></slot> <!-- 默认插槽 -->
        </div>
      </div>
    `;
  }
}

customElements.define('my-card', MyCard);
<my-card>
  <span slot="title">自定义标题</span>
  <p>这是卡片内容,在 Shadow DOM 中渲染。</p>
</my-card>

HTML Templates

<template> 标签包含不会被渲染的 HTML,但可以被 JavaScript 实例化:

<template id="user-card-template">
  <style>
    .user-card {
      display: flex;
      align-items: center;
      gap: 12px;
      padding: 12px;
      border-radius: 8px;
      background: #f5f5f5;
    }
    .avatar {
      width: 48px;
      height: 48px;
      border-radius: 50%;
      background: #007bff;
      color: white;
      display: flex;
      align-items: center;
      justify-content: center;
      font-size: 1.5em;
    }
  </style>
  <div class="user-card">
    <div class="avatar"><slot name="avatar">👤</slot></div>
    <div>
      <div class="name"><slot name="name">用户名</slot></div>
      <div class="email"><slot name="email">user@example.com</slot></div>
    </div>
  </div>
</template>
class UserCard extends HTMLElement {
  constructor() {
    super();
    const template = document.getElementById('user-card-template');
    const content = template.content.cloneNode(true);
    this.attachShadow({ mode: 'open' }).appendChild(content);
  }
}

customElements.define('user-card', UserCard);

实战:完整组件示例

class TodoList extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this.todos = [];
  }

  connectedCallback() {
    this.render();
    this.shadowRoot.querySelector('.add-btn')
      .addEventListener('click', () => this.addTodo());
  }

  addTodo() {
    const input = this.shadowRoot.querySelector('.todo-input');
    const text = input.value.trim();
    if (!text) return;

    this.todos.push({ id: Date.now(), text, done: false });
    input.value = '';
    this.renderTodos();
  }

  toggleTodo(id) {
    const todo = this.todos.find(t => t.id === id);
    if (todo) todo.done = !todo.done;
    this.renderTodos();
  }

  render() {
    this.shadowRoot.innerHTML = `
      <style>
        .container { font-family: sans-serif; max-width: 400px; }
        .todo-input { padding: 8px; width: 70%; }
        .add-btn { padding: 8px 16px; background: #007bff; color: white; border: none; cursor: pointer; }
        .todo-item { display: flex; align-items: center; gap: 8px; padding: 8px 0; }
        .done { text-decoration: line-through; color: #999; }
      </style>
      <div class="container">
        <h3>待办事项</h3>
        <div>
          <input class="todo-input" placeholder="添加待办...">
          <button class="add-btn">添加</button>
        </div>
        <div class="todo-list"></div>
      </div>
    `;
  }

  renderTodos() {
    const list = this.shadowRoot.querySelector('.todo-list');
    list.innerHTML = this.todos.map(todo => `
      <div class="todo-item">
        <input type="checkbox" ${todo.done ? 'checked' : ''} data-id="${todo.id}">
        <span class="${todo.done ? 'done' : ''}">${todo.text}</span>
      </div>
    `).join('');

    list.querySelectorAll('input[type="checkbox"]').forEach(cb => {
      cb.addEventListener('change', () => this.toggleTodo(Number(cb.dataset.id)));
    });
  }
}

customElements.define('todo-list', TodoList);

与框架的对比

特性 Web Components React/Vue 组件
框架依赖 需要框架运行时
浏览器兼容 需 polyfill 框架处理
状态管理 手动管理 内置响应式系统
生态工具 有限 丰富
跨框架复用 天然支持 需封装
数据绑定 手动 自动

总结

Web Components 适合构建跨框架、长期维护的 UI 组件库。虽然开发体验不如主流框架便捷,但它们是浏览器原生标准,没有框架锁定风险。在实际项目中,可以根据需要选择纯 Web Components 或将其作为框架组件的补充。