Micro Frontend Solution 4 - Message Bus
The main function of the micro frontend message bus is to bridge communication between modules.
Black Box
Problem 1:
After applying microservices architecture, each individual module becomes a black box.
What happens inside, what state changes, other modules have no way of knowing.
For example, when Module A wants to perform an action based on a certain internal state of Module B, there is no way for black boxes to communicate. This is a major problem.
Problem 2:
Each module has its own lifecycle. When a module is unmounted, how can normal communication be maintained afterward?
ps. We must solve these problems. Communication between modules is absolutely necessary.
Breaking Down Barriers
On GitHub, single-spa-portal-example provides a solution.
It implements a message bus for front-end microservices based on Redux (without affecting the use of other state management tools when writing code).
The general idea is as follows:

Each module exposes a Store.js file externally.
The content of this file looks roughly like this:
import { createStore, combineReducers } from "redux";
const initialState = {
refresh: 0,
};
function render(state = initialState, action) {
switch (action.type) {
case "REFRESH":
return { ...state, refresh: state.refresh + 1 };
default:
return state;
}
}
// Export Store externally
export const storeInstance = createStore(
combineReducers({ namespace: () => "base", render })
);
Does this code look familiar?
Yes, it’s just a regular Store file.
The Store.js exported by each module is that module’s Store.
How is Store.js Used?
We need to export this Store.js in the module loader.
So we made the following modifications to the Register.js file from the module loader (which appeared in the previous chapter, go back if you don’t understand):
import * as singleSpa from 'single-spa';
// Global event dispatcher (newly added)
import { GlobalEventDistributor } from './GlobalEventDistributor'
const globalEventDistributor = new GlobalEventDistributor();
// hash mode, used when project routing uses hash mode
export function hashPrefix(app) {
...
}
// pushState mode
export function pathPrefix(app) {
...
}
// Application registration
export async function registerApp(params) {
// Import the dispatcher
let storeModule = {}, customProps = { globalEventDistributor: globalEventDistributor };
// Here, we use SystemJS to import the module's externally exported Store (referred to as module external API), and mount it on the message bus
try {
storeModule = params.store ? await SystemJS.import(params.store) : { storeInstance: null };
} catch (e) {
console.log(`Could not load store of app ${params.name}.`, e);
// If it fails, don't register the module
return
}
// Register app with the event dispatcher
if (storeModule.storeInstance && globalEventDistributor) {
// Extract the redux storeInstance
customProps.store = storeModule.storeInstance;
// Register with the global dispatcher
globalEventDistributor.registerStore(storeModule.storeInstance);
}
// After assembling with the dispatcher into an object, pass it into each module in this form
customProps = { store: storeModule, globalEventDistributor: globalEventDistributor };
// Pass customProps during registration
singleSpa.registerApplication(params.name, () => SystemJS.import(params.main), params.base ? (() => true) : pathPrefix(params), customProps);
}
GlobalEventDistributor
The main responsibility of the global event dispatcher is to trigger the external API of each module.
GlobalEventDistributor.js
export class GlobalEventDistributor {
constructor() {
// When the function is instantiated, initialize an array to hold all module external APIs
this.stores = [];
}
// Register
registerStore(store) {
this.stores.push(store);
}
// Dispatch, this function will be injected into each module so that every module can call other modules' APIs
// Roughly, it asks each module if there is a corresponding event to trigger. If multiple modules have it, all will be triggered.
dispatch(event) {
this.stores.forEach((s) => {
s.dispatch(event);
});
}
// Get all modules' current external state
getState() {
let state = {};
this.stores.forEach((s) => {
let currentState = s.getState();
console.log(currentState);
state[currentState.namespace] = currentState;
});
return state;
}
}
Receiving the Dispatcher and Own Store in the Module
As mentioned above, when registering the app, we pass a customProps containing the dispatcher and store.
In each individual module, how do we receive and use these passed-in items?
import React from "react";
import ReactDOM from "react-dom";
import singleSpaReact from "single-spa-react";
import RootComponent from "./root.component";
import { storeInstance, history } from "./Store";
import "./index.less";
const reactLifecycles = singleSpaReact({
React,
ReactDOM,
rootComponent: (spa) => {
// When creating the lifecycle, we pass what the message bus injects into the component as props
// This way, each module can directly call and query other modules' APIs and states
return (
<RootComponent
store={spa.customProps.store.storeInstance}
globalEventDistributor={spa.customProps.globalEventDistributor}
/>
);
},
domElementGetter: () => document.getElementById("root"),
});
export const bootstrap = [reactLifecycles.bootstrap];
export const mount = [reactLifecycles.mount];
export const unmount = [reactLifecycles.unmount];
To be continued …
Related Articles
Micro Frontend Solution 1 - Thinking
Micro Frontend Solution 2 - Single-SPA
Micro Frontend Solution 3 - Module Loader
Micro Frontend Solution 4 - Message Bus
Micro Frontend Solution 5 - Route Distribution
Micro Frontend Solution 6 - Build and Deploy
Micro Frontend Solution 7 - Static Data Sharing
Micro Frontend Solution 8 - Secondary Build
Demo
Micro Frontend Base App Source Code
Micro Frontend Submodule Source Code