Micro Frontend Solution 5 - Route Distribution

Route-Distributed Micro Frontend

From Application Distributing Routes to Routes Distributing Applications

There is no better way to explain micro frontend routing than this sentence.

Route-distributed micro frontend distributes different business functions to different, independent front-end applications through routing. This can usually be achieved through reverse proxy on an HTTP server, or through the routing that comes with the application framework. Currently, route-distributed micro frontend architecture is probably the most adopted and easiest “micro frontend” solution. However, this approach looks more like a聚合 of multiple front-end applications, i.e., we just piece together these different front-end applications to make them look like a complete whole. But they are not — every time a user goes from App A to App B, they often need to refresh the page. — Quoted from phodal The Matters of Micro Frontends

In the example code from the Module Loader chapter, the steps for route-distributed applications were already fully demonstrated.

In single-page front-end routing, there are currently two forms: One is hash routing, which is compatible with all mainstream browsers. The basic principle is that a change in the URL’s hash value triggers the browser’s onhashchange event, which then triggers component updates.

The other is the History API, supported only by modern browsers. When window.history.pushState(null, null, "/profile/"); is called, it triggers component updates.

// hash mode, used when project routing uses hash mode
export function hashPrefix(app) {
  return function (location) {
    let isShow = false;
    // If the app has multiple paths to match
    if (isArray(app.path)) {
      app.path.forEach((path) => {
        if (location.hash.startsWith(`#${path}`)) {
          isShow = true;
        }
      });
    }
    // Normal case
    else if (location.hash.startsWith(`#${app.path || app.url}`)) {
      isShow = true;
    }
    return isShow;
  };
}

// pushState mode
export function pathPrefix(app) {
  return function (location) {
    let isShow = false;
    // If the module has multiple paths to match
    if (isArray(app.path)) {
      app.path.forEach((path) => {
        if (location.pathname.indexOf(`${path}`) === 0) {
          isShow = true;
        }
      });
    }
    // Normal case
    else if (location.pathname.indexOf(`${app.path || app.url}`) === 0) {
      isShow = true;
    }
    return isShow;
  };
}

// Application registration
export async function registerApp(params) {
  // The third parameter indicates whether the module should be displayed
  singleSpa.registerApplication(
    params.name, // Module name
    () => SystemJS.import(params.main), // Module rendering entry file
    params.base ? () => true : pathPrefix(params) // Module display condition
  );
}

Routes Distributing Applications

When the URL prefix matches the prefix in the configuration, singleSpa activates the corresponding module and renders its content.

Application Distributing Routes

When a module is activated, it reads the URL and renders the corresponding page.

This is the workflow of micro frontend routing.

Challenges of Micro Frontend Routing

Hash Routing

Currently, all front-end frameworks that support SPA support hash routing. The general principle of hash routing is: a change in the URL’s hash value triggers the browser’s onhashchange event, which then triggers component updates. All front-end frameworks update pages based on onhashchange. When our architecture uses micro frontends, if we choose hash routing, we can ensure that all front-end technology frameworks have consistent update events. So using hash routing is also the most worry-free approach. If you don’t mind the # character in hash route URLs, using hash routing in micro frontends is also recommended.

HTML5 History Routing

As everyone knows, HTML5 added two new APIs on the History object (pushState and replaceState). With these two new APIs, we can also update the page without refreshing, and the URL does not need to have a # character. This maintains the highest aesthetic quality (for some people). Of course, almost all mainstream SPA technology frameworks now support this feature. But the problem is that when these two APIs are triggered, there is no global event fired. Different technology frameworks implement History routing differently. Even with the same React tech stack, there are several versions of routing.

So how do we ensure coordination between routing of multiple technology framework modules under one project?

Only One History

Prerequisite: Assume all our projects use React, and all use the same version of routing.

Idea: We can do this. In our base front-end module’s Store.js (because it is always loaded first and never destroyed), instantiate the React Router core library history, and pass this instance to all modules through the message bus. When initializing routing in each module, you can customize your own history. Redirect the module’s history to the passed-in history. This way, coordination between all modules’ routing can be achieved. Because when the page switches, history triggers the page update event. When all modules share the same history instance, all modules will update to the correct page. This ensures that all modules and routing are coordinated.

If you don’t understand what I’m talking about, here’s the code:

// Base front-end module's Store.js
import { createStore, combineReducers } from "redux";

// React router's core library history
import createHistory from "history/createBrowserHistory";

const history = createHistory();

// Export it
export const storeInstance = createStore(
  combineReducers({ namespace: () => "base", history })
);
// Application registration
export async function registerApp(params) {
    ...

    // Import history directly, import the instance via systemjs
    try {
        storeModule = params.store ? await SystemJS.import(params.store) : { storeInstance: null };
    } catch (e) {
        ...
    }
    ...

    // Put it in customProps along with the dispatcher
    customProps = { store: storeModule, globalEventDistributor: ... };


    // Pass customProps during registration
    singleSpa.registerApplication(params.name,
                                () => SystemJS.import(params.main),
                                params.base ? (() => true) : pathPrefix(params),
                                customProps // During registration, history will be included in customProps and injected into the module
                                );
}
// React main.js
import React from 'react'
import ReactDOM from 'react-dom'
import singleSpaReact from 'single-spa-react'
import RootComponent from './root.component'

const reactLifecycles = singleSpaReact({
  React,
  ReactDOM,
  rootComponent: (spa) => {
    // Here, pass history to the component
    return <RootComponent  history={spa.customProps.history}/>
  },
  domElementGetter: () => document.getElementById('root')
})

...
// RootComponent
import React from "react";
import { Provider } from "react-redux";
export default class RootComponent extends React.Component {
  render() {
    return (
      <Provider store={this.state.store}>
        // Redirect Router's history here
        <Router history={this.props.history}>
          <Switch>...</Switch>
        </Router>
      </Provider>
    );
  }
}

The above demonstrates how to make all modules’ routing coordinate by ensuring only one history instance.

Multi-Technology Stack Module Routing Coordination

Problem: The above approach works, but unfortunately, its application scenario is limited. It can only be used with a single technology stack and a single routing version. One of the biggest advantages of micro frontends is the freedom to choose your technology stack. In one project, multiple technology stacks suitable for different modules can be used.

Idea: We can actually have each module externally expose a route navigation interface, and based on the message bus dispatch, let each module render to the correct page. For example, if Module A wants to navigate to /a/b/c, Module A first updates to the page at the /a/b/c route, then tells all modules through the message bus that the navigation should go to /a/b/c. Then other modules that have the /a/b/c route will navigate directly, and those that don’t will do nothing.

We can do this:

// Store.js
import { createStore, combineReducers } from "redux";
import createHistory from "history/createBrowserHistory";
const history = createHistory();

// Export a 'to' interface. When a module needs to navigate, it calls this interface on all modules,
// and the corresponding module renders to the correct page.
function to(state, action) {
  if (action.type !== "to") return { ...state, path: action.path };
  history.replace(action.path);
  return { ...state, path: action.path };
}

export const storeInstance = createStore(
  combineReducers({ namespace: () => "base", to })
);

export { history };

This is a perfect combination of routing and message bus. The message bus has much more potential, which will be explained gradually later. 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-5-route-distribution/

# Related Articles