一、文档概述
1.1 编写目的
本文档详细记录在存量 Vue 项目中接入 React 组件库的可复用技术方案,明确架构设计、实现细节、上线流程及注意事项 ,为其他 Vue 项目向 React 技术栈渐进式迁移提供标准化参考
1.2 需求背景
- 技术栈战略切换 :为降低长期维护成本,前端技术栈将逐步统一为 React ,存量 Vue 项目需分阶段迁移
- 项目特性适配 :本项目为 Vue 技术栈中架构最简单、页面数量最少的应用,且团队 React 熟练度高于 Vue,适合作为迁移标杆
- 迁移约束:需避免一次性重构带来的风险,确保业务连续性,支持按页面逐步迁移
1.3 需求目标
- 开发体验无妥协 :React 业务代码编写无需兼容 Vue ,支持 React 原生语法、Hooks、组件复用
- 组件库和平共存 :React 组件库与现有 Vue 组件库样式隔离,无 CSS 冲突
- 渐进式迁移 :支持单页面独立迁移,不影响未迁移的 Vue 页面功能
- 开发效率保障 :支持 React 热更新(Fast Refresh )、React DevTools 调试,工具链适配 React 生态
二、系统概要设计方案
2.1 核心技术决策框架
迁移方案需解决 "框架共存、样式隔离、路由兼容、数据互通、开发体验" 五大核心问题,决策框架如下
| 决策维度 | 核心诉求 | 技术选型方向 |
|---|---|---|
| 技术栈主次 | 最小改动、快速上线 | 保持 Vue 为主技术栈,React 作为次技术栈嵌入 |
| 样式隔离 | 组件库 CSS 无冲突,弹窗类组件正常渲染 | React 组件库支持 prefixCls 自定义 |
| 路由方案 | 按页面路由区分 Vue / React 渲染 | 复用 Vue Router ,React 通过 Vue 容器挂载 |
| 数据互通 | React 页面获取 Vue Store 数据 | 封装跨框架 Store API,提供类型安全调用 |
| 国际化 | 两套框架共用国际化方案 | 复用 Vue 项目现有 i18n 函数,组件库独立配置 |
| 开发体验 | 热更新、DevTools 调试 | SWC 编译 React 代码,集成 React Refresh |
2.2 微前端方案否定分析
微前端虽支持多框架共存,但针对本项目场景存在明显劣势
- 改造成本高 :需搭建微前端主应用、申请独立构建流程、配置跨应用路由,沟通与开发成本超预期
- 架构复杂度提升 :简单项目引入微前端会增加技术栈复杂度,后续维护需掌握微前端相关知识
- 扩展性局限 :若推广至已采用微前端的复杂项目,会出现 "微前端嵌套微前端" 的嵌套问题,兼容性风险高
结论 :放弃微前端方案,采用 "Vue 主框架 + React 嵌入容器" 的轻量架构,通过桥接层 解决路由、Store、国际化等跨框架问题
2.3 技术栈主次选择
两种方案对比
| 方案 | 核心优势 | 核心劣势 | 适配性评分 |
|---|---|---|---|
| React 为主技术栈 | 一步到位,迁移完成后无 Vue 环境残留 | 需重构 Vue Layout 、路由、Store,改动量大,冲突风险高 | 3 / 5 |
| Vue 为主技术栈 + React 嵌入 | 最小化业务改动,支持按页面迁移,快速上线 | Vue 环境无法彻底清除,长期维护存在少量冗余 | 5 / 5 |
最终选择
Vue 为主技术栈,React 通过专用容器嵌入
选择理由
- 业务开发无需关注底层架构(Layout 、路由、Store ),仅需专注 React 页面开发
- 项目迭代周期短,大篇幅重构易导致代码冲突与测试成本激增
- 残留 Vue 环境不影响 React 开发体验,后续可逐步清理,风险可控
2.4 样式隔离方案深度解析
四种方案对比
| 方案 | 实现原理 | 优势 | 劣势 | 适配性 |
|---|---|---|---|---|
| Shadow DOM | 组件 DOM 树隔离,样式仅作用于内部 | 完全隔离,无样式污染 | 弹窗 / 浮层组件无法挂载到 document.body,功能异常 | ❌ |
| 属性选择器 | 为 CSS 添加 [data-app-name] 后缀 |
实现简单,基于 Vue scoped 思想 | 挂载到 body 的组件丢失属性,样式失效 | ❌ |
| PostCSS 前缀 | 构建时为选择器添加自定义前缀 | 无运行时开销 | 弹窗组件样式丢失,需手动适配 | ❌ |
组件库 prefixCls 配置 |
通过 ConfigProvider 修改组件默认 className 前缀 | 适配所有组件(含弹窗),无样式丢失 | 需组件库支持前缀自定义 | ✅ |
最终方案:组件库 prefixCls 自定义
- React 组件库改造:通过 ConfigProvider 配置
prefixCls为ssc-react。eg:Button 组件 className 从ssc-button改为ssc-react-button - Less 变量同步:Vue 项目中修改 Less 全局变量
@prefixCls: 'ssc-react',确保 React 组件样式前缀一致 - Vue 组件库保持不变:Vue 组件沿用原有前缀
ssc,与 React 组件样式天然隔离
2.5 路由方案设计
核心思路 :复用 Vue Router ,通过 "Vue 容器组件 + React 挂载" 实现路由区分
- 定义 React 专属容器(Vue 组件),在
mounted生命周期中通过ReactDOM.render挂载 React 应用 - 路由配置时,通过工厂函数 包装 React 页面,标记路由元信息 (
isReactPage: true) - 根组件
<router-view>根据路由元信息 动态添加容器类名,适配 React / Vue 页面样式
2.6 Store 跨框架访问方案
Vue 项目的 Store 的核心数据(枚举值、用户信息、权限 等)通过封装无框架依赖的 API 供 React 页面调用,避免直接操作 Vuex
| 数据类型 | 封装方式 | 调用示例 |
|---|---|---|
| 枚举值(enums) | 自定义 Hook useEnums,支持缓存与类型提示 |
useEnums('CageStatus').getValue('CountScanned') |
| 用户 / 权限数据 | 工具函数 storeAPI,封装 Vuex 读取逻辑 |
storeAPI.getCurrentWhs()、storeAPI.hasSoupPermission() |
2.7 国际化方案设计
1. 业务国际化(无框架依赖)
Vue 项目现有 i18n 函数为纯 JS 实现,React 页面可直接导入使用,语言切换时通过 Vue Layout 的强制刷新机制同步状态
2. 组件库国际化(框架专属)
- React 组件库通过
getCurrentLocale()函数获取 Vue 项目当前语言 - 借助 React 组件库的 ConfigProvider 注入语言包,实现组件国际化同步
2.8 开发体验保障方案
为确保 React 页面开发体验与纯 React 项目一致,需在复用现有 Vue 工程化体系的基础上,针对性解决 React 热更新、调试工具适配等核心诉求
2.8.1 基础工具链复用
项目采用的 Vue CLI Service (底层基于 Webpack )和 ESLint 代码约束体系,对 React 生态具备天然兼容性
- 构建工具 :Vue CLI Service 无需大幅改造即可处理 React 代码,仅需新增针对性的文件编译规则
- 代码规范 :通过扩展 ESLint 配置(引入
eslint-plugin-react / eslint-plugin-react-hooks),可直接复用现有 ESLint 流程约束 React 代码风格与语法规范
2.8.2 React DevTools 调试支持
React DevTools 的调试能力与页面中是否存在 Vue 代码无关,核心依赖 ReactDOM.render(或ReactDOM.createRoot)挂载的 React 组件树
- 只要在开发环境下通过标准 React DOM API 挂载 React 应用,React DevTools 即可自动识别组件结构、Hook 状态、Props 等调试信息
- 无需额外配置,开发人员可像调试纯 React 项目一样使用断点调试、组件性能分析等功能
2.8.3 React Fast Refresh 热更新实现
Fast Refresh 是 React 开发的核心体验诉求(修改代码后保留组件状态并快速刷新),但 Vue CLI Service 默认使用 ts-loader 处理 TSX 文件,其对应的 Fast Refresh 解决方案 存在兼容性问题(如 Hooks 状态丢失、配置复杂)。结合团队其他项目已验证的成熟实践,决定采用 SWC(Speedy Web Compiler) 替代 ts-loader 编译 React TSX 文件,实现稳定的 Fast Refresh
Fast Refresh 核心原理(基于 Webpack HMR 协议 + React Runtime 支持)
- 监听文件变更 :Vue CLI Service(Webpack) 监听
.react.tsx文件修改,通过 WebSocket 将更新通知发送至浏览器端 - 注入热更新代码 :react-refresh-webpack-plugin 在编译阶段为 React 代码注入热更新处理逻辑,暴露
window.$RefreshReg$和window.$RefreshSig$全局函数 - SWC 编译增强 :SWC 在转换 React TSX 代码时,为每个组件生成与热更新绑定的
$RefreshReg$ / $RefreshSig$方法,该方法会调用react-refresh / runtime提供的核心 API - 运行时刷新 :
react-refresh / runtime的performReactRefresh方法通过 React 内核的状态保留机制,仅更新修改的组件,而非全页面刷新,最终实现无状态丢失的热更新
热更新的底层实现细节可参考 :A rough guide to manually implement React fast refresh
2.9 方案核心架构图
typescript
┌───────────────────────────────────────────────────────────────────────────────────────────────┐
│ Vue主应用(核心基座) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────────────┐ │
│ │ Vue Router │ │ Vuex Store │ │ i18n 国际化 │ │ Webpack生态 │ │
│ │ - 路由配置 │ │ - 核心数据 │ │ - 纯JS函数 │ │ - react-refresh插件 │ │
│ │ - 路由守卫 │ │ - 枚举值 │ │ - 语言切换 │ │ - SWC编译规则 │ │
│ │(区分框架) │ │(全局状态) │ │(无框架依赖)│ │ - 规则拆分 │ │
│ └─────┬───────┘ └───────┬─────┘ └───────┬─────┘ └───────────────────┘ │
│ │ │ │ │
│ ┌─────▼────────┐ ┌──────▼─────┐ ┌──────▼────────────────────┐ ┌────────────────────────┐ │
│ │ 路由桥接层 │ │ 数据桥接层 │ │ 国际化桥接层 │ │ 样式隔离层 │ │
│ │- 标记React路由│ │ - storeAPI │ │ - 业务i18n复用 │ │ - Vue组件库:ssc │ │
│ │- 容器类名切换 │ │ - useEnums │ │ - React组件库ConfigProvider│ │ - React组件库:ssc-react│ │
│ └─────┬────────┘ └───────┬────┘ └───────────────────────────┘ └─────────────────────────┘ │
│ │ │ │
│ ┌─────▼───────┐ ┌─────────▼─────────────────┐ ┌───────────────┐ ┌──────────────────┐ │
│ │ Vue业务层 │ │ React容器层 │ │ Vue组件库 │ │ 开发体验层 │ │
│ │- 原有.vue页面│ │ - Vue组件载体 │ │ - 原生prefixCls│ │ - Fast Refresh │ │
│ │- 历史业务逻辑│ │ - ReactDOM.createRoot挂载 │ └────────────────┘ │ - React DevTools │ │
│ └─────────────┘ └───────┬───────────────────┘ └─────────────────┘ │
│ │ │
│ ┌───────────────────▼────────────────────────────────────────────────────┐ │
│ │ React业务层 │ │
│ │ ┌───────────────┐ ┌─────────────────────────────┐ │ │
│ │ │ 编译层 │ │ 核心依赖 │ │ │
│ │ │-SWC编译器 │ │ - react/react-dom │ │ │
│ │ │-.react.tsx后缀│ │ - react-refresh/runtime │ │ │
│ │ └──────────────┘ └─────────────────────────────┘ │ │
│ │ ┌────────────────────────────────┐ │ │
│ │ │ 组件层 │ │ │
│ │ │-React组件库(prefixCls=ssc-react)│ │ │
│ │ │-自定义React业务组件(.react.tsx) │ │ │
│ │ └─────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────────────────────────────────────────────┘
1. 顶层基座:Vue 主应用
保留原有核心能力,同时扩展工具链生态,其中
- Vue Router 承担路由分发职责,通过 "路由守卫" 区分 Vue / React 页面
- Vuex Store 作为全局数据源,为跨框架提供数据支撑
- Webpack 生态 新增
react-refresh插件和 SWC 编译规则,是 Fast Refresh 的核心依赖。
2. 跨框架桥接层(核心适配层)
解决 Vue 与 React 的协同问题,是方案的核心设计
- 路由桥接层 :通过 "标记 React 路由" 和 "容器类名切换" ,实现路由与 UI 容器的联动
- 数据桥接层 :
storeAPI封装用户 / 权限数据访问,useEnums处理枚举值,提供类型安全的跨框架数据调用 - 国际化桥接层 :业务层面直接复用 Vue 的 i18n 纯 JS 函数,React 组件库通过 ConfigProvider 注入语言包
- 样式隔离层 :通过
prefixCls差异化(Vue:ssc / React:ssc-react),实现组件库样式无冲突
3. 业务与容器层
- React 容器层 :本质是 Vue 组件,通过
ReactDOM.createRoot挂载 React 应用,是 Vue 与 React 的 "衔接载体" - Vue 业务层 :保留原有
.vue页面和历史逻辑,无任何改造,确保业务连续性
4. React 业务层
独立的 React 开发环境,完全遵循 React 生态规范
- 编译层 :通过 SWC 编译器处理
.react.tsx后缀文件,替代ts-loader实现高效编译与 Fast Refresh - 组件层 :包含 React 组件库(已适配
prefixCls)和自定义业务组件,开发体验与纯 React 项目一致 - 核心依赖 :明确
react-refresh / runtime为 Fast Refresh 的运行时依赖,确保热更新功能稳定。
5. 开发体验层
聚焦 React 开发的核心诉求,包含 Fast Refresh (状态保留式热更新)和 React DevTools(组件调试)
三、系统详细设计方案
3.1 项目基建升级
1. 依赖冲突解决
问题
npm run serve 启动失败,因单元测试库依赖 deasync 包,而 M1 Mac (ARM 架构)缺少对应的预构建包
解决方案
升级单元测试库至无 deasync 依赖的版本,同步升级相关依赖(如 jest、vue-test-utils)
2. 开发环境配置
- 安装 React 核心依赖 :
react、react-dom、@types/react、@types/react-dom - 安装构建依赖 :
swc-loader、react-refresh-webpack-plugin、@swc/core - 安装代码规范依赖 :
eslint-plugin-react、eslint-plugin-react-hooks
3.2 React 容器实现(核心代码)
1. React 容器工厂函数
typescript
// src/utils/react-container.tsx
import React, { lazy, Suspense } from 'react';
import ReactDOM from 'react-dom/client';
import { ConfigProvider } from 'react-component-library'; // 假设 React 组件库
import { getCurrentLocale } from '@/i18n';
import { storeAPI } from '@/utils/store-api';
// React 组件库全局配置(含样式前缀、国际化)
const ReactAppWrapper = ({ children }: { children: React.ReactNode }) => {
const locale = getCurrentLocale(); // 获取 Vue 项目当前语言
return (
<ConfigProvider
prefixCls="ssc-react" // 样式前缀,与 Less 变量一致
locale={locale} // 组件库国际化
>
<Suspense fallback={<div>加载中...</div>}>
{children}
</Suspense>
</ConfigProvider>
);
};
/**
* React 容器工厂函数
* @param component React 页面动态导入函数
* @param props 传递给 React 页面的 props
* @returns Vue 组件配置
*/
export default (
component: () => Promise<{ default: React.ComponentType<any> }>,
props?: Record<string, unknown>
) => () =>
Promise.resolve({
name: 'ReactContainer',
render: (h: Vue.CreateElement) => h('div', { attrs: { id: 'react-root' } }),
mounted() {
// 挂载 React 应用
const RootComponent = lazy(component);
const root = ReactDOM.createRoot(document.getElementById('react-root')!);
root.render(
<React.StrictMode>
<ReactAppWrapper>
<RootComponent {...props} storeAPI={storeAPI} />
</ReactAppWrapper>
</React.StrictMode>
);
// 保存 root 实例,用于卸载
(this as any).reactRoot = root;
},
beforeDestroy() {
// 卸载 React 应用,避免内存泄漏
(this as any).reactRoot.unmount();
},
});
2. 路由标记与配置
typescript
// src/utils/declare-router-react.ts
export const reactPageKey = 'isReactPage';
/**
* 路由装饰器:标记 React 页面路由
* @param router Vue Router 配置
* @returns 标记后的路由配置
*/
export const declareRouterReact = (router: any) => {
const { children, ...rest } = router;
return {
...rest,
meta: { ...rest.meta, [reactPageKey]: true }, // 添加 React 页面标记
children: children?.map(declareRouterReact),
};
};
// src/router/index.ts
import { declareRouterReact } from '@/utils/declare-router-react';
import reactContainer from '@/utils/react-container';
export default new VueRouter({
routes: [
// Vue 页面(原有)
{
path: '/vue-page',
component: () => import('@/views/vue-page/index.vue'),
},
// React 页面(新增)
declareRouterReact({
path: '/react-page',
component: reactContainer(() => import('@/views/react-page/index.react.tsx')),
}),
],
});
3. 根组件路由视图适配
typescript
<!-- src/App.vue -->
<template>
<div id="app">
<router-view
:class="{
'ssc-ui-vue-container': !isReactPage,
'ssc-ui-react-container': isReactPage,
}"
/>
</div>
</template>
<script lang="ts">
import Vue from 'vue';
import { reactPageKey } from '@/utils/declare-router-react';
export default Vue.extend({
computed: {
isReactPage() {
return !!this.$route.meta?.[reactPageKey];
},
},
});
</script>
<style lang="less">
// Vue 页面容器样式(原有)
.ssc-ui-vue-container {
padding: 20px;
}
// React 页面容器样式(新增)
.ssc-ui-react-container {
padding: 20px;
// 适配 React 组件库默认样式
font-size: 14px;
line-height: 1.5;
}
</style>
4. Webpack 配置修改(vue.config.js)
typescript
const { defineConfig } = require('@vue/cli-service');
const ReactRefreshWebpackPlugin = require('@pmmmwh/react-refresh-webpack-plugin');
module.exports = defineConfig({
configureWebpack: (config) => {
const isDevelopment = process.env.NODE_ENV === 'development';
// 1. 新增 React TSX 文件处理规则(.react.tsx 后缀)
config.module.rules.push({
test: /\.react\.tsx$/,
use: [
{
loader: 'swc-loader',
options: {
jsc: {
parser: {
syntax: 'typescript',
tsx: true,
decorators: true,
dynamicImport: true,
},
transform: {
react: {
runtime: 'automatic', // 自动导入 React
refresh: isDevelopment, // 开发环境启用 Fast Refresh
},
},
},
},
},
],
});
// 2. 修改原有 TSX 规则,排除 .react.tsx 文件
const tsxRule = config.module.rules.find(rule => rule.test.test('.tsx'));
if (tsxRule) {
tsxRule.test = /^(.{0,4}|((?!react).){5,})\.tsx$/; // 不匹配.react.tsx
}
// 3. 开发环境添加 React Refresh 插件
if (isDevelopment) {
config.plugins.push(new ReactRefreshWebpackPlugin());
}
return config;
},
// 3. 修改 Less 全局变量(适配 React 组件库前缀)
css: {
loaderOptions: {
less: {
modifyVars: {
prefixCls: 'ssc-react', // 与 React 组件库 ConfigProvider 配置一致
},
javascriptEnabled: true,
},
},
},
});
3.3 Store 跨框架访问实现
1. storeAPI 封装(类型安全)
typescript
// src/utils/store-api.ts
import Vue from 'vue';
import { safeGet } from '@/utils/tool';
// 全局 Vue 实例(通过 Vue 原型获取)
const vm = new Vue();
export const storeAPI = {
/** 获取当前仓库 ID */
getCurrentWhs: (): string => {
return safeGet(vm.$store.state, 'user.userInfo.current_whs', '');
},
/** 获取仓库列表 */
getWhsList: (): string[] => {
return safeGet(vm.$store.state, 'user.userInfo.whs_setting', []);
},
/** 检查是否有 Soup 权限 */
hasSoupPermission: (): boolean => {
const perms = safeGet(vm.$store.state, 'perms', []);
return perms.includes('soup:manage');
},
/** 获取当前用户角色 */
getCurrentRole: (): string => {
return safeGet(vm.$store.state, 'user.userInfo.role', 'user');
},
};
// 类型声明(确保 TS 类型提示)
declare global {
interface Window {
storeAPI: typeof storeAPI;
}
}
window.storeAPI = storeAPI;
2. useEnums Hook 封装(枚举值处理)
typescript
// src/hooks/useEnums.ts
import { useMemo } from 'react';
import Vue from 'vue';
import { safeGet } from '@/utils/tool';
// 枚举值返回类型
export interface EnumsValueType {
/** 获取枚举值JSON */
getJSON: () => Record<string, number>;
/** 根据值获取文本 */
getText: (value: string | number) => string;
/** 根据文本获取值 */
getValue: (text: string) => number;
/** 转换为下拉选项(冻结防止修改) */
toOptions: () => ReadonlyArray<{ label: string; value: number }>;
}
// 枚举值缓存(避免重复计算)
const enumsCache = new Map<string, EnumsValueType>();
/**
* 自定义 Hook:获取枚举值
* @param region 枚举值区域(如UserProductionLine)
* @returns 枚举值操作方法
*/
export function useEnums(region: string): EnumsValueType {
return useMemo(() => {
if (enumsCache.has(region)) {
return enumsCache.get(region)!;
}
const vm = new Vue();
// 从 Vuex 获取枚举值(格式:{ key: 文本, value: 值 })
const enums = safeGet(vm.$store.state, `enums.systemEnums.${region}`, []);
const enumMap = Object.fromEntries(enums.map((item: any) => [item.key, item.value]));
const options = enums.map((item: any) => ({ label: item.key, value: item.value }));
const result: EnumsValueType = {
getJSON: () => ({ ...enumMap }),
getText: (value) => {
const entry = enums.find((item: any) => item.value === value);
return entry?.key || '';
},
getValue: (text) => enumMap[text] || -1,
toOptions: () => Object.freeze(options), // 冻结数组,防止篡改
};
enumsCache.set(region, result);
return result;
}, [region]);
}
3.4 国际化实现细节
1. 业务国际化(直接复用)
typescript
// src/views/react-page/index.react.tsx
import React from 'react';
import { Button, Table } from 'react-component-library';
import { i18n } from '@/i18n';
import { useEnums } from '@/hooks/useEnums';
export default function ReactPage() {
const productionLineEnums = useEnums('UserProductionLine');
// 表格列配置(复用 i18n 函数)
const columns = [
{ title: i18n('Production Line'), dataIndex: 'line', key: 'line' },
{ title: i18n('Status'), dataIndex: 'status', key: 'status' },
];
return (
<div>
<h1>{i18n('Production Line Management')}</h1>
<Button type="primary">{i18n('Create')}</Button>
<Table columns={columns} dataSource={[]} />
</div>
);
}
2. 组件库国际化(ConfigProvider 注入)
typescript
// src/utils/react-container.tsx(补充国际化配置)
import { ConfigProvider } from 'react-component-library';
import { getCurrentLocale } from '@/i18n';
// 导入 React 组件库语言包
import zhCN from 'react-component-library/es/locale/zh-CN';
import enUS from 'react-component-library/es/locale/en-US';
const localeMap = {
'zh-CN': zhCN,
'en-US': enUS,
};
const ReactAppWrapper = ({ children }: { children: React.ReactNode }) => {
const currentLocale = getCurrentLocale(); // 获取Vue项目当前语言
const componentLocale = localeMap[currentLocale as keyof typeof localeMap] || zhCN;
return (
<ConfigProvider
prefixCls="ssc-react"
locale={componentLocale} // 注入组件库语言包
>
<Suspense fallback={<div>加载中...</div>}>
{children}
</Suspense>
</ConfigProvider>
);
};
3.5 兼容性与风险控制
1. 现有业务影响评估
| 影响点 | 风险等级 | 应对措施 |
|---|---|---|
| 原有 TSX 文件编译异常 | 低 | 项目无 .react.tsx 后缀文件,修改路由规则无影响 |
| Vue 与 React 样式冲突 | 低 | 组件库前缀不同,通过测试用例覆盖常见场景 |
| 热更新失效 | 中 | 开发环境测试所有 React 页面热更新功能 |
| 浏览器兼容性 | 低 | SWC 编译输出 ES5 代码,支持 IE11+ |
2. 测试重点
- 样式隔离测试 :Vue 组件与 React 组件同屏渲染,检查样式是否冲突
- 弹窗组件测试 :React 组件库的 Modal、Message 等挂载到 body 上的组件是否正常渲染
- 热更新测试 :修改 React 组件代码,检查是否快速刷新且状态保留
- 路由切换测试 :Vue 页面与 React 页面切换,检查是否正常挂载 / 卸载
四、上线方案
4.1 上线流程
- 开发阶段 :完成基建改造、React 容器实现、Store / 国际化桥接层开发
- 自测阶段 :开发人员验证 React 页面功能、样式、热更新、调试工具可用性
- 测试阶段 :测试团队执行兼容性测试、功能测试、回归测试(重点验证未迁移 Vue 页面)
- UAT 部署 :部署至 UAT 环境,业务方验收 React 页面功能
- 生产部署:验收通过后,与常规迭代一起部署至生产环境
4.2 回滚方案
若生产环境出现 React 相关异常,直接注释 React 页面路由配置,恢复为 Vue 页面路由;
紧急情况下,可通过构建脚本排除 React 相关依赖,快速回滚至纯 Vue 版本
五、总结
5.1 方案核心特点
本技术方案以 "最小侵入、渐进迁移、体验无损" 为核心,成功实现 Vue 项目与 React 组件库的无缝融合,具备以下显著优势
- 渐进式迁移,风险可控 :支持按页面独立迁移,无需一次性重构全量业务,未迁移的 Vue 页面功能不受任何影响,确保业务连续性
- 样式隔离彻底,无兼容性冲突 :通过
prefixCls差异化配置(Vue 组件库:ssc ;React 组件库:ssc-react),从根源解决两套组件库的样式冲突,弹窗、浮层等特殊组件渲染正常 - 开发体验与纯 React 一致 :基于 SWC 编译实现 React Fast Refresh 热更新,支持 React DevTools 调试,业务开发无需妥协语法特性与开发效率
- 对现有业务零侵入 :仅通过新增基建配置、桥接层 实现跨框架协同,原有 Vue 业务代码、组件库、路由逻辑无需修改
- 方案高度可复用 :架构设计不依赖特定业务场景,桥接层、编译配置、样式隔离 等核心逻辑可直接复用于其他 Vue 转 React 的迁移项目
5.2 关键技术创新点
方案的核心价值源于对跨框架协同问题的精准解决,关键技术点包括
- Vue 容器化挂载 React :设计 React 容器工厂函数,通过 Vue 组件的生命周期钩子(mounted / beforeDestroy )实现 React 应用的挂载与卸载,无缝集成 Vue Router
- 路由装饰器标记机制 :通过装饰器模式为 React 页面路由添加元信息,实现路由与 UI 容器类名的联动,适配框架专属样式
- 文件后缀差异化策略 :采用
.react.tsx后缀区分 React 组件,避免被vue-loader误编译,同时为 SWC 编译提供明确匹配规则 - SWC 编译替代方案 :替换
ts-loader为 SWC 编译器,解决 React Fast Refresh 兼容性问题,编译效率提升数倍 - 跨框架数据桥接层 :封装
storeAPI与useEnumsHook ,提供类型安全的 Vuex 数据访问能力,隔绝 React 页面与 Vuex 的直接依赖 - 双层国际化适配 :业务层面复用 Vue 纯 JS 国际化函数,组件库层面通过 ConfigProvider 注入语言包,实现全场景国际化同步
5.3 适用场景
本方案适用于以下技术转型与项目改造场景
- Vue 存量项目渐进式迁移 React:页面数量多、业务复杂,无法承受一次性重构风险的项目
- Vue 与 React 组件库共存需求 :需在 Vue 项目中引入 React 生态组件库,且要求样式无冲突
- 团队技术栈切换过渡期 :团队逐步从 Vue 转向 React ,需保障过渡期内双框架协同开发效率
- 小型 Vue 项目标杆迁移 :架构简单、页面数量少的 Vue 项目,适合作为技术栈切换的试点项目,验证方案可行性后推广
5.4 落地注意事项
为确保方案稳定落地,需严格遵循以下规范与约束
- 文件命名强制规范 :React 组件、页面必须使用
.react.tsx后缀,避免编译冲突 - 路由配置强制标记 :所有 React 页面路由必须通过
declareRouterReact装饰器标记,确保容器类名正确切换 - 样式隔离配置校验 :上线前需验证 React 组件库的
prefixCls与 Less 全局变量@prefixCls一致,避免样式泄漏 - 跨框架数据访问约束 :React 页面禁止直接读取
window.$store或 Vue 实例,必须通过storeAPI与useEnums访问数据 - 国际化使用规范 :业务代码统一使用 Vue 项目的
i18n函数,React 组件库国际化通过全局 ConfigProvider 配置,禁止单独引入语言包 - 依赖版本兼容性 :控制 React、SWC 、
react-refresh-webpack-plugin等核心依赖版本,避免与 Vue CLI 生态产生版本冲突
5.5 后续优化方向
为进一步提升方案的完备性与易用性,建议后续开展以下优化工作
- 完善
Store API:基于 Vuex 模块定义,逐步完善storeAPI的功能 - 枚举值类型自动同步 :对接后端枚举值接口,在构建阶段生成 TypeScript 类型文件,确保
useEnumsHook 类型准确性 - 工具链性能优化 :基于项目实际使用场景,优化 SWC 编译配置、Webpack 缓存策略,进一步提升 React 页面构建与热更新速度
- 迁移工具链开发 :开发 Vue 组件转 React 组件的自动化脚本(如模板语法转换、生命周期映射),降低手动迁移工作量
- 最佳实践文档完善 :补充 React 组件开发规范、跨框架调试指南、常见问题排查手册,形成完整的迁移知识库
- 多环境适配增强 :扩展方案对 Vue 3 + Vite 项目的支持,适配更多工程化体系,提升方案复用范围