楔子
最近在做一些调研类的事情,然后公司技术栈是以 Vue 为主的,虽然现在 Vue 的生态非常繁荣,但是总有各种各样的原因找不到合适的 开源库 使用,而全自研的话成本又很大,所以极小概率出现了在 Vue 社区里面找不到合适的 开源库 (当然也可能只是我没找到而已),但是 React 的社区里面有知道且合适的 开源库 ,换技术栈肯定是换不了的,那么就只能做一些骚操作了,比如在 Vue 的项目中使用 React 的组件?
vuera
同样的经过一番检索,发现了 vuera 这么一个 开源库 ,而且它已经实现了 Vue 项目加载 React 组件 以及 React 项目加载 Vue 组件的能力。
但是吧!!!它已经停止维护了!!!它的功能仅支持在 Vue2 和 React 15+ (用的 class 组件)这个版本,而我用的是 Vue3 ,实在是非常难受!
虽然它已经停止维护了,并且对 Vue3 不支持,但是它也提供了 Vue 和 React 组件转换/加载的思路,所以扒一扒源码,看看它如何实现的,然后自己写一个能支持 Vue3 的出来。
转换原理
扒了下源码后,发现它的原理其实非常的简单,而且整体的代码量也不多。
不管是 Vue 还是 React,在创建实例的时候,都需要一个 根节点 作为容器,之后就会接管该容器的所有内容了,所以在一个 页面 中,同时使用 Vue 和 React 是没问题的,只需要分别给它们设置不同的 根节点 即可,它们之间也互不干扰。
那么!可不可以,通过一个自定义的 Vue/React 组件,组件的 模板(template/jsx) 有且只有一个 根节点 ,然后在组件挂载后,通过该 根节点 创建一个 Vue/React 实例 ,最后通过这个 Vue/React 实例 来 渲染指定的组件 ?
没错,是可以的,这也是 vuera 这个库实现 Vue 和 React 组件转换/加载 的方案!
不得不说思想决定成败。
实现
那么就来看一下具体如何实现 Vue 和 React 组件转换/加载。
为了简约以及一致性,这里 Vue 和 React 统一使用 createElement/h 的方法创建 vnode 而不是 template/jsx 的语法糖形式。
Vue 项目加载 React 组件
首先创建一个自定义 Vue 的组件;
组件会接收一个指定的 props
参数 component
即是 react 组件;
该组件的 基础模板(template) 是一个 div
;
分别创建了两个响应式变量 react
和 reactInstance
;
react
用来存储 根节点 元素的实例;
reactInstance
用来存储通过 ReactDOM.createRoot
创建的 react dom root
实例
在 mounted 也就是 Vue 组件已经挂载 div
元素已经渲染后,使用 ReactDOM.createRoot
方法并传入变量 react.value
也就是 div
元素的实例,来创建一个 react dom root
实例,并使用响应式变量 reactInstance
来保存该实例,使用 react dom root
实例的 render
方法来渲染通过 props
传递的 react component
同时透传参数和属性。
import { createElement } from "react"
import ReactDOM from 'react-dom/client';
import { defineComponent, h, onMounted, ref } from "vue"
// Vue 组件
export default defineComponent({
props: ['component'],
setup(props, ctx) {
const react = ref() // 根节点实例
const reactInstance = ref() // react dom root
const { component, ...rest } = props // 接收的 React 组件
const setReactRef = ref => { react.value = ref }
onMounted(() => {
// 创建 react 实例
reactInstance.value = ReactDOM.createRoot(react.value)
// 渲染 react 组件
reactInstance.value.render(
createElement(
component,
{
...rest,
...ctx.attrs,
ref: setReactRef // 跟新根节点实例
},
null
)
)
})
// 渲染根节点
return () => h('div', { ref: react })
},
})
使用形式:
<template>
<div>
<VueWrapper
:component="Editor"
mode="default"
style="height: 500px; overflow-y: hidden;"
:defaultConfig="{ placeholder: '请输入内容...' }"
/>
</div>
</template>
<script setup>
import VueWrapper from './rewite/VueWrapper.js'
// wangEditor 的 React 组件
import { Editor } from '@wangeditor/editor-for-react'
</script>
React 项目加载 Vue 组件
首先创建一个自定义 React 组件;
组件会接收一个指定的 props
属性 component
;
该组件的 基础模板(jsx) 是一个 div
;
创建两个变量 instance
和 options
;
instance
用来存储 createApp
返回的 vue app
实例;
options
其实就是一个 options api
的 Vue 组件,其主要目的是可以透传参数和属性;
createVueInstance
方法则是借助 ref
的机制来实例化 Vue。
import { useCallback, useRef, createElement } from "react"
import { createApp, defineComponent, h } from "vue"
// react 组件
const ReactWrapper = (props) => {
const { component, ...rest } = props // 传入的 Vue 组件和属性
const instance = useRef() // vue app 实例
// vue 组件,主要是用来传递参数
const options = useRef(defineComponent({
render() {
return h(component, rest, null)
}
}))
// 创建 vue app 实例
const createVueInstance = useCallback((targetElement) => {
instance.current = createApp(options.current)
instance.current.mount(targetElement)
}, [])
return createElement('div', { ref: createVueInstance })
}
export default ReactWrapper
使用形式:
import ReactWrapper from './rewrite/ReactWrapper'
// wangEditor 的 Vue 组件
import { Editor } from '@wangeditor/editor-for-vue'
import '@wangeditor/editor/dist/css/style.css'
function App() {
return (
<div className="App">
<ReactWrapper
component={Editor}
mode="default"
style={{ height: '500px', overflowY: 'hidden' }}
defaultConfig={{ placeholder: '请输入内容...' }}
/>
</div>
);
}
export default App;
转换
那么可能有同学说:我不想通过传参的形式,想直接以正常组件的形式可以不?
当然,也是可以的。
Vue
import { defineComponent, h } from "vue"
import VueWrapper from "../VueWrapper/index"
const VueResolver = (component) => {
return defineComponent({
inheritAttrs: false,
setup(props, ctx) {
return () => h(
VueWrapper,
{
component: component,
...props,
...ctx.attrs,
},
ctx.slots.default
)
}
})
}
export default VueResolver
React
import { isValidElement, createElement } from 'react'
import ReactWrapper from '../VueWrapper/index'
const ReactResolver = (component) => {
if (isValidElement(component)) return component
return (props) => {
return createElement(ReactWrapper, { component, ...props })
}
}
export default ReactResolver
使用
两种的使用形式都是一样的,这里以 Vue 为例。
<template>
<div>
<WangEditor
mode="default"
style="height: 500px; overflow-y: hidden;"
:defaultConfig="{ placeholder: '请输入内容...' }"
/>
</div>
</template>
<script setup>
import VueResolver from './VueResolver.js'
import { Editor } from '@wangeditor/editor-for-react'
const WangEditor = VueResolver(Editor)
</script>
这样是不是就和正常的 Vue 组件的使用形式是一样的了?
总结
虽然是极小概率才会发生的事情,但是总归还是有出现场景的,同时也扩充了视野。
注:文本的例子只是极简的实现,适用场景比较单一,如果想真正的用到项目里面,还需要根据使用场景进行一些改造,比如对 children 的支持。
如果文本对您有帮助,那么可以给咱一个赞吗?🥺
我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿。