Home

Awesome

Vue和React快捷集成的工具包,并且适合复杂的集成场景

<div align=center> <img src="https://raw.githubusercontent.com/devilwjp/VueReact/master/vuereact-combined.png"/> </div> <div align=center> <p> <h4>可以在任何的Vue和React项目中使用另一个类型框架的组件,并且解决了复杂的集成问题 </h4> <p> </div>

支持Vue3?

Vue3和React的共同开发场景请使用Veaury

安装

npm i vuereact-combined -S

Why?

让vue和react的同学们一起来完成同一个项目同一个页面甚至同一个组件

Benchmark

非常感谢vuera的存在,开辟了Vue和React融合的想法,但是vuera只能解决非常基础的组件融合,并且存在插槽(children)和数据变更后的渲染性能问题,因此无法用于复杂的场景以及生产环境
vuereact-combined将融合做到了极致,支持了大部分的Vue和React组件的功能,并且在渲染更新上使用了和vuera不同的思路,完美解决了渲染性能问题

场景/功能vuereact-combinedvuera
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 React
import { Popover } from 'antd'
// element-ui Vue
import { Button } from 'element-ui'

export default {
  components: {
    // 使用applyReactInVue高阶组件将antd Popover转换成Vue组件
    Popover: applyReactInVue(Popover),
    Button,
  },
}
</script>

<style scoped>

</style>
// React JSX File
import React, { useState } from 'react'
// element-ui DatePicker Vue
import { DatePicker } from 'element-ui'
import { applyVueInReact } from 'vuereact-combined'

// 使用applyVueInReact高阶组件讲element-ui DatePicker转换成React组件
const ElDatePicker = applyVueInReact(DatePicker)
export default function() {
  const [timeValue, setTimeValue] = useState(Date.now())
  return <ElDatePicker
    {/* Vue组件的v-model在React中的用法 */}
    $model={{
        value: timeValue,
        setter: (val) => { setTimeValue(val) },
    }}
    type="date"
    placeholder="选择日期"/>
}

使用场景

最基本的,项目中至少应该存在vue@^2.6react@^16.3react-dom@^16.3

Vue项目中使用第三方的React组件

第三方的react组件已经是通过babel进行过处理,不包含React的jsx
此情况下,可以直接在项目中使用applyReactInVue对第三方的React组件进行处理

React项目中使用第三方的Vue组件

第三方的Vue组件已经是通过vue-loaderbabel进行过处理,不包含.vue文件以及Vue的jsx
此情况下,可以直接在项目中使用applyVueInReact对第三方的Vue组件进行处理

复杂情况(项目中同时安装和配置react和vue的相关环境)

此情况可以在一个项目中同时开发编写React和Vue的组件代码,由于需要同时具备两种技术栈所依赖的环境,因此需要对项目的构建(一般是webpack的配置)和babel.config.js进行一些配置上的修改
可以参考以下案例

属性传递

在React中正常的使用React的方式向Vue组件传递属性和children

// React JSX File
import React, { useState } from 'react'
// element-ui Vue
import { Button, ButtonGroup } from 'element-ui'
import { applyVueInReact } from 'vuereact-combined'

const ElButton = applyVueInReact(Button)
const ElButtonGroup = applyVueInReact(ButtonGroup)

