在React中融合Vue项目

想要融合两个不同项目时,最关键的地方也就是怎样去融合路由和状态树。

实现方式

框架融合.png

如图所示,我们需要做的就是:

  • 调整布局组件,为外部项目提供一个固定挂载点;
  • 每个子项目都会有一个固定的路由出口;
  • 区分路由,实现不同框架路由跳转;
  • 在项目特定的路由出口会提供该项目所需要的context等数据;
  • 对应项目下的路由在新项目中的表现将会与在原项目中的变现一致。

实现难点

    1. Bridge(桥)的实现;
      我们需要桥的目的是要在不同的状态管理库之间实现数据共享,并实时更新。(该功能可借助vuex中间件和 Redux中间件或enhancer来实现。)
    1. 路由处理
      为了能够正常渲染不同框架下的路由,我们需要对路由的跳转做一些区分。
      从React页面跳转到vue页面时需要使用vue的路由进行跳转,并更新状态树。
      从Vue页面跳转到React页面时需要使用React的路由进行跳转,并更新状态树。

具体实现

在这里,我们以dva app为作为外壳。路由融合在dva app内完成。

    1. 路由的共存显示方式需要状态树的参与。所以我们先实现状态树的共享。
      这里实现了一个通用的简单工具,为了实现通用性,仅提供了注册store的方法,并未提供具体实现。
// Combiner.js
export class Combiner {
  observers = [];
  state = {}

  subscribe(func) {
    this.observers.push(func);
    // 订阅时,主动推送一次状态
    func(this.state);
    // 返回一个方法用于取消订阅
    return () => {
      this.observers.filter(f => f !== func)
    }
  }

  publish() {
    this.observers.forEach(obs => obs({...this.state}));
  }

  registerStore(namespace, handler) {
    handler((state) => {
      this.state[namespace] = state;
      this.publish();
    });
  }
}

这里实际上就是实现了一个订阅发布规则,每当状态改变,我们就会通知观察者,观察者执行相应的操作。

    1. 在入口处注册store
import  {Combiner} from './utils/bridge/combiner.js';
import store from './store/index'; // vuex store

// Initialize
const app = dva({
  history: require('history').createBrowserHistory()
});

window.dvaApp = app;

// 创建Combiner实例,并挂载在window对象上(为了实现单例,后期可以注册到di工具)
const appStore = window.store = new Combiner();

// 注册dva的状态
appStore.registerStore('dva', (callback) => {
  app.use({
    onStateChange(state) {
     callback(state)
    }
  });
})

// 注册vuex状态
appStore.registerStore('vuex', callback => {
  store.subscribe((mutation, state) => {
    callback(state);
  })
})

// 订阅示例
appStore.subscribe((state) => {
  console.log('__app_state', state);
});

// Model
app.model(require('./models/example').default);

// Router
app.router(require('./router').default([]));

到这里,我们实现了状态树的注册。

    1. 使用状态树,例如在layout中,我们整合了vue路由和react路由,为了区分路由,我们在状态树中保存了一个变量来标记当前路由类型(routing.type = 'vue' | 'react')。但是当前应该显示哪个路由我们需要根据状态树中的某个状态确定,我们要使用vuex的状态。
import React, { useRef, useLayoutEffect, useEffect, useState } from 'react';
import { createVueApp } from '../../utils/entryCreator';

export default (props) => {
  const { dispatch } = props;
  const containerRef = useRef(null);
  const [activeRouteType, setActiveRouteType] = useState('/');
  
  /* 创建并挂载Vue实例 */
  useLayoutEffect(() => {
    const container = containerRef.current;
     // createVueApp 为封装的一个方法,用于创建一个完整的Vue App实例,参考附录代码
    const app = createVueApp();
    app.$mount(container);
  }, []);
  
  /*
    订阅状态,将vuex状态树中的状态set到当前组件的state中。
    实际上下边的逻辑很容易抽离成一个通用hook或者HOC,这里仍然使用原始的模式。
  */
  useEffect(() => {
    const unsubscribe = window.store.subscribe(function(state) {
      if (state.vuex.routing) {
        setActiveRouteType(state.vuex.routing.type);
      }
    });
    return () => {
      // 组件卸载时取消订阅
      unsubscribe();
    }
  }, []);
  return (
    
{ // React App 挂载点 activeRouteType !== 'vue' && (
{props.children}
) } { {/* Vue App 挂载点 */}
}
); }
    1. 跳转路由时更新状态树
      对于vue的路由跳转,我们可以借助于路由守卫完成,示例如下:
import VueRouter from 'vue-router';
import Vue from 'vue';
import TestPage from '../routes/TestPage.vue';
import store from '../store'
import { routerRedux } from 'dva/router'
Vue.use(VueRouter)
const router = new VueRouter({
  routes: [
    { 
      path: '/vue',
      component: () => import('../Layout/VueRouterLayout.vue'),
      children: [
        { path: 'test', component: TestPage},
        { path: 'test2', component: () => import('../routes/TestPage2.vue')}
      ]
    }
  ],
  mode: 'history',
});
/*
    为了方便跳转React页面,我们在router实例上挂载一个pushReact方法
  这里是简单实现。
  注意:如果直接push React路由,那么Vue的路由并不会离开,如果再次用Vue路由push方才的页面,vue会认为你
  在当前的路由重复push当前的路由,所以会报错,并且失败。所以在这里,我们先replace Vue的路由。再 push 
  React路由。
*/
router.pushReact = function(pathname) {
  this.replace(pathname).then(() => 
    window.dvaApp._store.dispatch(
      routerRedux.push({
        pathname
      })
    )
  )
}
// 进入路由之前,将路径信息commit到vuex的状态树
router.beforeEach((to, form, next) => {
  store.commit('setRouteType', 'vue');
  next();
})
export default router;

对于React的路由跳转,则可以借助于dva的subscriptions,例如:

import router from '../config/vue-routes'
import store from '../store/index'

export default {
  namespace: 'example',
  state: {},
  effects: {},
  reducers: {},
  
  subscriptions: {
    setup({ history, dispatch }) {
      history.listen(({ pathname }) => {
        store.commit('setRouteType', 'react');
      });
    },
  }
};

之所以区分出来,是因为React的路由状态和Vue的路由状态是不共享的,无法相互触发。

到此为止,我们已经可以在两个框架的路由之间任意跳转。并且状态树之间的也做到了共享。

关键点

  • React跳Vue路由时使用Vue的路由方法;
  • Vue跳react路由时使用React的路由方法。
    跳转路由时,实时更新状态树。

附录

entryCreator.js

import Vue from 'vue';
import App from '../App.vue';
import router from '../config/vue-routes';
import store from '../store';

export function createVueApp() {
  return new Vue({
    router,
    render: h => h(App),
    store
  });
}

vuex 状态树(store.js)

import Vuex from 'vuex';
import Vue from 'vue';
import bridge from '../utils/bridge/bridge'

Vue.use(Vuex);

const store = new Vuex.Store({
  state: {
    routing: {type: 'vue'},
  },
  mutations: {
    setRouteType(state, payload) {
      state.routing.type = payload;
    },
  },
  plugins: []
});

export default store;

你可能感兴趣的:(在React中融合Vue项目)