一、父子间通信

1.1 基于 Props

注册子应用列表

父应用(Vue)中,进行所有子应用服务的注册。
main.js

  1. import { navList } from './utils/sub';
  2. import { registerApp } from './utils';
  3. // 注册、加载、启动子应用
  4. registerApp(navList);

在登记所有子应用服务的配置信息时,需将父应用的状态以及状态对应操作的action等信息添加至配置信息中。
utils/sub.js

  1. import * as loading from '../store/loading';
  2. import * as appInfo from '../store';
  3. /**
  4. * 创建子应用的所有信息
  5. */
  6. export const navList = [
  7. {
  8. name: 'react15', // 唯一的Key值,子应用的唯一标识
  9. entry: '//localhost:9002/', // 告诉主应用去哪个入口获取子应用的文件
  10. loading,
  11. container: '#micro-container', // 渲染容器:告知子应用在哪个容器中进行显示
  12. activeRule: '/react15', // 子应用激活规则
  13. appInfo, // 将主应用的store传递给子应用
  14. },
  15. {
  16. name: 'react16',
  17. entry: '//localhost:9003/',
  18. loading,
  19. container: '#micro-container',
  20. activeRule: '/react16',
  21. appInfo,
  22. },
  23. ...
  24. ]

挂载子应用

挂载子应用的时候(即mount生命周期),主应用的状态会传递给子应用。

  1. export const mounted = async (app) => {
  2. app?.mount?.({
  3. appInfo: app.appInfo, // 将主应用的状态传给子应用
  4. entry: app.entry,
  5. });
  6. await runMainLifeCycle('mounted');
  7. };

缓存父应用状态

子应用(react16)中的 mount 生命周期中,缓存父应用信息。
index.js

  1. export async function mount(app) {
  2. setMain(app); // 缓存父应用
  3. render();
  4. }

utils/main.js
将父应用的全局状态以及状态操作的相关 Action 存储至子应用 main 中。

  1. let main = null;
  2. export const setMain = (data) => {
  3. main = data;
  4. };
  5. export const getMain = () => {
  6. return main;
  7. };

更改父应用状态

在子应用的登录页面,调用父应用传递过来action,进行父应用的全局状态修改。
pages/login/index.jsx

  1. const Login = () => {
  2. useEffect(() => {
  3. const main = getMain();
  4. if (!main.appInfo) {
  5. return;
  6. }
  7. // 登录页面隐藏头部底部
  8. main.appInfo.footerState.changeFooter(false);
  9. main.appInfo.headerState.changeHeader(false);
  10. }, []);
  11. ...
  12. }

1.2 基于 CustomEvent

创建事件总线

在父应用中,基于 CustomEvent 创建一个事件总线(EventBus)
micro/event/index.js

  1. /**
  2. * 事件总线
  3. */
  4. export class EVENT_BUS {
  5. // 监听事件
  6. on(name, cb) {
  7. window.addEventListener(name, (e) => cb(e.detail));
  8. }
  9. // 触发事件
  10. emit(name, data) {
  11. const event = new CustomEvent(name, {
  12. detail: data,
  13. });
  14. window.dispatchEvent(event);
  15. }
  16. }

启动微前端框架时创建事件总线实例,并将该实例添加至全局window对象中,方便子应用访问。
micro/event/start.js

  1. // ...
  2. import { EVENT_BUS } from './event';
  3. const event_bus = new EVENT_BUS();
  4. // 监听 init 事件
  5. event_bus.on('init', (data) => {
  6. console.log('bootstrap event:', data);
  7. });
  8. // 重要:添加事件总线全局标识
  9. window.__EVENT_BUS__ = event_bus;
  10. // ...

基于事件总线通信

子应用(react16)中的 bootstrap 生命周期中,基于事件总线触发一个事件(init事件),在父应用中则会监听到该事件,并打印相关日志信息。

  1. export async function bootstrap() {
  2. window.__EVENT_BUS__.emit('init', {
  3. msg: 'react16 bootstrap success',
  4. });
  5. }

二、子应用间通信

2.1 基于 CustomEvent

子应用之间的通信可以借助事件总线(EventBus)来完成,创建事件总线的过程参照上述1.2节内容。

vue2 子应用中,添加对 react16 事件的监听,并在监听到事件后派发一个事件,示例代码如下:

  1. export async function mount() {
  2. window.__EVENT_BUS__.on('react16', (data) => {
  3. console.log('vue2 event:', data);
  4. window.__EVENT_BUS__.emit('vue2', {
  5. msg: 'vue2 mount success',
  6. });
  7. });
  8. render();
  9. }

react16 子应用的 mount 生命周期中,添加对 vue2 事件的监听,并派发一个事件,示例代码如下:

  1. export async function mount(app) {
  2. window.__EVENT_BUS__.on('vue2', (data) => {
  3. console.log('react16 event:', data);
  4. });
  5. window.__EVENT_BUS__.emit('react16', {
  6. msg: 'react16 mount success',
  7. });
  8. setMain(app);
  9. render();
  10. }

vue2 子应用切换到 react16 子应用的时候,控制台就会输出如下日志。由此完成子应用之间的消息通信。

  1. vue2 event: {msg: "react16 mount success"}
  2. 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

  1. export const createStore = (initData = {}) =>
  2. (() => {
  3. // 利用闭包缓存初始数据
  4. let store = initData;
  5. // 管理所有的订阅者(即依赖内容)
  6. const observers = [];
  7. // 获取store
  8. const getStore = () => store;
  9. // 更新store
  10. const update = (value) => {
  11. if (value !== store) {
  12. const oldValue = store; // 缓存旧store
  13. store = value; // 更新新的store
  14. // 通知所有订阅者store发生了变化(新值、旧值)(订阅者可能是异步,所以需加async)
  15. observers.forEach(async (item) => await item(store, oldValue));
  16. }
  17. };
  18. // 添加订阅者
  19. const subscribe = (fn) => {
  20. observers.push(fn);
  21. };
  22. return {
  23. getStore,
  24. update,
  25. subscribe,
  26. };
  27. })();

在主应用中进行子应用服务注册时,创建全局状态管理实例,并挂载在window对象上
main/src/utils/index.js

  1. const store = createStore();
  2. // 将实例挂载在window对象上
  3. window.store = store;
  4. store.subscribe((newValue, oldValue) => {
  5. console.log('subscribe:', newValue, oldValue);
  6. });

在子应用中就可以获取全局状态管理实例,并进行相应的状态更新。
react16/index.js

  1. export async function mount(app) {
  2. const storeData = window.store.getStore();
  3. window.store.update({
  4. ...storeData,
  5. a: 1111,
  6. });
  7. setMain(app);
  8. render();
  9. }

综上,可以看出使用全局状态有以下好处:

  • 不使用任何 eventName,就可以做到事件监听;
  • 同一个事件,可以添加非常多的订阅(observers);