Web Components自定义元素实战:Shadow DOM与跨框架组件封装

Web Components是浏览器原生支持的组件化标准,由Custom Elements、Shadow DOM和HTML Templates三部分组成。与React/Vue等框架组件不同,Web Components不依赖任何运行时库,浏览器直接解析执行。在需要跨框架复用组件、构建与框架无关的设计系统、或开发嵌入式微件时,Web Components提供了一条技术中立路径。

Custom Elements定义与生命周期回调

自定义元素通过customElements.define注册,类继承HTMLElement并实现生命周期回调:

class RatingStars extends HTMLElement {
  constructor() {
    super();
    this._value = 0;
    this._max = 5;
  }

  // 元素插入DOM时调用
  connectedCallback() {
    this.render();
    this.attachEvents();
  }

  // 元素移除DOM时调用
  disconnectedCallback() {
    this.cleanup();
  }

  // 监听的属性变化
  static get observedAttributes() {
    return ['value', 'max', 'readonly'];
  }

  // 属性变化时调用
  attributeChangedCallback(name, oldVal, newVal) {
    if (name === 'value') {
      this._value = Number(newVal) || 0;
      this.updateDisplay();
    }
    if (name === 'max') {
      this._max = Number(newVal) || 5;
      this.render();
    }
  }

  render() {
    // 渲染逻辑
  }

  attachEvents() {
    // 事件绑定
  }

  cleanup() {
    // 清理定时器、事件监听等
  }
}

customElements.define('rating-stars', RatingStars);

生命周期回调的执行顺序:constructor(元素创建时)-> connectedCallback(插入DOM时)-> attributeChangedCallback(属性变化时)-> disconnectedCallback(移除时)。observedAttributes声明需要追踪的属性,未声明的属性变化不会触发回调。

使用方式与原生HTML元素一致:

<rating-stars value="3" max="5"></rating-stars>

Shadow DOM封装与样式隔离

Shadow DOM创建独立的DOM子树,外部CSS选择器和JavaScript无法穿透Shadow边界,实现真正的样式隔离。在constructor中开启Shadow DOM:

constructor() {
  super();
  const shadow = this.attachShadow({ mode: 'open' });
  
  const style = document.createElement('style');
  style.textContent = `
    :host {
      display: inline-flex;
      gap: 4px;
    }
    .star {
      font-size: 24px;
      color: #ccc;
      cursor: pointer;
      transition: color 0.2s;
    }
    .star.active {
      color: #f5a623;
    }
    .star:hover {
      color: #f5a623;
    }
  `;
  
  const container = document.createElement('div');
  container.className = 'container';
  
  shadow.appendChild(style);
  shadow.appendChild(container);
  this._container = container;
}

:host选择器匹配自定义元素本身,相当于Shadow DOM内的this。Shadow DOM内的样式不会泄漏到外部,外部全局CSS的类名也不会影响Shadow DOM内的元素。

mode: 'open'允许外部通过element.shadowRoot访问Shadow DOM,便于调试。mode: 'closed'则禁止外部访问,安全性更高但调试不便。生产环境推荐open模式配合文档说明。

HTML Templates与slot插槽机制

使用<template>定义可复用的DOM结构,配合<slot>实现内容分发:

<template id="card-template">
  <style>
    .card {
      border: 1px solid #e0e0e0;
      border-radius: 8px;
      padding: 16px;
      box-shadow: 0 2px 4px rgba(0,0,0,0.08);
    }
    .card-header {
      font-weight: 600;
      margin-bottom: 8px;
    }
    ::slotted([slot="title"]) {
      color: #1a1a1a;
    }
  </style>
  <div class="card">
    <div class="card-header">
      <slot name="title">默认标题</slot>
    </div>
    <div class="card-body">
      <slot>默认内容</slot>
    </div>
  </div>
</template>

<script>
class InfoCard extends HTMLElement {
  constructor() {
    super();
    const shadow = this.attachShadow({ mode: 'open' });
    const template = document.getElementById('card-template');
    shadow.appendChild(template.content.cloneNode(true));
  }
}
customElements.define('info-card', InfoCard);
</script>

<!-- 使用 -->
<info-card>
  <span slot="title">用户信息</span>
  <p>这是卡片内容</p>
</info-card>

::slotted选择器样式化被插入到slot中的元素。cloneNode(true)深拷贝template内容,每次实例化都获得独立的DOM副本。具名slot(name="title")匹配slot="title"的子元素,默认slot(无name属性)接收未指定slot的子元素。

与React和Vue集成互操作

Web Components可以被任何框架直接使用。在React中使用时,需注意React对自定义元素属性传递的限制——React不会自动将非标准属性映射到DOM属性:

// React封装组件
function RatingInput({ value, max, onChange }) {
  const ref = useRef(null);

  useEffect(() => {
    const el = ref.current;
    el.addEventListener('rating-change', (e) => {
      onChange(e.detail.value);
    });
    return () => {
      el.removeEventListener('rating-change', () => {});
    };
  }, [onChange]);

  return (
    <rating-stars
      ref={ref}
      value={value}
      max={max}
    />
  );
}

// React 19+ 直接支持自定义元素属性
// React 18及以下需要手动设置属性
useEffect(() => {
  if (ref.current) {
    ref.current.value = value;
    ref.current.max = max;
  }
}, [value, max]);

自定义元素通过CustomEvent向外发送事件,React通过addEventListener监听。Vue中使用更简洁,@rating-change可直接绑定:

<template>
  <rating-stars
    :value="rating"
    :max="5"
    @rating-change="onRatingChange"
  />
</template>

<script setup>
const rating = ref(3);
function onRatingChange(e) {
  rating.value = e.detail.value;
}
</script>

Vue的v-model无法直接绑定自定义元素,需要通过:value@event手动实现双向绑定。Vue 3.3+的defineCustomElement可将Vue组件编译为Web Components:

import { defineCustomElement } from 'vue';

const MyButton = defineCustomElement({
  props: ['label', 'type'],
  emits: ['click'],
  template: `<button :class="'btn-' + type" @click="$emit('click')">{{ label }}</button>`
});

customElements.define('my-button', MyButton);

浏览器兼容与Polyfill策略

Web Components的核心API在Chrome、Firefox、Safari和Edge中已全面支持。对于需要兼容旧浏览器的项目,使用@webcomponents/webcomponentsjspolyfill:

<script type="module">
  if (!customElements || !Element.prototype.attachShadow) {
    await import('@webcomponents/webcomponentsjs/webcomponents-bundle.js');
    await customElements.whenDefined('rating-stars');
  }
</script>

polyfill仅在不支持时按需加载,减少现代浏览器的额外开销。customElements.whenDefined返回Promise,确保元素注册完成后再执行依赖逻辑。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/webcomponents-zi-ding-yi-yuan-su-shi-zhan-shadowdom-yu-kua/

(0)
小编小编
上一篇 9小时前
下一篇 9小时前

相关推荐