想要融合两个不同项目时,最关键的地方也就是怎样去融合路由和状态树。
实现方式
如图所示,我们需要做的就是:
- 调整布局组件,为外部项目提供一个固定挂载点;
- 每个子项目都会有一个固定的路由出口;
- 区分路由,实现不同框架路由跳转;
- 在项目特定的路由出口会提供该项目所需要的context等数据;
- 对应项目下的路由在新项目中的表现将会与在原项目中的变现一致。
实现难点
-
- Bridge(桥)的实现;
我们需要桥的目的是要在不同的状态管理库之间实现数据共享,并实时更新。(该功能可借助vuex中间件和 Redux中间件或enhancer来实现。)
- Bridge(桥)的实现;
-
- 路由处理
为了能够正常渲染不同框架下的路由,我们需要对路由的跳转做一些区分。
从React页面跳转到vue页面时需要使用vue的路由进行跳转,并更新状态树。
从Vue页面跳转到React页面时需要使用React的路由进行跳转,并更新状态树。
- 路由处理
具体实现
在这里,我们以dva app为作为外壳。路由融合在dva app内完成。
-
- 路由的共存显示方式需要状态树的参与。所以我们先实现状态树的共享。
这里实现了一个通用的简单工具,为了实现通用性,仅提供了注册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();
});
}
}
这里实际上就是实现了一个订阅发布规则,每当状态改变,我们就会通知观察者,观察者执行相应的操作。
-
- 在入口处注册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([]));
到这里,我们实现了状态树的注册。
-
- 使用状态树,例如在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 挂载点 */}
}
);
}
-
- 跳转路由时更新状态树
对于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;