一、为什么需要跨框架组件复用

前端开发中,我们经常会遇到一个问题:某个团队用 Vue,另一个团队用 React,还有一个项目可能直接用原生 HTML+JS。如果每个框架都重新实现一遍相同的组件(比如按钮、弹窗、表格),不仅浪费人力,还容易导致样式和功能不一致。

Web Components 是浏览器原生支持的组件化方案,它不依赖任何框架,可以在任何技术栈中使用。而 Vue 本身也支持将组件编译成 Web Components,这就为跨框架复用提供了可能。

二、Vue 组件如何变成 Web Components

Vue 提供了一个官方工具 @vue/web-component-wrapper,可以把 Vue 组件包装成标准的 Web Components。下面是一个完整的示例:

// 技术栈:Vue 3 + @vue/web-component-wrapper

// 1. 安装依赖
// npm install @vue/web-component-wrapper vue

// 2. 创建 Vue 组件
import { defineCustomElement } from 'vue'
import MyButton from './MyButton.vue'

// 3. 将 Vue 组件转换为 Web Component
const MyButtonElement = defineCustomElement(MyButton)

// 4. 注册自定义元素
customElements.define('my-button', MyButtonElement)

对应的 MyButton.vue 文件:

<template>
  <button @click="handleClick">
    <slot></slot>  <!-- 支持插槽 -->
  </button>
</template>

<script>
export default {
  props: {
    type: String  // 支持属性传递
  },
  methods: {
    handleClick() {
      this.$emit('custom-click')  // 支持事件触发
    }
  }
}
</script>

现在,这个 <my-button> 组件可以在任何 HTML 页面中使用,即使是没有 Vue 的环境:

<!-- 在纯 HTML 中使用 -->
<my-button type="primary">点击我</my-button>

<script>
document.querySelector('my-button').addEventListener('custom-click', () => {
  console.log('按钮被点击了!')
})
</script>

三、在 React/Angular 等其他框架中使用

由于 Web Components 是浏览器原生支持的,所以在 React 或 Angular 中也可以直接使用。

在 React 中使用

// 技术栈:React

function App() {
  return (
    <div>
      <my-button type="primary" 
        onCustomClick={() => console.log('React 中捕获到点击')}>
        我是 React 中的按钮
      </my-button>
    </div>
  )
}

在 Angular 中使用

// 技术栈:Angular

@Component({
  selector: 'app-root',
  template: `
    <my-button type="primary" (custom-click)="handleClick()">
      我是 Angular 中的按钮
    </my-button>
  `
})
export class AppComponent {
  handleClick() {
    console.log('Angular 中捕获到点击')
  }
}

四、技术细节与注意事项

1. 属性与事件的限制

  • 属性:Web Components 只支持字符串属性,如果需要传递复杂数据,可以用 JSON 序列化。
  • 事件:自定义事件名会被转换为全小写(比如 customClick 会变成 customclick)。

2. 样式隔离

默认情况下,Vue 组件的样式会被封装在 Shadow DOM 中,这意味着外部样式不会影响组件内部。如果需要全局样式穿透,可以这样设置:

defineCustomElement(MyButton, {
  shadowRoot: false  // 禁用 Shadow DOM
})

3. 浏览器兼容性

虽然现代浏览器都支持 Web Components,但如果你需要支持 IE11 或老旧浏览器,需要额外引入 polyfill:

<script src="https://unpkg.com/@webcomponents/webcomponentsjs@2.0.0/webcomponents-bundle.js"></script>

五、适用场景与优缺点

适合的场景

  1. 微前端架构:不同子应用使用不同框架,但需要共享基础组件。
  2. UI 组件库:希望一套组件能同时支持 Vue、React、Angular 等多个框架。
  3. 渐进式迁移:从 Vue 迁移到其他框架时,可以先用 Web Components 过渡。

优点

  • 一次编写,到处运行:无需为不同框架重写组件。
  • 无框架依赖:即使未来技术栈变更,组件仍然可用。
  • 性能较好:浏览器原生支持,没有额外的运行时开销。

缺点

  • 功能受限:相比原生 Vue 组件,某些高级特性(如指令、作用域插槽)无法使用。
  • 开发体验稍差:调试和类型支持不如直接使用 Vue 方便。

六、完整示例:一个跨框架的弹窗组件

下面我们实现一个更复杂的例子——支持 Vue、React、Angular 的弹窗组件。

<!-- MyDialog.vue -->
<template>
  <div v-if="visible" class="dialog-overlay">
    <div class="dialog-content">
      <h2>{{ title }}</h2>
      <slot></slot>
      <button @click="close">关闭</button>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    title: String,
    visible: Boolean
  },
  methods: {
    close() {
      this.$emit('close')
    }
  }
}
</script>

<style>
.dialog-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0,0,0,0.5);
}
.dialog-content {
  background: white;
  padding: 20px;
  margin: 100px auto;
  max-width: 500px;
}
</style>

包装成 Web Component:

import { defineCustomElement } from 'vue'
import MyDialog from './MyDialog.vue'

const MyDialogElement = defineCustomElement(MyDialog)
customElements.define('my-dialog', MyDialogElement)

在 React 中使用:

function App() {
  const [show, setShow] = useState(false)
  
  return (
    <div>
      <button onClick={() => setShow(true)}>打开弹窗</button>
      <my-dialog 
        title="React 中的弹窗" 
        visible={show}
        onClose={() => setShow(false)}>
        <p>这是从 React 传入的内容</p>
      </my-dialog>
    </div>
  )
}

七、总结

通过将 Vue 组件编译为 Web Components,我们可以轻松实现组件的跨框架复用。虽然这种方式有一定的功能限制,但对于基础 UI 组件(按钮、弹窗、输入框等)已经足够。如果你的团队正在使用多种前端框架,或者计划未来迁移技术栈,这个方案值得尝试。

关键点回顾:

  1. 使用 defineCustomElement 包装 Vue 组件
  2. 注意属性和事件的命名规则
  3. 考虑样式隔离和浏览器兼容性
  4. 最适合基础 UI 组件的共享