一、背景与需求

最近一直会有一些这样的需求, 两套完全独立的前端系统,分别基于React和Vue框架开发,用户体系及鉴权体系独立,本次测试将尝试把Vue系统嵌入React中,实现核心交互逻辑:点击切换至React系统时,侧边栏(Aside)渲染React菜单,内容区(Content)加载React组件;切换至Vue系统时,侧边栏与内容区同步渲染Vue对应的菜单及组件,形成视觉与功能统一的集成体验,基础UI如下图:
image.png

二、技术环境

  • Vue技术栈:Vue3 + Vite.js + UnoCss + TypeScript (Vue项目用的是开源的)
  • React技术栈:React17 + Webpack + Sass + TypeScript (React项目是自有的)
  • 后端及部署:Spring Boot + JAVA17 + Docker + MySQL + Redis (Vue项目后台)

三、方案选型

目前微前端领域已有qiankun.js、MicroApp等成熟方案,但也又一定的局限性,本次实践旨在探索更轻量化的浏览器原生方案——Web Component。作为W3C制定的浏览器原生组件化标准,Web Component具备跨框架UI复用与封装能力,无需依赖第三方框架,可天然实现不同技术栈的融合。

四、工程改造实现

4.1 Vue工程改造(Web Component打包)

核心目标是将Vue项目打包为可被React调用的Web Component自定义元素,需新增专属入口文件并配置打包规则。

4.1.1 新增Web Component入口文件

创建src/web-component-entry.ts作为打包入口,封装Vue应用为自定义元素,实现组件的挂载、卸载与属性监听,以下是伪代码:


// src/web-component-entry.ts
import App from './App.vue'
import { createApp, h } from 'vue'

class VueWebComponentElement extends HTMLElement {
  private _app: any = null
  private _reactToken: string = ''

  // 定义需要监听的属性
  static get observedAttributes() {
    return ['mode']
  }

  constructor() {
    super()
    // 监听来自React的事件
    this.addEventListener('app-changed', (e: CustomEvent) => {
      const { token } = e.detail
      this._reactToken = token
    })
  }

  async connectedCallback() {
    if (this._app) return
    // 创建挂载容器并设置样式
    const rootNode = document.createElement('div')
    rootNode.setAttribute('id', 'app-vue')
    rootNode.style.height = '100%'
    this.appendChild(rootNode)

    // 获取属性并初始化Vue应用
    const mode = this.getAttribute('mode') || 'full'
    const app = createApp({
      render() {
        return h(App, { mode })
      }
    })

    // 比如挂载Vue生态依赖(权限、指令、全局组件、Store、Router等)
    app.mount(rootNode)
    this._app = app
  }

  // 属性变化回调
  attributeChangedCallback(name: string, oldValue: string, newValue: string) {
    // 可根据属性变化执行对应逻辑(如样式切换、数据更新)
  }

  // 组件卸载回调
  disconnectedCallback() {
    if (this._app) {
      this._app.unmount()
      delete this._app
    }
  }
}

// 定义自定义元素(避免重复定义)
if (!customElements.get('wc-pvue')) {
  customElements.define('wc-pvue', VueWebComponentElement)
}

export default VueWebComponentElement

4.1.2 Vite打包配置调整

vite.config.ts中新增Web Component打包模式,指定输出格式、入口文件及资源命名规则:

// vite.config.ts部分配置
import { defineConfig, loadEnv, resolve } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig(({ mode }) => {
  const env = loadEnv(mode, process.cwd())
  const isWebComponent = env.VITE_BUILD_MODE === 'webcomponent'

  return {
    plugins: [vue()],
    build: {
      minify: 'terser',
      // 区分Web Component打包目录
      outDir: env.VITE_OUT_DIR && isWebComponent 
        ? `${env.VITE_OUT_DIR}/web-component` 
        : env.VITE_OUT_DIR || 'dist',
      sourcemap: env.VITE_SOURCEMAP === 'true' ? 'inline' : false,
      terserOptions: {
        compress: {
          drop_debugger: env.VITE_DROP_DEBUGGER === 'true',
          drop_console: env.VITE_DROP_CONSOLE === 'true'
        }
      },
      // Web Component专属打包配置
      ...(isWebComponent ? {
        lib: {
          entry: resolve(__dirname, 'src/web-component-entry.ts'),
          name: 'PVue',
          fileName: 'pvue',
          formats: ['umd'] // 输出UMD格式,兼容浏览器环境
        },
        rollupOptions: {
          output: {
            entryFileNames: 'pvue.js',
            assetFileNames: 'pvue.[ext]'
          }
        }
      } : {})
    }
  }
})

注:为简化测试,当前配置未分离Vue运行时依赖,导致最终UMD文件体积偏大。若需优化体积,可通过external配置排除Vue核心依赖,但需在React项目中同步引入对应依赖,确保Vue应用运行环境完整。

4.2 React工程改造(集成Web Component)

React端需通过布局组件控制系统切换逻辑,同时引入Vue打包后的资源文件。

4.2.1 布局组件改造

layout.tsx中通过状态控制渲染逻辑,切换至Vue系统时加载自定义元素<wc-pvue />


import React, { useState } from 'react'
import { Layout } from 'antd' // 假设使用Ant Design布局组件
import SiderMenu from './SiderMenu'
import Header from './Header'
import styles from './layout.module.sass'