export default function() {
    
  const [type, setType] = useState('primary')
  const [disabled, setDisabled] = useState(false)
  const [content, setContent] = useState('提交')
    
  return <ElButtonGroup>
    <ElButton type="danger" disabled>提交</ElButton>
    <ElButton type={type} disabled={disabled}>提交</ElButton>
    <ElButton type="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 React
import { Popover } from 'antd'

export default {
  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中使用Vue组件的v-model和sync修饰符

// React JSX File
import React, { useState } from 'react'
// element-ui DatePicker Vue
import { DatePicker } from 'element-ui'
// 一个开放sync修饰符属性的Vue组件
import VueComponent from './VueComponent.vue'
import { applyVueInReact } from 'vuereact-combined'

const ElDatePicker = applyVueInReact(DatePicker)
const VueComponentInReact = applyVueInReact(VueComponent)

export default function() {
  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中使用Vue组件的作用域插槽和具名插槽

// React JSX File
import React, { useState } from 'react'
// 一个开放具名插槽和作用域插槽的vue组件
import VueComponent from './VueComponent.vue'
import { applyVueInReact } from 'vuereact-combined'

const VueComponentInReact = applyVueInReact(VueComponent)
export default function() {
  // 具名插槽
  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组件中向React组件传递ReactNode类型的属性和renderProps类型的属性

<!--Vue File-->
<template>
  <ReactComponentInVue>
    我是普通children
    <!--  等同于向react组件传入 slotA={<span>我是ReactNode类型的slotA属性</span>}  -->
    <template v-slot:slotA>
      <span>我是ReactNode类型的slotA属性</span>
    </template>
    <!--  等同于向react组件传入 slotB={<span>我是ReactNode类型的slotA属性</span>}  -->
    <template v-slot:slotB>
      <span>我是ReactNode类型的slotB属性</span>
    </template>
    <!--  等同于向react组件传入 slotC={(context) => <span>我是renderProps类型:{{context.value}}</span>}  -->
    <template v-slot:slotC="context">
      <span>我是renderProps类型:{{context.value}}</span>
    </template>
  </ReactComponentInVue>
</template>

<script>
import { applyReactInVue } from 'vuereact-combined'
// 一个开放ReactNode类型属性和renderProps类型属性的React组件
import ReactComponent from './ReactComponent'
export default {
  components: {
    ReactComponentInVue: applyReactInVue(ReactComponent)
  }
}
</script>

applyReactInVue会将ReactNode类型的属性转会为Vue的具名插槽,将renderProps类型的属性转换为作用域插槽,具名插槽和作用域插槽的插槽名就是属性名

在Vue组件中调用React组件的Context/Provider

<!--Vue File-->
<template>
  <MyProvider :value="content">
    <Button>Vue按钮</Button>
    <!--  React组件中可以正常的使用Consumer消费Context  -->
    <ReactComponentInVue/>
  </MyProvider>
</template>

<script>
import { applyReactInVue } from 'vuereact-combined'
// React Context
import MyContext from "./MyContext"
import {Button} from 'element-ui'
import ReactComponent from './ReactComponent'
export default {
  data() {
    return {
      content: 'hahahahaha!'
    }
  },
  components: {
    Button,
    ReactComponentInVue: applyReactInVue(ReactComponent),
    // 把Provider当作React组件直接转换
    MyProvider: applyReactInVue(MyContext.Provider),
  }
}
</script>

VueContainer,在React组件中使用Vue的动态组件

VueContainer是一个高阶组件,通过component属性直接渲染Vue组件

// React JSX File
import React, { useState, useEffect } from 'react'
import VueComponent1 from './VueComponent1.vue'
import VueComponent2 from './VueComponent2.vue'
import { VueContainer } from 'vuereact-combined'

const ElButton = applyVueInReact(Button)
const ElButtonGroup = applyVueInReact(ButtonGroup)

export default function() {
  const [vueComponent, setVueComponent] = useState(VueComponent1)
  useEffect(() => {
    // 3秒之后换成VueComponent2组件
    setTimeout(() => {
      setVueComponent(VueComponent2)
    }, 3000)
  }, [])
  const prop1 = '属性1'
  const prop2 = '属性2'
  return <div>
      <VueContainer component={vueComponent} prop1={prop1} prop2={prop2}/>
      {/* component属性为string类型时,表示使用vue的全局组件,以下的例子表示在react组件中使用vue-router的<router-view/> */}
      <VueContainer component="RouterView"/>
  </div>
}

在React组件中使用Vue组件的事件

注意:这里没有使用onEvent的属性传递方法,因为无法排除vue组件有属性与此种用法重名的可能

// React JSX File
import React, { useState } from 'react'
// 一个开放了某些事件的Vue组件
import VueComponent from './VueComponent.vue'
import { applyVueInReact } from 'vuereact-combined'

const VueComponentInReact = applyVueInReact(VueComponent)

export default function() {
    const click = () => {
        console.log('click')
    }
    const mouseEnter = () => {
        console.log('mouseEnter')
    }
    const customEvent = () => {
        console.log('mouseEnter')
    }
    // 通过on属性传递将事件所对应的函数传递给vue组件
    // 以下的代码等于同于vue中v-on="{click, mouseEnter, customEvent}"
    return <VueComponentInReact on={{click, mouseEnter, customEvent}}/>
}

applyRedux

作用:使得所有的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的context
import { ReactReduxContext } from 'react-redux'
import store from '../reactComponents/reduxStore'
applyRedux({ store, ReactReduxContext })

store.js

// 原生的redux store的创建方式
import { createStore } from 'redux'
import someCombineReducer from './reducer' // 建议通过react-redux的combineReducer输出
let store = createStore(someCombineReducer)
export default store

React组件连接redux的方式这里就不再做介绍了,应该使用react-redux的connect方法
这里介绍Vue组件如何使用redux,工具包尽可能的实现了vue组件使用vuex的方式去使用redux,通过vm.$redux可以在组件实例里获取到redux状态管理

<template>
  <div>
    redux状态testState1: {{$redux.state.testState1}}
  </div>
</template>

<script>
export default {
  name: 'demo3',
  mounted () {
    // 打印redux的testState2状态值
    console.log(this.$redux.state.testState2)
    // 五秒后将testState1修改成8888
    // 需要在reducer里存在一个action的type为test1可以修改testState1
    // 这里需要按照标准的redux的action标准(必须有type)触发dispatch
    setTimeout(() => {
      this.$redux.dispatch({
        type: 'test1',
        value: 8888
      })
    }, 5000)
  }
}
</script>

applyVuex

作用:使得所有的Redux组件可以使用Vuex的状态管理
对工具包开启vuex状态管理,这个场景一般存在于以Vue为主的项目中,为了使React组件也可以共享到vuex,需要在项目的入口文件引入applyVuex方法(整个项目应该只引一次),将vuex的store作为参数传入

import store from '../store' // vuex的store文件
applyVuex(store)

connectVuex

类似react-redux的connect方法,在React组件中使用,由于vuex的关键字比redux多,所以将参数改成了对象,包含了mapStateToProps、mapCommitToProps、mapGettersToProps、mapDispatchToProps,每个都是一个纯函数,返回一个对象(和redux的connect使用方式完全一致)

export default connectVuex({
  mapStateToProps (state) {
    return {
      vuexState: state,
      state1: state.state1,
      moduleAstate: state.moduleA
    }
  },
  mapCommitToProps (commit) {
    return {
      vuexCommit: commit
    }
  },
  // mapGettersToProps = (getters) => {},
  // mapDispatchToProps = (dispatch) => {},
})(ReactComponent)

lazyVueInReact

在React的router里懒加载Vue组件

import React, { lazy, Suspense } from "react"
import { lazyVueInReact } from 'vuereact-combined'
const Hello = lazy(() => import("./react_app/hello"));
//懒加载vue组件
const TestVue = lazyVueInReact(() => import("./vue_app/test.vue"))


export default [
{
    path: "/reactHello",
    component: () => {
        return (
            <Suspense fallback={<div>Loading...</div>}>
                <Hello />
            </Suspense>
        );
    }
},
{
    path: "/vuetest1",
    component: () => {
        return (
            <Suspense fallback={<div>Loading...</div>}>
                <div>
                    <h1>我是一个vue组件</h1>
                    <TestVue />
                </div>
            </Suspense>
        );
    }
}]

lazyReactInVue

在Vue的router里懒加载React组件

import Vue from 'vue'
import VueRouter from 'vue-router'
import { lazyReactInVue } from 'vuereact-combined'
Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'home',
    component: () => import('../views/Home')
  },
  {
    path: '/reactInVueDemo',
    name: 'reactInVueDemo',
    component: lazyReactInVue(() => import('../reactComponents/cc.jsx'))
  }
]

const router = new VueRouter({
  routes
})

export default router

withVueRouter

在react组件中获取vue router对象,可以通过props属性获取倒$vueRouter和$vueRoute

import React from 'react'
import { withVueRouter } from 'vuereact-combined'
class Test2 extends React.Component {
  constructor (props) {
    super(props)
  }
  componentWillMount () {

  }
  componentDidMount () {
    // 可以通过props属性获取倒$vueRouter和$vueRoute
    console.log(this.props.$vueRouter, this.props.$vueRoute)
  }

  render () {
    return (
      <div>
        test2
        <h3>{this.props.$vueRoute.query.b}</h3>
      </div>
    )
  }
}
export default withVueRouter(Test2)

applyReactRouterInVue

建议在react项目的app或者main引入,然后再任何一个被转换的vue组件中都可以直接获取到实例属性$reactRouter,其中包含了react router的history、location、match

app.jsx

import { applyReactRouterInVue } from 'vuereact-combined'
import { withRouter } from 'react-router-dom'
applyReactRouterInVue(withRouter)

demo.vue

<template>
    <div>
      <h1>demo</h1>
      <h2>{{$reactRouter.location.search}}</h2>
    </div>
</template>

<script>
export default {
  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

import VueComponent from './VueComponent.vue'
import { applyVueInReact } from 'vuereact-combined'
const VueComponentInReact = applyVueInReact(VueComponent, {
  react: {
    // react.componentWrapAttrs代表是vue组件在react组件中的组件包囊层的标签设置
    // 以下设置将设置组件的包囊层div的display为inline-block
    componentWrapAttrs: {
      style: {
            display: 'inline-block'
        },
      class: 'react-wrap-vue-component-1'
    },
    // react.slotWrapAttrs代表是vue组件在react组件中的插槽包囊层的标签设置
    // 以下设置将设置插槽的包囊层div的display为inline-block
    slotWrapAttrs: {
      style: {
          display: 'inline-block'
      }
    },
  },
})

以下是默认配置

// 默认配置
const originOptions = {
    react: {
        componentWrap: 'div',
        slotWrap: 'div',
        componentWrapAttrs: {
            __use_react_component_wrap: '',
            style: {
                all: 'unset'
            }
        },
        slotWrapAttrs: {
            __use_react_slot_wrap: '',
            style: {
                all: 'unset'
            }
        }
    },
    vue: {
        // 组件wrapper
        componentWrapHOC: (VueComponentMountAt, nativeProps = []) => {
            // 传入portals
            return function ({ portals = [] } = {}) {
                return (<div {...nativeProps}>{VueComponentMountAt}{portals.map((Portal, index) => <Portal key={index}/>)}</div>)
            }
        },
        componentWrapAttrs: {
            'data-use-vue-component-wrap': '',
            style: {
                all: 'unset',
            }
        },
        slotWrapAttrs: {
            'data-use-vue-slot-wrap': '',
            style: {
                all: 'unset'
            }
        }
    }
}

支持程度

在react组件中引入vue组件

功能支持程度说明
普通属性完全支持
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

在vue组件中引入react组件

功能支持程度说明
普通属性完全支持
具名插槽完全支持在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