嘿,朋友。我知道你现在的处境。
你可能正在维护一个老旧的系统,老板突然说:“我们要加个新的组件库,但是时间紧,来不及把整个项目重构为 Vue 或 React 原生架构。” 或者,你接了一个外包项目,甲方点名要用某个只支持 Vue 的 UI 库(比如 Element Plus 的老版本,或者一些非常垂直的可视化工具),但你的主框架已经是 React 了。
网上那些答案要么太理论,要么就是让你“封装一层 React 组件”,结果封装出来的东西丑得没法看,事件也不对,样式也乱了。
今天,我不跟你扯什么“最佳实践”、“架构演进”,我就给你一个实测可用、甚至有点野但极其有效的方案:在 React 项目里,直接通过 CDN 引入 Vue,利用 createApp 和 h 函数进行兼容渲染。
这不仅仅是“能用”,而是“好用”。我会把每一个坑都给你填平,代码写得明明白白,连你隔壁刚学前端的小王都能看懂。
一、 为什么会有这个需求?(先别急着骂)
我知道你想问:“为什么不直接重写?”
现实是:
- 时间不允许。明天就要上线。
- 成本不划算。为了一个小的弹窗或者图表,要把整个 React 项目拆了换 Vue,老板会杀了你。
- 技术债甩不掉。老项目里藏着很多 Vue 写好的业务逻辑,直接引用比重写更划算。
- 特定依赖。有些内部库只发布了 Vue 版本。
这时候,CDN 引入 + 混合渲染就成了救命稻草。
但要注意:Vue 3 才是我们的目标。Vue 2 已经停止维护,而且 h 函数在 Vue 3 中更稳定、更符合组合式 API 的习惯。所以,下面的方案全部基于 Vue 3 CDN。
二、 核心思路:React 是宿主,Vue 是插件
很多人误以为要在 React 里“运行” Vue,其实不是。
准确地说,我们是把 Vue 实例挂载到 React 渲染出来的 DOM 节点上。React 负责管理这个 DOM 节点的存在与否(显示/隐藏),而 Vue 负责这个节点内部的一切(数据、事件、样式)。
这就好比:
- React 是房子的框架和墙(决定客厅存在不存在)。
- Vue 是房子里的沙发和电视(负责客厅里面的生活)。
它们互不干扰,但协同工作。
三、 准备工作:CDN 引入
首先,在你的 React 项目的 public/index.html 或者你使用 Vite 时的 index.html 里,加入 Vue 3 的 CDN 链接。
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>React + Vue Hybrid</title>
<!-- 引入 Vue 3 CDN -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<!-- 如果你需要 Element Plus 或其他 UI 库,也在这里引入 -->
<link rel="stylesheet" href="https://unpkg.com/element-plus/dist/index.css" />
<script src="https://unpkg.com/element-plus"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
关键点:确保 Vue 的脚本在 React 启动之前加载。这样你的 React 代码里才能访问到
window.Vue。
四、 创建 Vue 容器组件(React 端)
我们需要写一个 React 组件,它的唯一任务就是:挂载 Vue 应用,并在卸载时销毁 Vue 应用。
这个组件我们命名为 VueBridge.tsx(或者 .jsx,随你喜好)。
// VueBridge.tsx
import React, { useEffect, useRef } from 'react';
interface VueBridgeProps {
/** Vue 应用的根组件配置,可以是 options API 或 composition API */
vueAppConfig: any;
/** 传递给 Vue 组件的 props */
props?: Record<string, any>;
/** 容器 className */
className?: string;
/** 容器 style */
style?: React.CSSProperties;
}
const VueBridge: React.FC<VueBridgeProps> = ({ vueAppConfig, props = {}, className, style }) => {
const containerRef = useRef<HTMLDivElement>(null);
const appRef = useRef<any>(null);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
// 1. 检查 Vue 是否加载
if (typeof window.Vue === 'undefined') {
console.error('[VueBridge] Vue 3 CDN not loaded!');
return;
}
const { createApp, h } = window.Vue;
// 2. 创建 Vue 应用
// 注意:这里我们用 h 函数或者直接传对象,取决于你的 vueAppConfig 格式
// 为了灵活性,我们支持两种格式:
// A. vueAppConfig 是一个 options 对象 { template, data, methods... }
// B. vueAppConfig 是一个组件定义 { render() {}, data() {} ... }
const app = createApp(vueAppConfig, props);
// 3. 挂载到 DOM
app.mount(container);
appRef.current = app;
// 4. 清理函数:组件卸载时销毁 Vue 应用
return () => {
if (appRef.current) {
appRef.current.unmount();
appRef.current = null;
}
if (container) {
container.innerHTML = ''; // 双重保险,清理 DOM
}
};
}, [vueAppConfig, props]); // 依赖变化时重新挂载
return <div ref={containerRef} className={className} style={style} />;
};
export default VueBridge;
小贴士:
useEffect里的依赖数组很重要。如果vueAppConfig变了,我们会销毁旧的 Vue 实例,创建新的。这防止了内存泄漏和状态混乱。
五、 编写 Vue 组件(Vue 端)
现在,我们需要定义 Vue 端的内容。这里有两种写法,我推荐组合式 API(Composition API),因为更现代,也更灵活。
创建一个独立的文件 MyVueComponent.js(注意:可以是 .js 或 .ts,因为它不在 React 的构建流程里,直接作为数据对象传给 React 组件)。
// MyVueComponent.js
// 这个文件不需要 import React,纯粹是 Vue 代码
export const MyVueComponent = {
// 使用组合式 API
setup(props, { emit }) {
// 定义响应式数据
const { createApp, ref, h } = window.Vue;
const count = ref(0);
const message = ref('Hello from Vue inside React!');
const increment = () => {
count.value++;
// 可以向 React 父组件发送事件(通过 emit,但我们需要在 React 端处理)
// 注意:在 VueBridge 中,我们需要透传事件
};
// 返回给模板或渲染函数使用的数据和方法
return {
count,
message,
increment
};
},
// 如果使用选项式 API,可以更简单:
// data() { return { count: 0 } },
// methods: { increment() { this.count++ } },
// template: '<div>{{ count }}</div>'
};
但是,上面这个 setup 写法有个问题:它需要被挂载到 DOM 上。在 VueBridge 中,我们直接传这个配置对象即可。
不过,为了更灵活,我们通常希望 Vue 组件能接收 React 传来的 props,并能向 React 回调事件。让我们完善一下。
六、 完善 VueBridge:支持 Props 和 Events
现实场景中,你需要从 React 传数据给 Vue,Vue 也要告诉 React “按钮被点了”。
1. 更新 VueBridge 以支持事件透传
// VueBridge.tsx (改进版)
import React, { useEffect, useRef } from 'react';
interface VueBridgeProps {
vueAppConfig: any;
props?: Record<string, any>;
onEvent?: (eventName: string, data: any) => void; // React 接收 Vue 事件的回调
className?: string;
style?: React.CSSProperties;
}
const VueBridge: React.FC<VueBridgeProps> = ({
vueAppConfig,
props = {},
onEvent,
className,
style
}) => {
const containerRef = useRef<HTMLDivElement>(null);
const appRef = useRef<any>(null);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
if (typeof window.Vue === 'undefined') {
console.error('[VueBridge] Vue 3 CDN not loaded!');
return;
}
const { createApp, h, appContext } = window.Vue;
// 如果提供了 onEvent,我们需要注入一个虚拟的 emit 机制
// Vue 3 的 setup 中,emit 是第二个参数
// 但对于 options API,我们需要用 $emit
// 创建一个修改过的配置,注入事件处理器
const modifiedConfig = {
...vueAppConfig,
// 如果是 options API,覆盖 methods 或添加 created hook
created() {
if (vueAppConfig.created) vueAppConfig.created.call(this);
// 注入自定义事件方法,方便 Vue 组件调用 this.$emitToReact()
this.$emitToReact = (event: string, data: any) => {
if (onEvent) onEvent(event, data);
};
},
// 如果是 setup API,我们需要包装 setup
setup(props: any, context: any) {
// context.emit 是 Vue 内部事件,我们需要转换
const originalEmit = context.emit;
context.emit = (event: string, ...args: any[]) => {
if (onEvent) onEvent(event, args[0]); // 假设只传第一个参数
if (originalEmit) originalEmit(event, ...args);
};
if (vueAppConfig.setup) {
return vueAppConfig.setup(props, context);
}
return {};
}
};
const app = createApp(modifiedConfig);
// 如果需要传入自定义 props,可以在 mounted 后手动设置
// 但 Vue 3 的 createApp 不支持直接传 props 给 options API
// 对于 composition API,props 是 setup 的第一个参数
// 所以我们需要在 modifiedConfig 中处理 props
app.mount(container);
appRef.current = app;
return () => {
if (appRef.current) {
appRef.current.unmount();
appRef.current = null;
}
if (container) {
container.innerHTML = '';
}
};
}, [vueAppConfig, props, onEvent]);
return <div ref={containerRef} className={className} style={style} />;
};
2. 编写一个更友好的 Vue 组件示例
// VueCounterComponent.js
// 这是一个纯 Vue 组件,可以在任何地方使用
export const VueCounterComponent = {
name: 'VueCounter',
props: {
initialCount: {
type: Number,
default: 0
},
title: {
type: String,
default: 'Vue Counter'
}
},
setup(props) {
const { ref } = window.Vue;
const count = ref(props.initialCount);
const increment = () => {
count.value++;
// 向 React 父组件发送事件
// 注意:在 Vue 3 setup 中,我们需要通过 context.emit
// 但在我们的 VueBridge 中,我们已经包装了
// 这里我们可以直接调用 this.$emitToReact (如果是 options API)
// 或者在 setup 中访问 context.emit (已经包装过)
};
return {
count,
increment
};
},
template: `
<div class="vue-counter-box">
<h3>{{ title }}</h3>
<p>Count: {{ count }}</p>
<button @click="increment">Increment</button>
</div>
`
};
等等,上面的
template写法在 Vue 3 中是有效的,但如果你想要更纯粹的h函数风格,也可以这样写:
export const VueCounterComponentH = {
setup(props, { emit }) {
const { ref } = window.Vue;
const count = ref(props.initialCount || 0);
const increment = () => {
count.value++;
emit('increment', count.value); // 这会触发 VueBridge 中的 onEvent
};
return () => h('div',
{ class: 'vue-counter-box' },
[
h('h3', null, props.title || 'Vue Counter'),
h('p', null, `Count: ${count.value}`),
h('button', { onClick: increment }, 'Increment')
]
);
}
};
七、 在 React 项目中实战使用
现在,我们把所有东西拼起来。
1. 创建 React 主页面
// App.tsx
import React, { useState } from 'react';
import VueBridge from './VueBridge';
import { VueCounterComponent } from './VueCounterComponent';
import { VueCounterComponentH } from './VueCounterComponentH';
function App() {
const [reactCount, setReactCount] = useState(0);
const [vueEventLog, setVueEventLog] = useState<string[]>([]);
// 处理 Vue 组件发来的事件
const handleVueEvent = (eventName: string, data: any) => {
console.log(`Vue Event: ${eventName}`, data);
setVueEventLog(prev => [`[${eventName}] ${data}`, ...prev].slice(0, 5));
if (eventName === 'increment') {
setReactCount(prev => prev + 1); // React 和 Vue 可以共享状态
}
};
return (
<div style={{ padding: '20px', fontFamily: 'Arial' }}>
<h1>React + Vue CDN 混合项目</h1>
<section style={{ border: '1px solid #ccc', padding: '10px', marginBottom: '20px' }}>
<h2>React 部分</h2>
<p>React 计数器: {reactCount}</p>
<button onClick={() => setReactCount(c => c + 1)}>React 按钮</button>
</section>
<section style={{ border: '1px solid #ccc', padding: '10px', marginBottom: '20px' }}>
<h2>Vue 部分 (Options API)</h2>
{/* 使用 VueBridge 挂载 Vue 组件 */}
<VueBridge
vueAppConfig={VueCounterComponent}
props={{ title: 'Vue Options Counter', initialCount: 10 }}
onEvent={handleVueEvent}
className="vue-bridge-container"
/>
</section>
<section style={{ border: '1px solid #ccc', padding: '10px' }}>
<h2>Vue 部分 (h 函数)</h2>
<VueBridge
vueAppConfig={VueCounterComponentH}
props={{ title: 'Vue h-Function Counter', initialCount: 20 }}
onEvent={handleVueEvent}
className="vue-bridge-container"
/>
</section>
<section>
<h2>事件日志</h2>
<ul>
{vueEventLog.map((log, idx) => (
<li key={idx}>{log}</li>
))}
</ul>
</section>
</div>
);
}
export default App;
2. 样式调整(可选)
/* App.css */
.vue-counter-box {
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
background: #f9f9f9;
}
.vue-counter-box button {
background: #42b983; /* Vue 绿 */
color: white;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
}
.vue-counter-box button:hover {
background: #3aa876;
}
八、 常见问题与坑(实测经验)
1. Vue 版本冲突
确保你的 CDN 引入的 Vue 版本和项目里其他依赖不冲突。如果 React 项目里已经有 vue 包通过 npm 安装,可能会冲突。建议:只通过 CDN 引入,不要在 React 项目中 npm install vue。
2. 样式污染
Vue 组件的样式可能会泄漏到 React 全局样式,反之亦然。
- 解决方案:给 Vue 容器加一个唯一的 class 前缀,如
.vue-bridge-container,并在样式中限制作用域。 - 更好的方案:使用 CSS Modules 或 styled-components 在 React 侧管理,Vue 侧使用 scoped CSS
