7.消息总线
其实微前端,就是将每个app装入了自己的黑匣子中,与外界相对隔离,相互之间无法通信,这其实与我们的现实并不符合。所以,微前端应用之间的相互通信,成了微前端架构的关键之一。
应用之间的通信,可以分为两类:
- 每个黑匣子里面发生了什么,外面如何知道?
- 每个黑匣子都是有生命周期的,当被卸载的时候,外面如何知道你已经被卸载了,卸载后又如何保证正常的通信?
解决方案
github上的single-spa-portal-example提供了解决方案。
该方案是基于redux的。
pre.了解redux的原理

- 原理简单描述下:
这里有以下几类角色:
- 组件:A、B、C
- 仓库:store,里面有数据状态state
- 管理员:reducer
reducer管理着仓库store中的状态state,只要reducer才能修改仓库中的状态
当组件A需要修改仓库中变量a的值,就告诉管理员reducer:我要把仓库中的a变量的值加1。
这时候reducer就会去查下有没有加这个动作,有的话,就给变量a加1,没有就不变。
由于其他组件B和C都一直观察者仓库的状态,一旦变化,就会更新自己用到的变量。
加入这时候reducer把a加1了,那么B和C立马会将自己组件内用到的a更新成最新值。
白话文往往是最好的解释,但程序员不得不将这白话文转成代码:
- 首先我们创建如下目录结构:
reducers
├── actions
│ └── cart-actions.js
├── reducers
│ ├── cart-reducer.js
│ ├── index.js
│ └── products-reducer.js
├── index.js
└── store.js
- 首先,我们需要创建一个仓库store——创建store.js
import { createStore } from 'redux';
import rootReducer from './reducers/index.js';
const store = createStore(rootReducer);
export default store;
-
我们看到创建仓库store需要一份rootReducer,这个rootReducer就是仓库总管,为啥是总管,而不是简简单单的管理员?因为rootReducer管理着所有reducer。我们来看下reducers文件夹下的内容。
- 在cart-reducer.js
import { ADD_TO_CART } from '../actions/cart-actions'; let initialState = { cart: [ { product: 'bread 700g', quantity: 2, unitCost: 90 }, { product: 'milk 500ml', quantity: 1, unitCost: 47 } ] } /** * * * @export * @param {*} [state=initialState] 仓库总的数据,默认值为initialState * @param {*} action 操作,内有属性type和playload,分别表示动作类型和新数据 */ export default function(state=initialState, action){ switch (action.type) { case ADD_TO_CART: return { ...state, cart: [...state.cart, action.payload] }; default: return state; } }
- 在products-reducer.js
export default function(state=[], action) { return state; }
- 然后将这两个reducer汇总,即index.js
import { combineReducers } from 'redux'; import cartReducer from './cart-reducer.js'; import productsReducer from './products-reducer.js'; let allReducers = [ cartReducer, productsReducer, ] const rootReducer = combineReducers(allReducers); export default rootReducer;
这时候就产生了一个rootReducer。
- 我们在上面的reducer代码中看到需要引入一个actions。这个actions是干嘛的,其实就是上面白话文重点的加1这个动作。这个动作包含两个内容,一个是"加"这个操作,一个是"1"这个载荷,所以,我们可以这么来写代码:
- actions/cart-actions.js
export const ADD_TO_CART = 'ADD_TO_CART'; export function addToCart(product, quantity, unitCost) { return { type: ADD_TO_CART, payload: { product, quantity, unitCost } } }
- 上面第一个export的是动作加,第二个export其实就是载荷。
- 另外,我们还需要去订阅和派发修改数据。即在index.js中:
import store from './store.js';
import { addToCart } from './actions/cart-actions.js';
console.log("inital state", store.getState());// 获取全部数据
let unsubscribe = store.subscribe(() => {
console.log(store.getState());
})
// 提交数据(修改仓库数据)
store.dispatch(addToCart('Coffee 500mg', 1, 250));
store.dispatch(addToCart('Flour 1kg', 2, 110));
store.dispatch(addToCart('Juice 2L', 1, 250));
// 取消观察
unsubscribe();
- store.getState()获取仓库中的数据
- store.subscribe订阅(观察)仓库,一旦有变化,就在回调内处理。
- store.dispatch派发,修改数据,其中的参数就代表了修改数据的行为和数值,就是白话文中的"加1".
如何使用redux,成为微前端的消息总线
基本思路是这样的:
- 每个应用都暴露出自己的store.js,这个store.js内容其实就是暴露该应用自己的store(这句好像是废话,尴尬!),如下:
import { createStore, combineReducers } from 'redux'
const initialState = {
app: 'react',
refresh: 2
}
function reactRender(state = initialState, action) {
switch (action.type) {
case 'REFRESH':
return { ...state,
refresh: state.refresh + 1
}
default:
return state
}
}
// 向外输出 Reducer
export const storeInstance = createStore(combineReducers({ namespace: () => 'react', reactRender }));// 特别注意:这里的参数是对象,对象的value必须是函数
- **特别注意: ** createStore(combineReducers())的参数是对象,对象的value必须是函数
- 注册应用: 在入口文件single-spa.config.js中,导入每个应用的配置所组成的数组,然后循环这个数组,对每份配置进行注册:
/* 以下是模块加载器版的config */
require('babel-polyfill')
import * as singleSpa from 'single-spa';
// 导入所有模块的配置集合
import projectConfig from './project.config.js';
import { registerApp } from './Register';
async function bootstrap(){
// 批量注册:对所有模块依次注册
projectConfig.forEach(config => {
registerApp({
name: config.name,
main: config.main,
url: config.prefix,
store: config.store,
base: config.base,
path: config.path,
})
})
singleSpa.start();
}
// 启动
bootstrap();
- 上面注册的核心是registerApp函数,该函数来自Register.js,但是在看Register.js之前,需要先来看一下GlobalEventDistributor这个类:
export class GlobalEventDistributor {
constructor() {
// 在函数实例化的时候,初始一个数组,保存所有模块的对外api
this.stores = [];
}
// 注册
registerStore(store) {
this.stores.push(store);
}
// 触发,这个函数会被种到每一个模块当中.便于每一个模块可以调用其他模块的 api
// 大致是每个模块都问一遍,是否有对应的事件触发.如果每个模块都有,都会被触发.
dispatch(event) {
this.stores.forEach((s) => {
s.dispatch(event)
});
}
// 获取所有模块当前的对外状态
getState() {
let state = {};
this.stores.forEach((s) => {
let currentState = s.getState();
// console.log('currentState', currentState)
state[currentState.namespace] = currentState
});
return state
}
}
Register.js如下:
// Register.js
import * as singleSpa from 'single-spa';
//全局的事件派发器 (新增)
import { GlobalEventDistributor } from './GlobalEventDistributor'
const globalEventDistributor = new GlobalEventDistributor();
// hash 模式,项目路由用的是hash模式会用到该函数
export function hashPrefix(app) {
return function (location) {
let isShow = false
//如果该应用 有多个需要匹配的路劲
if(isArray(app.path)){
app.path.forEach(path => {
if(location.hash.startsWith(`#${path}`)){
isShow = true
}
});
}
// 普通情况
else if(location.hash.startsWith(`#${app.path || app.url}`)){
isShow = true
}
console.log('hashPrefix', isShow)
return isShow;
}
}
// pushState 模式
export function pathPrefix(app) {
return function (location) {
let isShow = false
//如果该模块 有多个需要匹配的路径
if(isArray(app.path)){
app.path.forEach(path => {
if(location.pathname.indexOf(`${path}`) === 0){
isShow = true
}
});
}
// 普通情况
else if(location.pathname.indexOf(`${app.path || app.url}`) === 0){
isShow = true
}
return isShow;
}
}
// 应用注册
export async function registerApp(params) {
// 导入派发器
let storeModule = {}, customProps = { globalEventDistributor: globalEventDistributor };
// 在这里,我们会用SystemJS来导入模块的对外输出的Reducer(后续会被称作模块对外API),统一挂载到消息总线上
try {
storeModule = params.store ? await import(`./src/${params.store}`) : { storeInstance: null };
} catch (e) {
console.log(`Could not load store of app ${params.name}.`, e);
//如果失败则不注册该模块
return
}
// 注册应用于事件派发器
if (storeModule.storeInstance && globalEventDistributor) {
//取出 redux storeInstance
customProps.store = storeModule.storeInstance;
// 注册到全局
globalEventDistributor.registerStore(storeModule.storeInstance);
}
//当与派发器一起组装成一个对象之后,在这里以这种形式传入每一个单独模块
customProps = { store: storeModule, globalEventDistributor: globalEventDistributor };
// 在注册的时候传入 customProps
singleSpa.registerApplication(params.name, () => import(`./src/${params.main}`), params.base ? (() => true) : pathPrefix(params), customProps);
}
//数组判断 用于判断是否有多个url前缀
function isArray(o){
return Object.prototype.toString.call(o)=='[object Array]';
}
- 上面registerApp这个函数,也就是真正注册的过程,具体步骤是这样的:
-
- 创建自定义属性集合customProps,这个customProps为对象,存放着全局的globalEventDistributor
-
- 创建派发器storeModule,这个派发器里面的storeInstance属性就存放着对应应用的仓库store
-
- 将storeModule.storeInstance挂在到指定以熟悉集合customProps的store属性上
-
- 将应用的仓库实例storeInstance全局注册
-
- 这时候自定义属性集合为{ store: storeModule, globalEventDistributor: globalEventDistributor },里面包含了当前应用的派发器,也包含了全局的globalEventDistributor,里面存放着所有应用的store
-
- 利用singleSpa.registerApplication真正注册应用
-
- 注册完毕后,在应用的入口文件,就能获取到全局所有应用的store,其实是拿到了刚刚传入的storeModule,这里面包含了所有应用的store。
- 在vue中,入口文件应该这样写:
import Vue from 'vue';
import singleSpaVue from 'single-spa-vue';
import Hello from './main.vue'
import createVuexStore from './vuexStore/index.js';
import { CHANGE_ORIGIN, GLOBAL_PROPS } from './vuexStore/types.js';
let store = createVuexStore();
const vueLifecycles = singleSpaVue({
Vue,
appOptions: {
el: '#vue',
render: r => r(Hello),
store,
}
});
export const bootstrap = [
vueLifecycles.bootstrap,
];
// export const mount = [
// vueLifecycles.mount,
// ];
export function mount(props) {
console.log('传递进来的属性', props); // do something with the common authToken in app1
// 这个props就是传递过来的globalEventDistributor,全局的数据,也就是Register.js中registerApplication时传入的第三个参数customProps
// 这时候,就可以用vuex将数据传递到仓库中了
let origin = props.globalEventDistributor.stores[0].getState().reactRender.app;
store.commit('users/' + CHANGE_ORIGIN, origin);
store.commit('users/' + GLOBAL_PROPS, props); // 将全局的globalEventDistributor挂到vue的store中,这样可以在vue中派发事件去更新其他应用的仓库,因为globalEventDistributor包含了所有应用的store。
return vueLifecycles.mount(props);
}
export const unmount = [
vueLifecycles.unmount,
];