Web Components原生组件Custom Element与Shadow DOM封装实践

Web Components原生组件Custom ElementShadow DOM封装实践

Web Components是浏览器原生支持的组件化标准,由Custom Element、Shadow DOM、HTML Template三个核心API组成。与React/Vue等框架组件不同,Web Components不依赖任何框架运行时,可直接在任何HTML页面中使用。本文从Custom Element生命周期钩子、Shadow DOM样式隔离到表单集成和框架兼容,给出完整的开发实战指南。

Custom Element生命周期钩子与属性响应

Custom Element通过customElements.define注册后,浏览器在解析到对应标签时自动实例化。关键生命周期回调:

class TooltipBox extends HTMLElement {
  constructor() {
    super();
    this._visible = false;
    this._content = '';
  }

  connectedCallback() {
    this.render();
    this.addEventListener('mouseenter', this._show);
    this.addEventListener('mouseleave', this._hide);
  }

  disconnectedCallback() {
    this.removeEventListener('mouseenter', this._show);
    this.removeEventListener('mouseleave', this._hide);
  }

  static get observedAttributes() {
    return ['content', 'position'];
  }

  attributeChangedCallback(name, oldVal, newVal) {
    if (oldVal === newVal) return;
    switch (name) {
      case 'content':
        this._content = newVal;
        this._updateContent();
        break;
      case 'position':
        this._updatePosition(newVal);
        break;
    }
  }

  _show = () => {
    this._visible = true;
    this._toggle();
  };

  _hide = () => {
    this._visible = false;
    this._toggle();
  };

  _toggle() {
    const tip = this.shadowRoot.querySelector('.tip');
    if (tip) tip.style.display = this._visible ? 'block' : 'none';
  }

  render() {
    this.innerHTML = '<slot></slot>';
  }
}

customElements.define('tooltip-box', TooltipBox);

使用方式:

<tooltip-box content="提示文字" position="top">悬停查看</tooltip-box>

注意点:自定义标签名必须包含连字符(-),这是规范强制要求;connectedCallback可能被多次触发(如DOM移动),需处理重复绑定问题。

Shadow DOM样式隔离与CSS自定义属性穿透

Shadow DOM创建独立的DOM子树,外部CSS无法穿透到内部,内部样式也不会泄漏到外部。这是Web Components样式封装的核心机制。

class CardItem extends HTMLElement {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.shadowRoot.innerHTML = `
      <style>
        :host {
          display: block;
          border-radius: 8px;
          overflow: hidden;
          background: var(--card-bg, #ffffff);
          border: 1px solid var(--card-border, #e0e0e0);
          box-shadow: var(--card-shadow, 0 2px 8px rgba(0,0,0,0.1));
        }
        .header {
          padding: 16px;
          font-size: 18px;
          font-weight: 600;
          border-bottom: 1px solid var(--card-border, #e0e0e0);
        }
        .body { padding: 16px; }
        ::slotted(h3) {
          margin: 0;
          color: var(--card-title-color, #1a1a1a);
        }
      </style>
      <div class="header">
        <slot name="header"></slot>
      </div>
      <div class="body">
        <slot></slot>
      </div>
    `;
  }
}

customElements.define('card-item', CardItem);

使用方式及外部样式穿透:

<style>
  card-item {
    --card-bg: #f8f9fa;
    --card-border: #dee2e6;
    --card-title-color: #212529;
  }
</style>

<card-item>
  <span slot="header">标题区域</span>
  <p>内容区域</p>
</card-item>

:host选择器匹配组件宿主元素,::slotted选择器匹配插槽中分发进来的内容。CSS自定义属性是Shadow DOM内外通信的唯一样式通道。

表单集成与ElementInternals API

Web Components默认不参与表单提交,需通过ElementInternals API将自定义元素注册为表单控件:

class RatingInput extends HTMLElement {
  static formAssociated = true;

  constructor() {
    super();
    this.attachShadow({ mode: 'open' });
    this._value = 0;
  }

  connectedCallback() {
    this.shadowRoot.innerHTML = `
      <style>
        .star { cursor: pointer; font-size: 24px; display: inline-block; }
        .star.active { color: #ffc107; }
        .star.inactive { color: #ddd; }
      </style>
      <div class="stars"></div>
    `;
    this._renderStars();
  }

  get internals() {
    return this.attachInternals();
  }

  set value(val) {
    this._value = val;
    this.internals.setFormValue(String(val));
    this.internals.setValidity({});
    this._renderStars();
  }

  get value() { return this._value; }

  formResetCallback() { this.value = 0; }

  formDisabledCallback(disabled) {
    this.shadowRoot.querySelectorAll('.star').forEach(s => {
      s.style.pointerEvents = disabled ? 'none' : 'auto';
      s.style.opacity = disabled ? '0.5' : '1';
    });
  }

  _renderStars() {
    const container = this.shadowRoot.querySelector('.stars');
    container.innerHTML = '';
    for (let i = 1; i <= 5; i++) {
      const star = document.createElement('span');
      star.className = 'star ' + (i <= this._value ? 'active' : 'inactive');
      star.textContent = '\u2605';
      star.addEventListener('click', () => { this.value = i; });
      container.appendChild(star);
    }
  }
}

customElements.define('rating-input', RatingInput);

使用方式:

<form id="reviewForm">
  <label>评分:<rating-input name="rating"></rating-input></label>
  <button type="submit">提交</button>
</form>

<script>
  document.getElementById('reviewForm').addEventListener('submit', (e) => {
    e.preventDefault();
    const fd = new FormData(e.target);
    console.log('Rating:', fd.get('rating'));
  });
</script>

框架兼容性与打包发布策略

Web Components在React中使用有一个已知问题:React 18及之前版本不会将自定义属性传递给自定义元素。解决方案是使用@lit/react包生成React绑定:

import { createComponent } from '@lit/react';
import React from 'react';
import { CardItem } from './card-item.js';

export const CardItemReact = createComponent({
  tagName: 'card-item',
  elementClass: CardItem,
  react: React,
  eventName: { onCardClick: 'card-click' },
});

Vue 3对Web Components有原生支持,通过compilerOptions.isCustomElement配置即可:

// vite.config.js
export default defineConfig({
  vue: {
    template: {
      compilerOptions: {
        isCustomElement: (tag) => tag.includes('-'),
      },
    },
  },
});

打包发布时,每个Web Component建议独立输出为一个JS文件,通过ES Module按需加载。组件样式内联在Shadow DOM中,无需外部CSS文件,这天然避免了样式冲突问题。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/webcomponents-yuan-sheng-zu-jian-customelement-yu-shadowdom/

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

相关推荐