const AppLayout = ({ children }: { children: React.ReactNode }) => {
  const [app, setApp] = useState<'react' | 'vue'>('react')

  // 系统切换回调
  const onAppChanged = (targetApp: 'react' | 'vue') => {
    setApp(targetApp)
    // 延迟发送事件,确保Vue组件已渲染
    setTimeout(() => {
      const wcEl = document.querySelector('wc-pvue')
      wcEl?.dispatchEvent(
        new CustomEvent('app-changed', {
          detail: {
            token: (cache.getCache('accessInfo', 'session') as any)?.accessToken,
          },
          bubbles: true,
          composed: true, // 允许事件穿透Shadow DOM
        })
      )
    }, 500)
  }

  return (
    <Layout className={styles['app-layout-wrapper']}>
      <Header onAppChanged={onAppChanged} />
      {app === 'react' ? (<Layout className={styles['app-content-wrapper']}>
          <SiderMenu />
          <Layout>{children}</Layout>
        </Layout>
      ) : (
        // 加载Vue对应的Web Component
        <wc-pvue />
      )}
    </Layout>
  )
}

export default AppLayout

4.2.2 引入Vue资源

在React项目的index.html中引入Vue打包后的CSS与JS文件,确保自定义元素可正常渲染:


<!-- 引入Vue Web Component样式 -->
<link rel="stylesheet" href="vue/pvue.css" /<!-- 引入Vue Web Component脚本 -->

至此,基础嵌入功能实现完成,可通过切换菜单验证两侧系统的渲染效果。

五、关键技术点突破

5.1 样式隔离与覆盖

Web Component天然支持Shadow DOM,可构建独立DOM树实现样式隔离,避免与React主系统样式冲突;Vue端也可通过Scoped CSS限定样式作用域。但实际业务中常需覆盖子系统样式,结合本次Vue项目使用UnoCSS及CSS变量的特性,采用变量覆盖方案实现样式定制:


wc-pvue {
  height: 100%;
  /* 覆盖Vue项目内部CSS变量 */
  --app-footer-height: 0px;
  --tags-view-height: 0px;
  --top-tool-height: 0px;

  /* 隐藏Vue项目中不需要的元素 */
  #v-tool-header,
  #v-tags-view {
    display: none;
  }
}

样式覆盖需结合项目实际场景调整:若无法通过CSS变量或选择器覆盖,需修改Vue项目源码;若涉及主题切换等动态需求,可通过自定义元素属性传递状态,在Vue端监听属性变化同步更新样式。

5.2 跨框架消息通讯

UI层嵌入仅完成视觉整合,跨框架逻辑协同的核心在于消息通讯。常用方案包括全局状态共享(挂载至window)、属性传递、事件驱动等,本次实践采用浏览器原生CustomEvent实现解耦式通讯。

前文实现了React向Vue发送事件传递Token,但通过setTimeout规避渲染时机问题的方案存在不稳定性。更优实践为Vue主动发起通讯:在Vue组件的connectedCallback生命周期中发送就绪事件,React监听该事件后再传递数据,确保渲染与通讯时序一致:


// Vue端:web-component-entry.ts 中修改connectedCallback
async connectedCallback() {
  // 省略原有挂载逻辑...
  // 组件挂载完成后通知React
  this.dispatchEvent(
    new CustomEvent('vue-ready', {
      bubbles: true,
      composed: true
    })
  )
}

// React端:layout.tsx 中监听事件
useEffect(() => {
  const handleVueReady = () => {
    const wcEl = document.querySelector('wc-pvue')
    wcEl?.dispatchEvent(
      new CustomEvent('app-changed', {
        detail: { token: (cache.getCache('accessInfo', 'session') as any)?.accessToken },
        bubbles: true,
        composed: true
      })
    )
  }
  document.addEventListener('vue-ready', handleVueReady)
  return () => document.removeEventListener('vue-ready', handleVueReady)
}, [])

六、实践总结与待解决问题

基于Web Component可实现React与Vue跨栈系统的基础融合,通过自定义元素封装、原生事件通讯、CSS变量覆盖等手段,满足核心交互与样式适配需求。但本次实践仍存在诸多待优化点:

  1. 路由兼容性:React采用BrowserRouter(HTML5 History模式),Vue采用HashRouter,两者路由规则冲突,且页面切换时HTML标题同步、路由守卫协同等问题未解决。可通过统一路由模式(如均采用History模式)、主应用接管路由分发实现兼容。
  2. 统一认证体系:两套系统原有独立登录权限机制,目前仅实现Token传递,未完成身份态同步、权限统一校验等功能,需设计跨系统认证中心或共享令牌机制。
  3. 第三方系统改造限制:本次实践基于可自由修改的开源Vue项目,若需嵌入第三方不可控Vue系统,无法进行源码改造,需探索无侵入式封装方案。

相较于qiankun等成熟微前端框架,Web Component也是一种更轻量化的选择方案, 具体实践依然要根据具体的项目情况来选择和评估。当然,后续抽空还会分享一种基于类似门户系统的iframe融合方案,但不会在浏览器打开新页签,大家还有哪些方案可以分享呢,欢迎留言讨论!

标签: none

添加新评论