在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 (
    <div>
      {
        // React App 挂载点
        activeRouteType !== 'vue' &&  (
          <section>
            {props.children}
          </section>
        )
      }
      {
        {/* Vue App 挂载点 */}
        <section style={{display: activeRouteType === 'vue' ? 'block' : 'none'}}>
          <div ref={containerRef} />
        </section>
      }
    </div>
  );
}
    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;
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,463评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,868评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,213评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,666评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,759评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,725评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,716评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,484评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,928评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,233评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,393评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,073评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,718评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,308评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,538评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,338评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,260评论 2 352