Vue3和React的共同开发场景请使用Veaury
npm i vuereact-combined -S
- 使项目的人员选择性和机动性变得更强,vue和react的技术栈都可以加入项目
- 使项目的第三方插件选择性更强,vue和react的插件都可以通用
- 使研发人员的技术交流性更强,研发人员不应该被技术栈所限制
- 使项目可以集成更多的业务代码,其他vue和react项目的优秀代码可以快速引入
- 使前端研发人员可以更好的学习vue和react,了解两者的精华,促进团队在前端技术栈的广度
- 使用方式极其简便
非常感谢vuera的存在,开辟了Vue和React融合的想法,但是vuera只能解决非常基础的组件融合,并且存在插槽(children)和数据变更后的渲染性能问题,因此无法用于复杂的场景以及生产环境
vuereact-combined将融合做到了极致,支持了大部分的Vue和React组件的功能,并且在渲染更新上使用了和vuera不同的思路,完美解决了渲染性能问题
| 场景/功能 | vuereact-combined | vuera |
|---|---|---|
| normal prop (vue / react) | ✔ | ✔ |
| event (vue / react) | ✔ | ✔ |
| children (vue / react) | ✔ | ✔ |
| Provider/Consumer in vue (react) | ✔ | |
| Provider/Consumer cross react->vue->...->react | ✔ | |
| provide/inject cross vue->react->...->vue | ✔ | |
| named slots (vue) | ✔ | |
| scope slots (vue) | ✔ | |
| v-model (vue) | ✔ | |
| sync (vue) | ✔ | |
| render props (react) | ✔ | |
| node props (react) | ✔ | |
| enter & leave 事件委托传递 (react) | ✔ | |
| slots & children & node在父组件数据变更后的生命周期表现 | 触发更新 | 每次都触发创建和销毁 |
| vuex in react | ✔ | |
| vue-router in react | ✔ | |
| redux in vue | ✔ | |
| react-router in react | ✔ | |
| lazyReactInVue | ✔ | |
| lazyVueInReact | ✔ | |
| 第三方组件跨框架使用(比如antd、element) | 支持所有第三方组件 | 基本不支持 |
| 自定义融合包囊层的dom attr | ✔ |
<!--Vue File-->
<template>
<Popover content="I am React Popover" title="Title">
<Button type="primary">It's Vue Button</Button>
</Popover>
</template>
<script>import { applyReactInVue } from'vuereact-combined'// antd Reactimport { Popover } from'antd'// element-ui Vueimport { Button } from'element-ui'exportdefault { components: {// 使用applyReactInVue高阶组件将antd Popover转换成Vue组件 Popover:applyReactInVue(Popover), Button, },}</script>
<style scoped></style>// React JSX FileimportReact,{useState}from'react'// element-ui DatePicker Vueimport{DatePicker}from'element-ui'import{applyVueInReact}from'vuereact-combined'// 使用applyVueInReact高阶组件讲element-ui DatePicker转换成React组件constElDatePicker=applyVueInReact(DatePicker)exportdefaultfunction(){const[timeValue,setTimeValue]=useState(Date.now())return<ElDatePicker{/* Vue组件的v-model在React中的用法 */}$model={{value: timeValue,setter: (val)=>{setTimeValue(val)},}}type="date"placeholder="选择日期"/>}最基本的,项目中至少应该存在vue@^2.6、react@^16.3、react-dom@^16.3
第三方的react组件已经是通过babel进行过处理,不包含React的jsx
此情况下,可以直接在项目中使用applyReactInVue对第三方的React组件进行处理
第三方的Vue组件已经是通过vue-loader和babel进行过处理,不包含.vue文件以及Vue的jsx
此情况下,可以直接在项目中使用applyVueInReact对第三方的Vue组件进行处理
此情况可以在一个项目中同时开发编写React和Vue的组件代码,由于需要同时具备两种技术栈所依赖的环境,因此需要对项目的构建(一般是webpack的配置)和babel.config.js进行一些配置上的修改
可以参考以下案例
- 如果是通过vue-cli3创建的项目
请参考 https://github.com/devilwjp/vuereact-for-vuecli3-demo - 如果通过react-create-app创建的项目(react版本需要>=16.3)
请参考 https://github.com/devilwjp/vuereact-for-cra-demo
在React中正常的使用React的方式向Vue组件传递属性和children
// React JSX FileimportReact,{useState}from'react'// element-ui Vueimport{Button,ButtonGroup}from'element-ui'import{applyVueInReact}from'vuereact-combined'constElButton=applyVueInReact(Button)constElButtonGroup=applyVueInReact(ButtonGroup)exportdefaultfunction(){const[type,setType]=useState('primary')const[disabled,setDisabled]=useState(false)const[content,setContent]=useState('提交')return<ElButtonGroup><ElButtontype="danger"disabled>提交</ElButton><ElButtontype={type}disabled={disabled}>提交</ElButton><ElButtontype="danger">{content}</ElButton></ElButtonGroup>}在Vue中正常的使用Vue的方式向React组件传递属性和插槽
<!--Vue File-->
<template>
<Popover :content="content" :title="title">
{{popoverChildren}}
</Popover>
</template>
<script>import { applyReactInVue } from'vuereact-combined'// antd Reactimport { Popover } from'antd'exportdefault {data() {return { content:'I am React Popover', title:'Title', popoverChildren:`hover me!`, } }, components: {// 使用applyReactInVue高阶组件将antd Popover转换成Vue组件 Popover:applyReactInVue(Popover) },}</script>
<style scoped></style>// React JSX FileimportReact,{useState}from'react'// element-ui DatePicker Vueimport{DatePicker}from'element-ui'// 一个开放sync修饰符属性的Vue组件importVueComponentfrom'./VueComponent.vue'import{applyVueInReact}from'vuereact-combined'constElDatePicker=applyVueInReact(DatePicker)constVueComponentInReact=applyVueInReact(VueComponent)exportdefaultfunction(){const[timeValue,setTimeValue]=useState(Date.now())const[timeValue1,setTimeValue1]=useState(Date.now())// Vue组件的v-model在React中的用法const$model={value: timeValue,setter: (val)=>{setTimeValue(val)},}// Vue组件的sync在React中的用法const$sync={props1: {value: timeValue1,setter: (val)=>{setTimeValue1(val)},}}return<div><ElDatePicker$model={$model}type="date"placeholder="选择日期"/><VueComponentInReact$sync={$sync}/></div>}使用$model属性传递一个对象$model
Type:{value: state, setter: (val: nextState) => void}
其中value就是要传入给v-model的状态值,setter就是子组件向父组件发出修改状态值的触发函数,这个函数应该是个纯函数,不应该包含其他逻辑,确保函数内容仅仅只用于修改状态值$sync
Type:{[propName: {value: state, setter: (val: nextState) => void}]}
// React JSX FileimportReact,{useState}from'react'// 一个开放具名插槽和作用域插槽的vue组件importVueComponentfrom'./VueComponent.vue'import{applyVueInReact}from'vuereact-combined'constVueComponentInReact=applyVueInReact(VueComponent)exportdefaultfunction(){// 具名插槽const$slots={slotA: <div>具名插槽A</div>,slotB: <div>具名插槽B</div>}// 作用域插槽const$scopedSlots={slotC: (context)=><div>我是作用域插槽C:{context.value}</div>}return<div><VueComponentInReact$slots={$slots}$scopedSlots={$scopedSlots}><h1>我是普通的插槽</h1></VueComponentInReact></div>}$slots 具名插槽属性
Type: {[slotName: string]: ReactNode}$scopedSlots 作用域插槽属性
Type: {[slotName: string]: (context: RenderPropsContext) => ReactElement | ReactComponent}
<!--Vue File-->
<template>
<ReactComponentInVue>
我是普通children
<!-- 等同于向react组件传入 slotA={<span>我是ReactNode类型的slotA属性</span>} -->
<templatev-slot:slotA>
<span>我是ReactNode类型的slotA属性</span>
</template>
<!-- 等同于向react组件传入 slotB={<span>我是ReactNode类型的slotA属性</span>} -->
<templatev-slot:slotB>
<span>我是ReactNode类型的slotB属性</span>
</template>
<!-- 等同于向react组件传入 slotC={(context) => <span>我是renderProps类型:{{context.value}}</span>} -->
<templatev-slot:slotC="context">
<span>我是renderProps类型:{{context.value}}</span>
</template>
</ReactComponentInVue>
</template>
<script>import { applyReactInVue } from'vuereact-combined'// 一个开放ReactNode类型属性和renderProps类型属性的React组件importReactComponentfrom'./ReactComponent'exportdefault { components: { ReactComponentInVue:applyReactInVue(ReactComponent) }}</script>applyReactInVue会将ReactNode类型的属性转会为Vue的具名插槽,将renderProps类型的属性转换为作用域插槽,具名插槽和作用域插槽的插槽名就是属性名
<!--Vue File-->
<template>
<MyProvider :value="content">
<Button>Vue按钮</Button>
<!-- React组件中可以正常的使用Consumer消费Context -->
<ReactComponentInVue/>
</MyProvider>
</template>
<script>import { applyReactInVue } from'vuereact-combined'// React ContextimportMyContextfrom"./MyContext"import {Button} from'element-ui'importReactComponentfrom'./ReactComponent'exportdefault {data() {return { content:'hahahahaha!' } }, components: { Button, ReactComponentInVue:applyReactInVue(ReactComponent),// 把Provider当作React组件直接转换 MyProvider:applyReactInVue(MyContext.Provider), }}</script>VueContainer是一个高阶组件,通过component属性直接渲染Vue组件
// React JSX FileimportReact,{useState,useEffect}from'react'importVueComponent1from'./VueComponent1.vue'importVueComponent2from'./VueComponent2.vue'import{VueContainer}from'vuereact-combined'constElButton=applyVueInReact(Button)constElButtonGroup=applyVueInReact(ButtonGroup)exportdefaultfunction(){const[vueComponent,setVueComponent]=useState(VueComponent1)useEffect(()=>{// 3秒之后换成VueComponent2组件setTimeout(()=>{setVueComponent(VueComponent2)},3000)},[])constprop1='属性1'constprop2='属性2'return<div><VueContainercomponent={vueComponent}prop1={prop1}prop2={prop2}/>{/* component属性为string类型时,表示使用vue的全局组件,以下的例子表示在react组件中使用vue-router的<router-view/> */}<VueContainercomponent="RouterView"/></div>}注意:这里没有使用onEvent的属性传递方法,因为无法排除vue组件有属性与此种用法重名的可能
// React JSX FileimportReact,{useState}from'react'// 一个开放了某些事件的Vue组件importVueComponentfrom'./VueComponent.vue'import{applyVueInReact}from'vuereact-combined'constVueComponentInReact=applyVueInReact(VueComponent)exportdefaultfunction(){constclick=()=>{console.log('click')}constmouseEnter=()=>{console.log('mouseEnter')}constcustomEvent=()=>{console.log('mouseEnter')}// 通过on属性传递将事件所对应的函数传递给vue组件// 以下的代码等于同于vue中v-on="{click, mouseEnter, customEvent}"return<VueComponentInReacton={{click, mouseEnter, customEvent}}/>}作用:使得所有的Vue组件可以使用redux的状态管理 对工具包开启redux状态管理,这个场景一般存在于以React为主的项目中,为了使Vue组件也可以共享到redux,需要在项目的入口文件引入applyRedux方法(整个项目应该只引一次),将redux的store以及redux的context作为参数传入(或者至少在redux的Provider高阶组件引入的地方使用applyRedux方法)
// 第二个参数是redux的context,之所以需要传第二个参数,是因为有如下场景// Provider -> ReactCom1 -> VueCom1 -> ReactCom2// Provider无法直接透过Vue组件传递给之后的React组件,所以applyRedux提供了第二个参数,作用就是可以使通过Vue组件之后的React组件继续可以获取到redux的contextimport{ReactReduxContext}from'react-redux'importstorefrom'../reactComponents/reduxStore'applyRedux({ store, ReactReduxContext })// 原生的redux store的创建方式import{createStore}from'redux'importsomeCombineReducerfrom'./reducer'// 建议通过react-redux的combineReducer输出letstore=createStore(someCombineReducer)exportdefaultstoreReact组件连接redux的方式这里就不再做介绍了,应该使用react-redux的connect方法
这里介绍Vue组件如何使用redux,工具包尽可能的实现了vue组件使用vuex的方式去使用redux,通过vm.$redux可以在组件实例里获取到redux状态管理
<template><div>
redux状态testState1: {{$redux.state.testState1}}
</div></template><script>exportdefault{name: 'demo3',mounted(){// 打印redux的testState2状态值console.log(this.$redux.state.testState2)// 五秒后将testState1修改成8888// 需要在reducer里存在一个action的type为test1可以修改testState1// 这里需要按照标准的redux的action标准(必须有type)触发dispatchsetTimeout(()=>{this.$redux.dispatch({type: 'test1',value: 8888})},5000)}}</script>作用:使得所有的Redux组件可以使用Vuex的状态管理
对工具包开启vuex状态管理,这个场景一般存在于以Vue为主的项目中,为了使React组件也可以共享到vuex,需要在项目的入口文件引入applyVuex方法(整个项目应该只引一次),将vuex的store作为参数传入
importstorefrom'../store'// vuex的store文件applyVuex(store)类似react-redux的connect方法,在React组件中使用,由于vuex的关键字比redux多,所以将参数改成了对象,包含了mapStateToProps、mapCommitToProps、mapGettersToProps、mapDispatchToProps,每个都是一个纯函数,返回一个对象(和redux的connect使用方式完全一致)
exportdefaultconnectVuex({mapStateToProps(state){return{vuexState: state,state1: state.state1,moduleAstate: state.moduleA}},mapCommitToProps(commit){return{vuexCommit: commit}},// mapGettersToProps = (getters) => {},// mapDispatchToProps = (dispatch) => {},})(ReactComponent)在React的router里懒加载Vue组件
importReact,{lazy,Suspense}from"react"import{lazyVueInReact}from'vuereact-combined'constHello=lazy(()=>import("./react_app/hello"));//懒加载vue组件constTestVue=lazyVueInReact(()=>import("./vue_app/test.vue"))exportdefault[{path: "/reactHello",component: ()=>{return(<Suspensefallback={<div>Loading...</div>}><Hello/></Suspense>);}},{path: "/vuetest1",component: ()=>{return(<Suspensefallback={<div>Loading...</div>}><div><h1>我是一个vue组件</h1><TestVue/></div></Suspense>);}}]在Vue的router里懒加载React组件
importVuefrom'vue'importVueRouterfrom'vue-router'import{lazyReactInVue}from'vuereact-combined'Vue.use(VueRouter)constroutes=[{path: '/',name: 'home',component: ()=>import('../views/Home')},{path: '/reactInVueDemo',name: 'reactInVueDemo',component: lazyReactInVue(()=>import('../reactComponents/cc.jsx'))}]constrouter=newVueRouter({
routes
})exportdefaultrouter在react组件中获取vue router对象,可以通过props属性获取倒$vueRouter和$vueRoute
importReactfrom'react'import{withVueRouter}from'vuereact-combined'classTest2extendsReact.Component{constructor(props){super(props)}componentWillMount(){}componentDidMount(){// 可以通过props属性获取倒$vueRouter和$vueRouteconsole.log(this.props.$vueRouter,this.props.$vueRoute)}render(){return(<div>
test2
<h3>{this.props.$vueRoute.query.b}</h3></div>)}}exportdefaultwithVueRouter(Test2)建议在react项目的app或者main引入,然后再任何一个被转换的vue组件中都可以直接获取到实例属性$reactRouter,其中包含了react router的history、location、match
import{applyReactRouterInVue}from'vuereact-combined'import{withRouter}from'react-router-dom'applyReactRouterInVue(withRouter)<template>
<div>
<h1>demo</h1>
<h2>{{$reactRouter.location.search}}</h2>
</div>
</template>
<script>exportdefault {mounted () { }}</script>由于在每一次跨越一个框架进行组件引用时,都会出现一层包囊,这个包囊是默认是以div呈现,并且会被特殊属性标注
React->Vue,会在vue组件的dom元素外包囊一层标识data-use-vue-component-wrap的div
Vue->React,会在react组件的dom元素外包囊一层标识__use_react_component_wrap的div
如果引发样式问题,可以对applyVueInReact、applyReactInVue方法传入第二个参数options
importVueComponentfrom'./VueComponent.vue'import{applyVueInReact}from'vuereact-combined'constVueComponentInReact=applyVueInReact(VueComponent,{react: {// react.componentWrapAttrs代表是vue组件在react组件中的组件包囊层的标签设置// 以下设置将设置组件的包囊层div的display为inline-blockcomponentWrapAttrs: {style: {display: 'inline-block'},class: 'react-wrap-vue-component-1'},// react.slotWrapAttrs代表是vue组件在react组件中的插槽包囊层的标签设置// 以下设置将设置插槽的包囊层div的display为inline-blockslotWrapAttrs: {style: {display: 'inline-block'}},},})以下是默认配置
// 默认配置constoriginOptions={react: {componentWrap: 'div',slotWrap: 'div',componentWrapAttrs: {__use_react_component_wrap: '',style: {all: 'unset'}},slotWrapAttrs: {__use_react_slot_wrap: '',style: {all: 'unset'}}},vue: {// 组件wrappercomponentWrapHOC: (VueComponentMountAt,nativeProps=[])=>{// 传入portalsreturnfunction({ portals =[]}={}){return(<div{...nativeProps}>{VueComponentMountAt}{portals.map((Portal,index)=><Portalkey={index}/>)}</div>)}},componentWrapAttrs: {'data-use-vue-component-wrap': '',style: {all: 'unset',}},slotWrapAttrs: {'data-use-vue-slot-wrap': '',style: {all: 'unset'}}}}| 功能 | 支持程度 | 说明 |
|---|---|---|
| 普通属性 | 完全支持 | |
| html片段属性 | 变向支持 | 通过$slots,在vue中使用具名插槽获取 |
| render props | 变向支持 | 通过$scopedSlots,在vue中使用作用域插槽获取 |
| children(普通插槽) | 完全支持 | |
| 组件合成事件 | 完全支持 | 通过on属性 |
| 组件原生事件(.native) | 不支持 | react没有这种感念,可以自己包囊div |
| v-model | 变向支持 | 通过$model,并且支持vue组件中随意自定义model属性 |
| html片段中使用react或者vue组件 | 完全支持 | react组件直接传入,vue组件继续通过applyVueInReact转换 |
| 懒加载vue组件 | 完全支持 | 通过lazyVueInReact |
| redux共享 | 完全支持 | 使用applyRedux |
| mobx共享 | 变向支持 | mobx本身就有react和vue的连接方式 |
| vuex共享 | 完全支持 | 使用applyVuex |
| sync装饰 | 变向支持 | 使用$sync |
| 事件修饰(key.enter、click.once) | 不支持 | 自行处理 |
| 透传 | 变向支持 | 使用data-passed-props |
| ref | 变向支持 | ref首先会返回包囊实例的,在包囊实例中的属性vueRef可以获取倒vue组件实例 |
| react router(在vue组件中) | 完全支持 | 使用applyReactRouterInVue |
| 判断自身是否被转化 | 完全支持 | 通过props属性data-passed-props或者实例属性reactWrapperRef |
| 功能 | 支持程度 | 说明 |
|---|---|---|
| 普通属性 | 完全支持 | |
| 具名插槽 | 完全支持 | 在react中使用属性获取 |
| 作用域插槽 | 完全支持 | 在react中使用属性获取,类型是个函数 |
| 普通插槽 | 完全支持 | |
| 组件合成事件 | 完全支持 | 在react中使用属性获取 |
| 组件原生事件(.native) | 暂不支持 | |
| v-model | 不支持 | react组件没有这个概念 |
| provider/inject传入react | 暂不支持 | 未来会支持 |
| sync装饰 | 不支持 | react组件没有这个概念 |
| redux共享 | 完全支持 | 使用applyRedux |
| mobx共享 | 变向支持 | mobx本身就有react和vue的连接方式 |
| vuex共享 | 完全支持 | 使用applyVuex |
| 事件修饰(key.enter、click.once) | 不支持 | react组件没有这个概念 |
| 懒加载react组件 | 完全支持 | 通过lazyReactInVue |
| 透传 | 变向支持 | 使用data-passed-props |
| ref | 变向支持 | ref首先会返回包囊实例的,在包囊实例中的属性reactRef可以获取倒react组件实例 |
| vue router(在react组件中) | 完全支持 | 使用withVueRouter |
| 判断自身是否被转化 | 完全支持 | 通过props属性data-passed-props或者实例属性vueWrapperRef |
