Published on

用 Web Components 封装通用组件,并在 Vue 和 React 中复用

Authors
  • avatar
    Name
    Violet Chen
    Twitter

Web Components 是封装通用组件的好工具,能让代码跨框架复用,减少重复工作。基于 HTML5 的 Custom Elements、Shadow DOM 和 HTML Templates,组件像原生标签一样用。

封装一个通用组件

假设我们封装一个自定义按钮组件,支持自定义文本和点击事件。核心是用 customElements.define 注册。

组件代码

写个 JavaScript 文件 custom-button.js

class CustomButton extends HTMLElement {
  constructor() {
    super()
    const shadow = this.attachShadow({ mode: 'open' })

    // 模板
    const template = document.createElement('template')
    template.innerHTML = `
      <style>
        button {
          background: #007bff;
          color: white;
          padding: 8px 16px;
          border: none;
          border-radius: 4px;
          cursor: pointer;
        }
        button:hover {
          background: #0056b3;
        }
      </style>
      <button><slot>Default Text</slot></button>
    `

    shadow.appendChild(template.content.cloneNode(true))

    // 事件监听
    this.button = shadow.querySelector('button')
    this.button.addEventListener('click', () => {
      this.dispatchEvent(
        new CustomEvent('custom-click', { detail: { message: 'Button clicked!' } })
      )
    })
  }

  // 属性观察
  static get observedAttributes() {
    return ['disabled']
  }

  attributeChangedCallback(name, oldValue, newValue) {
    if (name === 'disabled') {
      this.button.disabled = newValue !== null
    }
  }
}

customElements.define('custom-button', CustomButton)

这个组件用 Shadow DOM 隔离样式,<slot> 插槽支持自定义内容。事件用 dispatchEvent 抛出自定义事件。

在 Vue 中复用

Vue 3 支持直接用 Web Components,不用额外插件。导入组件 JS 文件,然后像用标签一样。

Vue 配置

main.js 或组件中导入:

import './custom-button.js'

Vue 组件中使用:

<template>
  <custom-button @custom-click="handleClick">Vue Button</custom-button>
</template>

<script setup>
const handleClick = (event) => {
  console.log(event.detail.message) // 'Button clicked!'
}
</script>

Vue 会自动识别自定义元素。事件用 @custom-click,属性用 :disabled="true" 绑定。

优化

用 Vue 的 v-model 或 props 时,通过 attributeChangedCallback 处理属性变化。测试兼容性,用 vue.config.js 配置忽略自定义元素:

module.exports = {
  chainWebpack: (config) => {
    config.module
      .rule('vue')
      .use('vue-loader')
      .tap((options) => ({
        ...options,
        compilerOptions: {
          isCustomElement: (tag) => tag.startsWith('custom-'),
        },
      }))
  },
}

在 React 中复用

React 也支持 Web Components,直接导入 JS 文件,用 JSX 标签。

React 配置

App.js 或组件中导入:

import './custom-button.js'

使用:

function App() {
  const handleClick = (event) => {
    console.log(event.detail.message) // 'Button clicked!'
  }

  return (
    <custom-button onCustomClick={handleClick} disabled={true}>
      React Button
    </custom-button>
  )
}

事件用驼峰式 onCustomClick,属性用小写。React 17+ 支持自定义事件监听。

优化

ref 操作组件:

const buttonRef = useRef(null)

useEffect(() => {
  buttonRef.current.addEventListener('custom-click', handleClick)
  return () => buttonRef.current.removeEventListener('custom-click', handleClick)
}, [])

;<custom-button ref={buttonRef}>Button</custom-button>

避免 React 警告,用 suppressHydrationWarning 或忽略自定义元素警告。

总结

Web Components 封装通用组件简单,跨 Vue 和 React 复用省事。重点是 Custom Elements 注册、Shadow DOM 隔离和事件分发。优化属性观察和事件处理,就能建高效组件库。