background

In recent years, the micro front end has become more and more popular, and many teams around are trying to implement the micro front end. After understanding the basic concept of the micro front end, an old project needs to be revised and redone, because this project is special and faces some difficult problems:

  • The project forexpress + ejs + jqueryThe front and back ends do not separate items
  • Historical reasons Project code is extremely difficult to maintain (mainly due to lack of modularity and various bad redundant page redundant code)
  • Historical reasons The middleware and some query logic are redundant, resulting in slow page opening

The original idea was simplereactRefactoring the whole project to achieve modularity and front and back end separation so that you don’t have to maintain disgusting old code later, but because another project was more scheduled and important at the time, the whole thing workedreactRefactor the project on time is too late, a concept I decided to take the new project under micro front end, because the old project page revised only covers all details of the relevant pages, creating relevant pages, the rest of the many pages, there is no change, I decided on the new project for the demand for micro front-end transform, according to the features of old projects I modular disassemble, The module structure of the disassembled micro front end is as follows:

  • Main application (used to mount other sub-applications)
  • Details sub-application (all details pages)
  • Login sub-application (Login registration related module)
  • Authoring sub-application (Article Authoring Editor module)
  • Iframe subapplication (load other modules of the old project that have not been reconstructed by iframe)

Use React + single-SPA to develop each sub-application and the main application separately. However, there is still a problem. The remaining pages that do not need to be changed are ejS +jquery projects rendered on the server side. How do I load it into the main application of the new project? My final solution was to create an additional iframe shell application and use the React-Router route to control loading. The specific solution will be described later.

At this point, the general architecture of the new project is already out, and all that remains is the technical implementation details ~

Single – spa principle

We are using the framework development such as the react or vue ordinary single-page applications, will be in accordance with the function of the reuse of packaging or style the page into many components, components can be assembled into a separate page, and through the routing control to monitor changes in the address bar, the dynamic load different pages and components, this is the simple principle of single-page applications run. The principle of single-SPA is similar, and it can also be understood as a single page application developed in a modular way. However, modules divided into single page application for a single purpose are various components, while in the micro front end, each module is an independent sub-application. When users access the main application of the base, each sub-application will be registered according to the configuration. After the sub-application is loaded, it will be distributed to the page in the corresponding sub-application according to the route. This is the route loading principle of Single-SPA. First, the application is distributed, and then the application is distributed

Base application development

According to the previous division idea, we know that our base application should have the function of registering and loading sub-applications. After careful analysis of the requirements, we find: We all need each application of the basic user identity information such as userid data, but these data acquisition is registered in the login logic, so we need a similar story, ascending to the need to share data base application for storage and distribution, it has realized the data sharing among different applications, That is, the message bus single-SPA distributes applications through routes. When the URL cannot be distributed to the corresponding application, the 404 page should be redirected, and the link from App to wechat should be shared. Without changing the App, because the route of the same page of the old and new projects is different. So we also need to do some special routing redirection at the base layer, so in the base application we need to implement the following functions:

  • The module loader that loads the child application
  • A global state repository is responsible for state sharing and distribution
  • Route control module

Module loader

Single-spa is usually paired with SystemJS for module loading, which uses systemJS to request the single-SPA entry file address of the packaged child application. Then, SystemJS parses and executes the received code through Eval. Because there are multiple child applications to load, encapsulate a module loader

/* /ice/src/Loader.js */ import * as singleSpa from "single-spa"; import createHistory from "history/createBrowserHistory"; export const history = createHistory(); /** * Check whether routes match * @param {string} path Path to be matched */ export const pathMatch = (path) => {return (location) => {return location.pathname.startsWith(`${path}`); }; }; /** * Obtain the corresponding entry file * @param {string} URL * @param {string} baseUrl * @param {string} entryName */ export by applying the json file generated by packaging const getEntryfromJson = async (url, baseUrl, entryName) => { const fetchResult = await fetch(url).then((s) => s.json()); const { entrypoints, publicPath } = fetchResult; let truePath = `${publicPath}${entrypoints[entryName].assets[0]}` return getResult; }; /** * child application loader * @param {string} name * @param {string} pathName * @param {string} appUrl * @param {string} baseUrl * @param {boolean} hasStore * @param {GlobalEventDistributor} globalEventDistributor */ export const loadApp = async ( name, pathName, appUrl, baseUrl, hasStore, globalEventDistributor ) => { let store = {}, props = { globalEventDistributor }; try { store = hasStore ? await getEntryfromJson(appUrl, baseUrl, "store") : { storeInstance: null }; } catch (err) {console.log(' Failed to load ${name} data warehouse: ${err} '); } if (store.storeInstance && globalEventDistributor) { props.store = store.storeInstance; globalEventDistributor.registerStore(store.storeInstance); } SystemJS.config({ transpiler: "transpiler-module" }); props.history = history; props.data = globalEventDistributor && globalEventDistributor.getState(); singleSpa.registerApplication( name, () => getEntryfromJson(appUrl, baseUrl, "singleSpaEntry"), pathMatch(pathName), props ); };Copy the code

