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 Demo

Micro Frontend Module Loader

Micro Frontend Base App Source Code

Micro Frontend Submodule Source Code

Article Link:

/en/archive/micro-frontend-solution-4-message-bus/

# Related Articles