Use React in Vue3 and Vue3 in React, And as perfect as possible!
npm install vaexy
antd, element-ui, vuetify
sh
Install with yarn:
$ yarn add vaexy
or with npm:
$ npm i vaexy -S
`
项目的预配置
理论上,不需要在 React 项目中做额外的配置来支持 Vue,也不需要在 Vue 项目中做额外的配置来支持 React。
如果要转换的 React 或 Vue 组件来自 npm 包,或者已经经过构建(不是直接的vue文件以及不含有jsx),则可以直接使用 applyPureReactInVue 或 applyVueInReact。
如果需要在一个项目中同时开发 Vue 和 React(一个项目源码中同时存在.vue文件和react jsx文件),而不是仅仅使用现有的 npm 组件,那么应该做一些配置。
$3
如何配置由'@vue/cli'创建的vue项目支持开发react
如何配置由'create-react-app'创建的react项目支持开发vue
$3
如果项目是通过vite构建的,那么需要对vite.config.js做如下配置
首先安装 @vitejs/plugin-react, @vitejs/plugin-vue 和 @vitejs/plugin-vue-jsx。
- 主项目是vue:
`js
import { defineConfig } from 'vite'
// >= vaexy@1.0.2
import vaexyVitePlugins from 'vaexy/vite/index.js'
// 如果是vite 6, 应该使用 vaexy/vite/esm
// import vaexyVitePlugins from 'vaexy/vite/esm'
export default defineConfig({
plugins: [
// 关闭 vue 和 vuejsx 插件
// vue(),
// vueJsx(),
// type设为vue时, 所有名为react_app目录中的文件的jsx将被react jsx编译,其他文件里的jsx将以vue jsx编译
vaexyVitePlugins({
type: 'vue'
})
]
})
`
- The main project is React:
`js
import { defineConfig } from 'vite'
// >= vaexy@1.0.2
import vaexyVitePlugins from 'vaexy/vite/index.js'
export default defineConfig({
plugins: [
// 关闭 react 插件
// react(),
// type设为react时,所有.vue文件里的jsx将以vue jsx进行编译,其他文件的jsx都是以react jsx编译
vaexyVitePlugins({
type: 'react'
})
]
})
`
如果想自定义vue jsx编译的范围, 可以将type设置为custom,然后通过设置vueJsxInclude 和 vueJsxExclude来自定义编译范围
`js
import { defineConfig } from 'vite'
// >= vaexy@1.0.2
import vaexyVitePlugins from 'vaexy/vite/index.js'
export default defineConfig({
plugins: [
vaexyVitePlugins({
type: 'custom',
// 所有.vue文件中的jsx以及在名为vue_app目录里的jsx文件都将以vue jsx编译
vueJsxInclude: [/vue&type=script&lang\.[tj]sx$/i, /vue&type=script&setup=true&lang\.[tj]sx$/i, /[/\\]vue_app[\\/][\w\W]+\.[tj]sx$/],
// vueJsxExclude: []
})
]
})
`
用法
$3
`jsx
import {applyVueInReact, applyPureVueInReact} from 'vaexy'
// This is a Vue component
import BasicVueComponent from './Basic.vue'
import {useState} from 'react'
// Use HOC 'applyVueInReact'
const BasicWithNormal = applyVueInReact(BasicVueComponent)
// Use HOC 'applyPureVueInReact'
const BasicWithPure = applyPureVueInReact(BasicVueComponent)
export default function () {
const [foo] = useState('Hello!')
return <>
the default slot
the default slot
>
}
`
$3
现在推荐使用applyPureReactInVue代替applyReactInvue.
了解 applyPureReactInVue 和 applyReactInVue 的区别
`vue
children内容
`
$3
`jsx
import {applyVueInReact} from 'vaexy'
import BasicVue from './Basic.vue'
import {useState} from 'react'
const Basic = applyVueInReact(BasicVue)
export default function () {
function onClickForVue() {
console.log('clicked!')
}
return
{/在Vue组件Basic中可以使用$emit('click')触发这个事件绑定的函数/}
}
`
$3
`vue
`
$3
这个插槽的用法与Vue的jsx传递插槽的用法非常相似
`jsx
import {applyVueInReact} from 'vaexy'
import BasicVue from './Basic.vue'
const Basic = applyVueInReact(BasicVue)
export default function () {
const vSlots = {
// 在Vue组件的template内使用' '进行渲染
slot1: this is slot1(namedSlot),
// 在Vue组件的template内使用' '进行渲染
slot2: ({value}) => this is slot2(scopedSlot), and receive value: {value},
// 在Vue组件的template内使用' '进行渲染
default: this is children
}
return
{/只传递children/}
{/ 在Vue组件的template内使用' '进行渲染 /}
this is children
{/传递 v-slots/}
{/另一种用法/}
{vSlots}
}
`
$3
Vue3的具名插槽和作用域插槽 = React render props.
Vue3的默认插槽和children = React props.children.
一个带有node:前缀的具名插槽 = React Node
`vue
插槽1 (render props)
插槽2 (render props)
从React组件传递的内容: {{bar}}
插槽3 (react node)
默认插槽children (react node)
`
$3
Vaexy 会判断如果一个组件的外层有同一个框架的组件存在,那么Vaexy 就会使用 React 的 Portal 或者 Vue 的 Teleport创建被高阶组件包装的目标组件,而不是每次都创建一个新的应用实例。
这是非常牛逼的做法! Vaexy 可以很好地将根节点的上下文跨过不同的框架组件传递给内部与根节点相同框架的组件.
这意味着一个 Vue 组件使用了一个 React 组件,然后这个 React 组件使用了另一个 Vue 子组件。 这个 Vue 子组件可以获取外部 Vue 组件的上下文。
#### React组件使用Vue组件 - Provider / useContext 的用法
`jsx
import {applyVueInReact} from 'vaexy'
import BasicVue from './Basic.vue'
import {createContext, useContext} from 'react'
const Basic = applyVueInReact(BasicVue)
// 创建 React context 对象
const Context = createContext({})
// React子组件
function SubReactComponent() {
// 获取 context 值
const {bossName} = useContext(Context)
return bossName from Context: {bossName}
}
export default function () {
// 设置 context 值
return
{/ Vue组件Basic /}
{/ 在Vue组件的children里, React子组件可以获得从外层Provider传入的context的值 /}
}
`
#### Vue组件使用React组件 - Provide / Inject 的用法
`vue
`
$3
可以在React组件中直接使用 VueContainer 组件动态展示一个Vue组件
当这个React组件存在于某个Vue组件中时, 此时React组件中使用VueContainer 可以显示在上层 Vue 应用中注册的全局 Vue 组件。
`jsx
import {VueContainer} from "vaexy"
import BasicVue from './Basic.vue'
export default function () {
const passedProps = {
name: 'Mike'
}
// 如果 'vue-router' 存在,则渲染 '' 可以使用 ' '
return
}
`
VueContainer 也可以渲染 VNode。
`jsx
import {VueContainer} from "vaexy"
import {h} from 'vue'
const VNode = h('div', null, () => 'This is a VNode')
export default function ReactComponent() {
return
}
`
$3
VNode = getVNode(ReactNode)
大多数情况下,vue 组件遵循 SFC 规范,但也可以通过其他方式创建 vue 组件,例如 h 或 jsx,可能通过属性获取 VNode。
在 react 中将 VNode 类型的属性传递给 vue 组件时,可以使用 getVNode。
`jsx
import { applyVueInReact, getVNode } from 'vaexy'
import AAVue from './AA.vue'
const AA = applyVueInReact(AAVue)
const VNodeBar = getVNode(
rendered with a property
This is Bar's VNode
)
export default function ReactComponent () {
// VNodeBar is a property of type VNode, so use getVNode to convert reactNode to VNode.
return
}
`
$3
ReactNode = getReactNode(VNode)
有时候react组件的属性是一个复杂的数据结构,里面包含了ReactNode,而在vue文件中,jsx的定义会被编译成vue的jsx(也就是VNode),如果直接将这个属性传递给react组件,react组件是不能识别VNode的。
`vue
`
$3
有时react组件的render props的输入参数是ReactNode,而vue组件中如果直接使用作用域插槽去展示这个ReactNode是错误的。
RenderReactNode 是一个 vue 组件,它接受 node 参数,可以在 Vue 组件中渲染 ReactNode。
`vue
`
$3
'v-model' 的用法与Vue的jsx中的'v-model'用法相似
在React jsx中使用 v-model 属性, 可以有如下格式:
[ modelValue, modelSetter, argumentKey, argumentModifiers ]
[ modelValue, modelSetter, argumentModifiers ]
[ modelValue, modelSetter ]
'argumentKey'代表了v-model的自定义参数名, 默认情况下, v-model的参数名时modelValue, 也可以将'argumentKey'设置在v-model属性之后的附加后缀上, 比如 v-model-god={[godValue, setGodValue]} = v-model={[godValue, setGodValue, 'god']}
`typescript
// types
type modelValue = any
type modelSetter = (newValue) => void
type argumentKey = string
type argumentModifiers = string[]
`
`jsx
import {applyVueInReact} from 'vaexy'
import BasicVue from './Basic.vue'
import Basic1Vue from './Basic1.vue'
import {useState} from 'react'
const Basic = applyVueInReact(BasicVue)
const Basic1 = applyVueInReact(Basic1Vue)
export default function () {
const [foo, setFoo] = useState(Math.random())
const [bar, setBar] = useState(Math.random())
const [zoo, setZoo] = useState(Math.random())
return
{/ /}
{/ /}
{/ /}
// v-models对象中的key设置为'modelValue'时, 等同于默认的v-model属性
modelValue: [zoo, setZoo],
//...可以设置其他的自定义v-model的key
}} />
}
`
$3
useInjectPropsFromWrapper 是 applyReactInVue 和 applyVueInReact 的一个选项。
在同时开发 Vue 和 React 应用时,有时需要在 Vue 组件内部获取 React 应用的上下文,反之亦然。
例如,在Vue组件中使用react-router,或者在React组件中使用vuex。
#### 在 Vue 组件中注入 React hooks的用法
React 应用使用 Vue 组件,以下例子是在这个Vue组件中使用react-router。
`vue
This is the Vue Component.
the path info from 'react-router': {{fullPath}}
`
使用applyVueInReact将上面的Vue组件包装成一个React组件,并传入react-router。
`js
import { applyVueInReact } from 'vaexy'
import { useLocation, useNavigate } from 'react-router-dom'
import AboveVueComponent from './AboveVueComponent'
export default applyVueInReact(AboveVueComponent, {
useInjectPropsFromWrapper(reactProps) {
// 在这个函数中可以使用 React hooks
// 使用 react-router-dom's hooks
const location = useLocation()
const navigate = useNavigate()
// 返回的对象会作为 props 传递给 Vue 组件
return {
reactRouter: {
navigate,
location
}
}
}
})
`
#### 在 React 组件中注入 Vue hooks 的用法
Vue 应用使用 React 组件,示例在 React 组件中获取 vue-router 和 vuex。
注入函数有两种模式,“设置”和“计算”模式
`jsx
import React from 'react'
import {toRef} from 'vue'
import {useStore} from 'vuex'
import {useRoute, useRouter} from 'vue-router'
import {applyPureReactInVue} from 'vaexy'
// 这个 React 组件将在 Vue 应用程序中使用,需要使用 vue-router 和 vuex 钩子
// setup函数模式
function VueInjectionHookInSetupMode(vueProps) {
// 可以在这个函数中使用 Vue hooks
// 这个函数将在 Vue 包装器组件的 'setup' 函数中调用
const store = useStore()
const route = useRoute()
const router = useRouter()
// 返回的对象将作为 props 传递给 React 组件
return {
fullPath: toRef(route, 'fullPath'),
count: toRef(store.state, 'count'),
changeQuery: () => router.replace({
query: {
a: Math.random()
}
}),
incrementCount: () => store.dispatch('increment')
}
}
// computed函数模式
function VueInjectionHookInComputedMode(vueProps) {
// 该函数的上下文与来自getCurrentInstance().proxy
// 返回一个函数表示使用compute模式
// 所有逻辑代码都应该写在这个计算函数中。
return function computedFunction() {
return {
fullPath: this.$route.fullPath,
count: this.$store.state.count,
changeQuery: () => this.$router.replace({
query: {
a: Math.random()
}
}),
incrementCount: () => this.$store.dispatch('increment')
}
}
}
function ReactComponent (props) {
return (
This is the React Component
the path info from 'vue-router': {props.fullPath}
the count from 'vuex': {props.count}
)
}
// Vue 的注入函数有两种模式:'setup' 和 'computed'
// 请参考以上两种注入函数类型的案例
// 可以尝试用 'VueInjectionHookInComputedMode'
export default applyPureReactInVue(ReactComponent, {
useInjectPropsFromWrapper: VueInjectionHookInSetupMode
})
`
$3
虽然可以通过useInjectPropsFromWrapper使用其他框架的hook,然后通过属性在组件中获取hook的状态,但大多数情况下都是为了获取上下文类型数据,比如vue-router,react-router,redux,vuex,或者其他框架的自定义上下文。
使用 createCrossingProviderForPureReactInVue 和 createCrossingProviderForVueInReact 创建跨框架的Provider,在这个Provider内,另一个框架的组件可以获取到这个框架的上下文状态。
#### createCrossingProviderForVueInReact 的用法
通过createCrossingProviderForVueInReact创建一个Vue的hook和一个React的Provider,React Provider会将自定义的上下文传递给所有vue的子组件,例子中创建(reactRouterCrossingProvider.js)来定义一个crossing provider,provider包含了react-router。
`jsx
// Create a Provider that can get react hooks
// This Provider will be exported as a react component,
// and all of the vue components in this Provider can get the status of react hooks
import { useLocation, useNavigate } from 'react-router-dom'
import { createCrossingProviderForVueInReact } from 'vaexy'
// Execute 'useReactRouterForVue' in the setup function of the vue component to get the object returned by the incoming function
const [useReactRouterForVue, ReactRouterProviderForVue] = createCrossingProviderForVueInReact(
// This incoming function can execute react hooks
function() {
return {
location: useLocation(),
navigate: useNavigate()
}
}
)
export {
useReactRouterForVue,
ReactRouterProviderForVue
}
`
然后vue组件(Basic.vue)可以通过上面的js创建的vue hook获取到react-router的上下文。
`vue
This is the Vue Component.
the path info from 'react-router': {{pathname + search}}
`
在react项目或者组件中,将之前创建的provider包囊在外层即可
`jsx
import {applyVueInReact} from 'vaexy'
// Basic is a Vue component
import BasicVue from './Basic.vue'
import { ReactRouterProviderForVue } from './reactRouterCrossingProvider'
const Basic = applyVueInReact(BasicVue)
export default function () {
return
}
`
#### createCrossingProviderForReactInVue的用法
It is now recommended to use createCrossingProviderForPureReactInVue instead of createCrossingProviderForReactInVue.
Create a provider including vue-router and vuex and a React hooks that can be executed in the React function component and get the vue-router and vuex. (vueRouterAndVuexCrossingProvider.js)
`js
import {useStore} from 'vuex'
import {useRouter, useRoute} from 'vue-router'
import {createCrossingProviderForPureReactInVue} from 'vaexy'
const [useVueHooksInReact, VueProviderForReact] = createCrossingProviderForPureReactInVue(function() {
return {
vuex: useStore(),
vueRoute: useRoute(),
vueRouter: useRouter()
}
})
export {
useVueHooksInReact,
VueProviderForReact
}
`
The React component(Basic.js) can get the context from the provider through the custom hook.
`jsx
import React from 'react'
import { useVueHooksInReact } from '../vueRouterAndVuexCrossingProvider'
export default function (props) {
const { vuex, vueRoute, vueRouter } = useVueHooksInReact()
function changeQuery() {
vueRouter.replace({
query: {
a: Math.random()
}
})
}
function incrementCount() {
vuex.dispatch('increment')
}
return (
This is the React Component
the path info from 'vue-router': {vueRoute.fullPath}
the count from 'vuex': {vuex.state.count}
)
}
`
Vue components use the provider, so that all React components (including internal components) in the provider can get the context of this provider through custom hooks.
`vue
`
$3
Sometimes some features and plugins of Vue are really more useful than React, such as beforeEach of vue-router, and pinia.
So I implemented a factory function called createReactMissVue that returns a React provider component and a React hook.
With ReactMissVue, you can use Vue's plugins directly in React applications.
Enjoy it!
#### Usage of createReactMissVue
For detailed use cases, please refer to dev-project-react/src/components/reactMissVue
`jsx
import { defineStore, createPinia } from 'pinia'
import { createRouter, createWebHashHistory, useRouter, useRoute } from 'vue-router'
import { createReactMissVue, applyReactInVue, VueContainer } from 'vaexy'
// create vue-router instance
const router = createRouter({
// Using vue-router inside route 'ReactMissVue'
history: createWebHashHistory('/#/ReactMissVue'),
routes: [
{
name: '',
path: '/aaa',
component: applyReactInVue(() =>
react use vue-router
path: /aaa
)
},
{
name: 'empty',
path: '/:default(.*)',
component: applyReactInVue(() =>
react use vue-router
empty
)
},
],
})
// create a pinia store
const useFooStore = defineStore({
id: 'foo',
state() {
return {
name: 'Eduardo'
}
},
actions: {
changeName(name) {
this.$patch({
name
})
}
}
})
// create a ReactMissVue instance
let [useReactMissVue, ReactMissVue, ReactMissVueContext] = createReactMissVue({
useVueInjection() {
// This object can be obtained by using useReactMissVue in the react component
return {
fooStore: useFooStore(),
vueRouter: useRouter(),
vueRoute: useRoute()
}
},
// beforeVueAppMount can only be used in the outermost ReactMissVue
// Because vaexy will only create a vue application in the outermost layer
beforeVueAppMount(app) {
// register pinia
app.use(createPinia())
// register vue-router
app.use(router)
}
})
function Demo() {
const { fooStore } = useReactMissVue()
return
Foo's name: {fooStore?.name}
{/ Use the global component router-view /}
}
export default function () {
return
}
`
$3
It is now recommended to use lazyPureReactInVue instead of lazyReactInVue.
`vue
`
`typescript
// types
type lazyPureReactInVue = (asyncImport: Promise | defineAsyncComponentOptions, options?: options) => any;
`
$3
`jsx
import { lazyVueInReact, lazyPureVueInReact } from 'vaexy'
const AsyncBasicWithNormal = lazyVueInReact(() => import('./Basic'))
const AsyncBasicWithPure = lazyPureVueInReact(() => import('./Basic'))
export default function () {
return <>
>
}
`
`typescript
// types
type lazyReactInVue = (asyncImport: Promise, options?: options) => any
`
$3
Get the React component's instance in the Vue Component.
`vue
hello
`
Get the Vue component's instance in the React Component.
`jsx
import {applyVueInReact} from 'vaexy'
import BasicVue from './Basic.vue'
import React, { createRef, useEffect } from "react"
const Basic = applyVueInReact(BasicVue)
export default function () {
const basicInstance = createRef(null)
useEffect(() => {
// Get the real vue instance through __vaexyVueRef__
console.log(basicInstance.current.__vaexyVueRef__)
}, [])
return
}
`
Vue 和 React共存时会引发JSX的TS类型错误
>
> 如果您可以忽略 IDE 中的 TS 错误警告,则可以跳过本章。
Vue(@vue/runtime-dom) 和 React(@types/react) 都在全局命名空间 JSX 中扩展了类型接口,这会导致类型冲突。
例如,JSX.Element 不能同时扩展 ReactElement 和 VNode。
所以如果项目中同时安装了Vue和React,会在IDE(如vscode或webstorm)的JSX中引起TS错误警告,但这不会影响开发环境和生产环境的编译。
一个可行的解决方案是使用 patch-package 来修改 @vue/runtime-dom/dist/runtime-dom.d.ts 和 @types/react/index.d.ts,并且确保在tsconfig.json中设置compilerOptions.jsx为preserve。
比如这两个文件的改动如下。
node_modules/@types/react/index.d.ts(@types/react@18.0.14)
`diff
diff --git a/node_modules/@types/react/index.d.ts b/node_modules/@types/react/index.d.ts
index 5c5d343..a850f38 100644
--- a/node_modules/@types/react/index.d.ts
+++ b/node_modules/@types/react/index.d.ts
@@ -3118,7 +3118,9 @@ type ReactManagedAttributes = C extends { propTypes: infer T; defaultProps
declare global {
namespace JSX {
- interface Element extends React.ReactElement { }
+ interface Element extends React.ReactElement {
+ [k: string]: any
+ }
interface ElementClass extends React.Component {
render(): React.ReactNode;
}
@@ -3133,8 +3135,12 @@ declare global {
: ReactManagedAttributes
: ReactManagedAttributes;
- interface IntrinsicAttributes extends React.Attributes { }
- interface IntrinsicClassAttributes extends React.ClassAttributes { }
+ interface IntrinsicAttributes extends React.Attributes {
+ [k: string]: any
+ }
+ interface IntrinsicClassAttributes extends React.ClassAttributes {
+ [k: string]: any
+ }
interface IntrinsicElements {
// HTML
`
node_modules/@vue/runtime-dom/dist/runtime-dom.d.ts(@vue/runtime-dom@3.2.37)
`diff
diff --git a/node_modules/@vue/runtime-dom/dist/runtime-dom.d.ts b/node_modules/@vue/runtime-dom/dist/runtime-dom.d.ts
index 3366f5a..b9eacc6 100644
--- a/node_modules/@vue/runtime-dom/dist/runtime-dom.d.ts
+++ b/node_modules/@vue/runtime-dom/dist/runtime-dom.d.ts
@@ -1493,7 +1493,7 @@ type NativeElements = {
declare global {
namespace JSX {
- interface Element extends VNode {}
+ // interface Element extends VNode {}
interface ElementClass {
$props: {}
}
`
开发指引
本项目中的dev-project-react和dev-project-vue3目录是vaexy开发环境的基础项目,分别由create-react-app和@vue/cli创建的两个初始项目。
> Note: 在react项目中的config/webpack.config.js以及vue项目中的vue.config.js里,可以找到webpack的alias别名配置,将vaexy的别名注释解开,就可以对根项目中src目录里的vaexy源代码进行开发调试了
>
> Setup: 在主项目的根目录下运行命令行npm run setup:yarn 或者 npm run setup:npm,可以整体安装主项目和两个调试用的子项目
>
> Develop: 在主项目的根目录下运行命令行npm run dev:vue 以及 npm run setup:npm`就可以对子项目进行开发调试