一、父子间通信
1.1 基于 Props
注册子应用列表
在父应用(Vue)中,进行所有子应用服务的注册。
main.js
import { navList } from './utils/sub';
import { registerApp } from './utils';
// 注册、加载、启动子应用
registerApp(navList);
在登记所有子应用服务的配置信息时,需将父应用的状态以及状态对应操作的action等信息添加至配置信息中。
utils/sub.js
import * as loading from '../store/loading';
import * as appInfo from '../store';
/**
* 创建子应用的所有信息
*/
export const navList = [
{
name: 'react15', // 唯一的Key值,子应用的唯一标识
entry: '//localhost:9002/', // 告诉主应用去哪个入口获取子应用的文件
loading,
container: '#micro-container', // 渲染容器:告知子应用在哪个容器中进行显示
activeRule: '/react15', // 子应用激活规则
appInfo, // 将主应用的store传递给子应用
},
{
name: 'react16',
entry: '//localhost:9003/',
loading,
container: '#micro-container',
activeRule: '/react16',
appInfo,
},
...
]
挂载子应用
挂载子应用的时候(即mount生命周期),主应用的状态会传递给子应用。
export const mounted = async (app) => {
app?.mount?.({
appInfo: app.appInfo, // 将主应用的状态传给子应用
entry: app.entry,
});
await runMainLifeCycle('mounted');
};
缓存父应用状态
在子应用(react16)中的 mount 生命周期中,缓存父应用信息。
index.js
export async function mount(app) {
setMain(app); // 缓存父应用
render();
}
utils/main.js
将父应用的全局状态以及状态操作的相关 Action 存储至子应用 main 中。
let main = null;
export const setMain = (data) => {
main = data;
};
export const getMain = () => {
return main;
};
更改父应用状态
在子应用的登录页面,调用父应用传递过来action,进行父应用的全局状态修改。
pages/login/index.jsx
const Login = () => {
useEffect(() => {
const main = getMain();
if (!main.appInfo) {
return;
}
// 登录页面隐藏头部底部
main.appInfo.footerState.changeFooter(false);
main.appInfo.headerState.changeHeader(false);
}, []);
...
}
1.2 基于 CustomEvent
创建事件总线
在父应用中,基于 CustomEvent 创建一个事件总线(EventBus)。
micro/event/index.js
/**
* 事件总线
*/
export class EVENT_BUS {
// 监听事件
on(name, cb) {
window.addEventListener(name, (e) => cb(e.detail));
}
// 触发事件
emit(name, data) {
const event = new CustomEvent(name, {
detail: data,
});
window.dispatchEvent(event);
}
}
在启动微前端框架时创建事件总线实例,并将该实例添加至全局window对象中,方便子应用访问。
micro/event/start.js
// ...
import { EVENT_BUS } from './event';
const event_bus = new EVENT_BUS();
// 监听 init 事件
event_bus.on('init', (data) => {
console.log('bootstrap event:', data);
});
// 重要:添加事件总线全局标识
window.__EVENT_BUS__ = event_bus;
// ...
基于事件总线通信
在子应用(react16)中的 bootstrap 生命周期中,基于事件总线触发一个事件(init事件),在父应用中则会监听到该事件,并打印相关日志信息。
export async function bootstrap() {
window.__EVENT_BUS__.emit('init', {
msg: 'react16 bootstrap success',
});
}
二、子应用间通信
2.1 基于 CustomEvent
子应用之间的通信可以借助事件总线(EventBus)来完成,创建事件总线的过程参照上述1.2节内容。
在 vue2 子应用中,添加对 react16 事件的监听,并在监听到事件后派发一个事件,示例代码如下:
export async function mount() {
window.__EVENT_BUS__.on('react16', (data) => {
console.log('vue2 event:', data);
window.__EVENT_BUS__.emit('vue2', {
msg: 'vue2 mount success',
});
});
render();
}
在 react16 子应用的 mount 生命周期中,添加对 vue2 事件的监听,并派发一个事件,示例代码如下:
export async function mount(app) {
window.__EVENT_BUS__.on('vue2', (data) => {
console.log('react16 event:', data);
});
window.__EVENT_BUS__.emit('react16', {
msg: 'react16 mount success',
});
setMain(app);
render();
}
当 vue2 子应用切换到 react16 子应用的时候,控制台就会输出如下日志。由此完成子应用之间的消息通信。
vue2 event: {msg: "react16 mount success"}
react16 event: {msg: "vue2 mount success"}
注:以上机制是存在不少问题的,需注意下
- 需在unmount生命周期中,取消对事件的监听,否则每次执行mount生命周期就会不停地添加新的事件监听;
- 切换不同的子应用时,挂载事件监听的顺序不一致,可能会导致丢失一些事件的处理。如上述 react16子应用切换 vue2 子应用时,由于 vue2 子应用还未挂载监听事件,导致会丢失对
event react16
事件的处理; - 若当前监听的事件非常多,不同子应用中可能会出现监听事件重名情况,针对这一问题就需做事件名的唯一性进行管理,某种程度上提升了开发难度;
2.2 基于 Props
子应用之间的通信还可以借助 Props,通信逻辑大致如下:子应用1 -> 父应用 -> 子应用2
,可参考1.1节内容。
三、全局状态管理
在 2.1 节中介绍子应用基于 Custom Event 进行兄弟应用间通信是存在不少问题的,所以基于 Custom Event 进行子应用间通信并不是一个理想的技术方案,此时还可以借用全局状态管理(全局store)来进行应用间的通信。
全局状态管理也算是通信的一种,它不需我们自己去定义监听事件,也可以触发到我们所有的监听内容。
创建全局状态管理
micro/store/index.js
export const createStore = (initData = {}) =>
(() => {
// 利用闭包缓存初始数据
let store = initData;
// 管理所有的订阅者(即依赖内容)
const observers = [];
// 获取store
const getStore = () => store;
// 更新store
const update = (value) => {
if (value !== store) {
const oldValue = store; // 缓存旧store
store = value; // 更新新的store
// 通知所有订阅者store发生了变化(新值、旧值)(订阅者可能是异步,所以需加async)
observers.forEach(async (item) => await item(store, oldValue));
}
};
// 添加订阅者
const subscribe = (fn) => {
observers.push(fn);
};
return {
getStore,
update,
subscribe,
};
})();
在主应用中进行子应用服务注册时,创建全局状态管理实例,并挂载在window对象上。
main/src/utils/index.js
const store = createStore();
// 将实例挂载在window对象上
window.store = store;
store.subscribe((newValue, oldValue) => {
console.log('subscribe:', newValue, oldValue);
});
在子应用中就可以获取全局状态管理实例,并进行相应的状态更新。
react16/index.js
export async function mount(app) {
const storeData = window.store.getStore();
window.store.update({
...storeData,
a: 1111,
});
setMain(app);
render();
}
综上,可以看出使用全局状态有以下好处:
- 不使用任何 eventName,就可以做到事件监听;
- 同一个事件,可以添加非常多的订阅(observers);