Instead of requesting the file address directly, the file address is read from THE JSON file generated by each subproject. The purpose of this is to solve the caching problem, so that the hash value behind the file name generated by the subproject package is different each time and will be written into the JSON file. From json we can know what the name of the corresponding file is, which solves the caching problem such as opening the wechat browser.

Global state store

Implement a global redux data warehouse in single-SPA like the common single-page React app. Implement a state store in each child app. Then load the store object into the base app. Integrate store functions, the code is as follows:

/ / Export class GlobalEventDistributor{constructor() {super(); / / Import class GlobalEventDistributor{constructor() {super(); this.stores = []; } registerStore(store) { this.stores.push(store); } dispatch(event) { this.stores.forEach((s) => { s.dispatch(event); this.emit("dispatch"); }); } getState() { let state = {}; this.stores.forEach((s) => { let currentState = s.getState(); state = { ... state, ... currentState }; }); return state; } subscribe(subHandle) { this.stores.forEach(s => { s.subscribe(() => { subHandle.apply(null, [this.getState()]) }) }) } }Copy the code

Single-spa entry file

Register all sub-apps in single-SPA’s package entry file

import * as singleSpa from "single-spa";
import { GlobalEventDistributor } from "./globalEventDistributor";
import { loadApp } from "./loader";

const init = async () => {
  const globalEventDistributor = new GlobalEventDistributor();
  const loadingArray = [];
  loadingArray.push(
    loadApp(
      "login",
      "/login",
      "/app1/manifest.json",
      "/app1",
      true,
      globalEventDistributor
    )
  );
  loadingArray.push(
    loadApp(
      "show",
      "/show",
      "/app2/manifest.json",
      "/app2",
      false,
      globalEventDistributor
    )
  );
  loadingArray.push(
    loadApp(
      "create",
      "/create",
      "/app3/manifest.json",
      "/app3",
      true,
      globalEventDistributor
    )
  );
  loadingArray.push(
    loadApp("iframe", "/iframe", "/app4/manifest.json", "/app4", false, null)
  );
  await Promise.all(loadingArray);
  singleSpa.start();
};
init();
Copy the code

This completes the basic development of the base application, but there is a small optimization point, because we have an iframe subapplication in the initial application planning, that is, all the non-newly developed pages are routed to the iframe subapplication, and the iframe in this application is used to open the corresponding pages in the old project. So we need to design the routing control module in the base application. Here I use the react-router for routing management. Create a route.js file and set this file as a package entry in the Webpack.

import React from "react"; import { BrowserRouter, Switch, Route, Redirect } from "react-router-dom" import {history} from "./Loader.js" const Router = () => { return ( <BrowserRouter history={history}> <Switch> <Route path="/show"/> <Route path="/login" /> <Route path="/create" /> <Route path="/iframe"  /> <Redirect to={`/iframe${window.location.pathname}`} /> </Switch> </BrowserRouter> ) } export default RouterCopy the code

Since we have introduced systemJS to load the corresponding application in the single-SPA package entry, systemJS will take over the application load when the route changes to the corresponding address, so there is no need to control the page load through the react-Router in route.js. The Redirect component is not triggered when the route is matched. This code implements the logic that non-in-app routes are redirected to iframe. For example: www.abc.com/test will be redirected to www.abc.com/iframe/test. In the new application, I did not implement the corresponding page of the route test, but in the old project, so I opened the old page through iframe, which realized the gradual migration for the purpose of reconstruction

summary

This is the implementation and thinking of the base application. I will not go into the webpack configuration. I will continue to share the implementation and packaging and deployment of sub-applications