每天一个高级前端知识 - Day 6

4 阅读3分钟

每天一个高级前端知识 - Day 6

今日主题:Web Components 4.0 - 跨框架组件开发的终极方案

核心概念:真正的一次编写,随处运行

Web Components 不是框架,是浏览器原生标准。2025年的 Web Components 已经进化到 4.0,解决了以往的所有痛点。

🔍 Web Components 四大核心

Custom Elements (自定义元素)
      ↓
Shadow DOM (样式隔离)
      ↓
HTML Templates (模板复用)
      ↓
HTML Imports / ES Modules (模块化)

🚀 2025新特性:声明式 Shadow DOM

<!-- 过去:必须用JS构建 -->
<my-component></my-component>
<script>
  class MyComponent extends HTMLElement {
    constructor() {
      super();
      this.attachShadow({ mode: 'open' });
      this.shadowRoot.innerHTML = `<style>...</style>`;
    }
  }
</script>

<!-- 现在:声明式,DOM直接解析 -->
<my-component>
  <template shadowrootmode="open">
    <style>
      :host { display: block; }
      .title { color: blue; }
    </style>
    <div class="title">
      <slot name="title">默认标题</slot>
    </div>
    <slot></slot>
  </template>
  <span slot="title">自定义标题</span>
  这段内容会进入默认slot
</my-component>

💡 跨框架通信实战

// 定义一个全局通用的用户卡片组件
class UserCard extends HTMLElement {
  static observedAttributes = ['user-id', 'theme'];
  
  constructor() {
    super();
    // 声明式Shadow DOM已经存在,不需要动态创建
    this.shadow = this.shadowRoot; // 声明式方式,shadowRoot自动存在
    this.elements = {};
    
    // 响应式数据绑定
    this.state = {
      user: null,
      loading: false
    };
  }
  
  // 属性变化时自动触发
  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'user-id' && newValue !== oldValue) {
      this.fetchUser(newValue);
    }
    if (name === 'theme') {
      this.updateTheme(newValue);
    }
  }
  
  async fetchUser(id) {
    this.state.loading = true;
    this.render();
    
    const user = await fetch(`/api/users/${id}`).then(r => r.json());
    this.state = { user, loading: false };
    this.render();
  }
  
  // 事件系统 - 供外部监听
  dispatchUserEvent(eventName, detail) {
    this.dispatchEvent(new CustomEvent(eventName, { 
      detail, 
      bubbles: true, 
      composed: true  // 穿透Shadow DOM
    }));
  }
  
  render() {
    if (this.state.loading) {
      this.shadow.querySelector('.loading').style.display = 'block';
      this.shadow.querySelector('.content').style.display = 'none';
      return;
    }
    
    const user = this.state.user;
    this.shadow.querySelector('.loading').style.display = 'none';
    this.shadow.querySelector('.content').style.display = 'block';
    this.shadow.querySelector('.name').textContent = user.name;
    this.shadow.querySelector('.email').textContent = user.email;
  }
  
  updateTheme(theme) {
    this.shadow.host.style.setProperty('--card-theme', theme === 'dark' ? '#1a1a1a' : '#ffffff');
  }
}

// 注册组件
customElements.define('user-card', UserCard);
// 在任何框架中使用(React/Vue/Angular/Svelte都支持)

// React中使用
function App() {
  const cardRef = useRef();
  
  useEffect(() => {
    const card = cardRef.current;
    const handleUserUpdate = (e) => {
      console.log('用户更新:', e.detail);
    };
    
    card.addEventListener('user-update', handleUserUpdate);
    return () => card.removeEventListener('user-update', handleUserUpdate);
  }, []);
  
  return (
    <user-card 
      ref={cardRef}
      user-id="123"
      theme="dark"
      onUserUpdate={(e) => console.log(e.detail)}
    />
  );
}

// Vue中使用
<template>
  <user-card 
    ref="userCard"
    :user-id="userId"
    :theme="theme"
    @user-update="handleUserUpdate"
  />
</template>

<script setup>
import { ref } from 'vue';
const userId = ref('123');
const handleUserUpdate = (e) => console.log(e.detail);
</script>

🎯 高级模式:组合式组件

// 构建复杂的组合组件
class AccordionGroup extends HTMLElement {
  constructor() {
    super();
    // 管理子组件
    this.items = new Map();
  }
  
  connectedCallback() {
    // 监听子组件添加
    const observer = new MutationObserver((mutations) => {
      for (const mutation of mutations) {
        for (const node of mutation.addedNodes) {
          if (node.nodeType === 1 && node.matches?.('accordion-item')) {
            this.registerItem(node);
          }
        }
      }
    });
    
    observer.observe(this, { childList: true, subtree: true });
  }
  
  registerItem(item) {
    if (this.items.has(item)) return;
    
    this.items.set(item, {
      button: item.shadowRoot.querySelector('button'),
      content: item.shadowRoot.querySelector('.content')
    });
    
    // 确保只有一个打开
    item.addEventListener('toggle', (e) => {
      if (e.detail.expanded) {
        for (const [otherItem] of this.items) {
          if (otherItem !== item && otherItem.expanded) {
            otherItem.collapse();
          }
        }
      }
    });
  }
}

customElements.define('accordion-group', AccordionGroup);

📦 组件库构建最佳实践

// 1. 使用CSS自定义属性实现主题化
// styles/theme.css
:root {
  --wc-primary: #0066cc;
  --wc-border-radius: 8px;
  --wc-spacing: 1rem;
}

// 组件内使用
:host {
  --button-bg: var(--wc-primary);
  background: var(--button-bg);
  border-radius: var(--wc-border-radius);
  padding: var(--wc-spacing);
}

// 2. 提供完整的TypeScript类型定义
// index.d.ts
declare namespace JSX {
  interface IntrinsicElements {
    'user-card': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement> & {
      'user-id'?: string;
      theme?: 'light' | 'dark';
      onUserUpdate?: (e: CustomEvent) => void;
    }, HTMLElement>;
  }
}

// 3. 使用构建工具打包单文件
// rollup.config.js
export default {
  input: 'src/index.js',
  output: {
    dir: 'dist',
    format: 'es',
    sourcemap: true
  },
  plugins: [
    css({ inject: false }), // 提取CSS
    litcss() // 优化Shadow DOM CSS
  ]
};

🎯 今日挑战

实现一个跨框架的表单验证组件库

  1. <form-validator>:管理整个表单验证
  2. <input-field>:带验证规则的输入框
  3. 支持规则:required, email, minlength, 自定义正则
  4. 提供统一的事件系统(valid/invalid)
  5. 在React和Vue中分别使用
// 使用示例
<form-validator>
  <input-field name="email" rules="required|email">
    <input type="email" placeholder="邮箱" />
    <span slot="error">请输入有效的邮箱</span>
  </input-field>
  
  <input-field name="password" rules="required|minlength:8">
    <input type="password" />
    <span slot="error">密码至少8位</span>
  </input-field>
  
  <button type="submit">提交</button>
</form-validator>

<script>
document.querySelector('form-validator').addEventListener('valid', () => {
  console.log('表单验证通过');
});
</script>
核心实现提示
class FormValidator extends HTMLElement {
  constructor() {
    super();
    this.inputs = new Map();
  }
  
  registerInput(name, element, rules) {
    this.inputs.set(name, { element, rules, value: '' });
  }
  
  validate() {
    let isValid = true;
    for (const [name, { element, rules }] of this.inputs) {
      const value = element.value;
      const errors = this.runValidation(value, rules);
      
      if (errors.length > 0) {
        isValid = false;
        element.setAttribute('invalid', '');
        element.dispatchEvent(new CustomEvent('invalid', { detail: { errors } }));
      } else {
        element.removeAttribute('invalid');
        element.dispatchEvent(new CustomEvent('valid'));
      }
    }
    
    this.dispatchEvent(new CustomEvent(isValid ? 'valid' : 'invalid'));
    return isValid;
  }
}

📊 Web Components 4.0 vs 主流框架

特性Web ComponentsReactVue
跨框架复用⭐⭐⭐⭐⭐ 原生⭐ React only⭐ Vue only
样式隔离⭐⭐⭐⭐⭐ Shadow DOM⭐ CSS-in-JS⭐ Scoped CSS
包体积⭐⭐⭐⭐⭐ 0KB120KB80KB
性能⭐⭐⭐⭐ 接近原生⭐⭐⭐ 虚拟DOM⭐⭐⭐⭐ 编译优化
开发体验⭐⭐⭐ 需熟悉标准⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐
SSR支持⭐⭐⭐ 部分支持⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐

明日预告:WebGPU 实战 - 浏览器中的海量粒子系统,从十万到百万粒子!

💡 核心优势:Web Components 是唯一能让组件"写一次,在任何现代框架和原生HTML中都完美运行"的标准!