“This article has participated in the call for good writing activities, click to view: the back end, the big front end double track submission, 20,000 yuan prize pool waiting for you to challenge!”

preface

For now, talking about the micro front end inevitably involves the single-SPA library, for its role in the micro front end, the work of many people are still a little understanding, this article would like to take you to unlock the mystery of it

To read this article, you need to have a basic understanding of the use of single-SPA. For those who have not used it, please read my previous article ()[]

Read with questions

If I could sum up the role of single-SPA in one sentence, I think it would be:

Single-spa provides application lifecycle management for the micro front end

I usually read the source code with questions, and I’m going to solve single-SPA around the following two questions, which are the two main things that single-SPA does

  • 1. How to deal with routing?
  • 2. How is the application lifecycle managed?

How is routing handled?

When we use registerApplication to register the application, the third parameter can be used to match the route of the child application and define the route matching rule. SingleSpa calls this method when the route changes. Mount and unmount applications based on window.location changes

There are two problems here

PustState or history.replaceState does not trigger the popState and hashchange events, so how does singleSpa listen for route changes?

2. We know that popState and Hashchange events can be listened to by multiple listeners, but in the micro-front-end scenario, in order to ensure that the application loads properly and avoid collisions with other listeners, single-SPA should have the highest enforcement power. So how did Single-SPA get its first executive rights?

How do I monitor route changes to ensure primary execution

As mentioned earlier, popState and hashchange events are not triggered when we actively call history.pustState or history.replaceState

Single-spa’s solution is this

PushState and replaceState are called and window.dispatchEvent is called to trigger event 2. Intercept window. AddEventListener and window. The removeEventListener pushState and replaceState events, customize the trigger

We can source in the SRC/navigation/navigation – events. See this logic js


if (isInBrowser){
    // omit the code
    
    // Intercepts native methods
     window.history.pushState = patchedUpdateState(
    window.history.pushState,
    "pushState"
  );
  window.history.replaceState = patchedUpdateState(
    window.history.replaceState,
    "replaceState"
  );
}

/ / to the native window. History. PushState and window. The history. The replaceState to intercept
function patchedUpdateState(updateState, methodName) {
  return function () {
    const urlBefore = window.location.href;
    const result = updateState.apply(this.arguments);
    const urlAfter = window.location.href;

    if(! urlRerouteOnly || urlBefore ! == urlAfter) {if (isStarted()) {
        // Manually send a hashchange popState event
        window.dispatchEvent(
          createPopStateEvent(window.history.state, methodName)
        );
      } else{ reroute([]); }}return result;
  };
}
Copy the code
  window.addEventListener = function (eventName, fn) {  
    Intercept the ["hashchange", "popState "] event
    if (typeof fn === "function") {
      if (
        routingEventsListeningTo.indexOf(eventName) >= 0 &&
        !find(capturedEventListeners[eventName], (listener) = > listener === fn)
      ) {
        capturedEventListeners[eventName].push(fn);
        return; }}return originalAddEventListener.apply(this.arguments);
  };

  window.removeEventListener = function (eventName, listenerFn) {
    if (typeof listenerFn === "function") {
      if (routingEventsListeningTo.indexOf(eventName) >= 0) {
        capturedEventListeners[eventName] = capturedEventListeners[
          eventName
        ].filter((fn) = >fn ! == listenerFn);return; }}return originalRemoveEventListener.apply(this.arguments);
  };

Copy the code

This ensures that the route change event is triggered and that single-SPA processes the route first

When the route changes, single-spa calls rerouter, which is important because the application life cycle is executed in this method, just remember that reroute is triggered in the Hashchange and popState events

 function urlReroute() {
  reroute([], arguments);
}
  window.addEventListener("hashchange", urlReroute);
  window.addEventListener("popstate", urlReroute);

Copy the code

How is the application life cycle managed?

registerApplication

Let’s take a look at what registerApplication does. Simply wrap the parameters, create an application with state NOT_LOADED, and call reroute. In addition to popState and hashchange, there is one more place to call reroute

export function registerApplication(appNameOrConfig, appOrLoadApp, activeWhen customProps) {
  // Validates and wraps the parameters passed in
  const registration = sanitizeArguments(
    appNameOrConfig,
    appOrLoadApp,
    activeWhen,
    customProps
  );
  // omit the code

  // Register the application as NOT_LOADED
  apps.push(
    assign(
      {
        loadErrorTime: null.status: NOT_LOADED,
        parcels: {},
        devtools: {
          overlays: {
            options: {},
            selectors: [],
          },
        },
      },
      registration
    )
  );
  
  / / call reroute
  if(isInBrowser) { ensureJQuerySupport(); reroute(); }}Copy the code

In addition to registerApplication, one of the things we have to do in the base is call this method which is also very simple, set started to true, and call reroute method start method start,

export function start(opts) {
  started = true;
  if (opts && opts.urlRerouteOnly) {
    setUrlRerouteOnly(opts.urlRerouteOnly);
  }
  if(isInBrowser) { reroute(); }}Copy the code

So far, there are three ways to trigger reroute

  • The start method
  • RegisterApplication method
  • Hashchange, popState events

In addition, Single-SPA provides an actively triggered method called triggerAppChange

You can see how important reroute is

In this method, management of the entire application life cycle, application state flow

Application status

For convenience, I summarize the application states as follows

Application state describe
NOT_LOADED The application has not been loaded, default state
LOADING_SOURCE_CODE The second parameter to register the application is called in load
NOT_BOOTSTRAPPED The load is complete, but the applied bootstrap function has not yet been called
BOOTSTRAPPING Calling the applied bootstrap function
NOT_MOUNTED The bootstrap call succeeded, but the mount was not called
MOUNTED The mount life cycle function of the application is successfully executed, and the application is successfully mounted
UNLOADING To unload applications that have not been mounted, use the Unload method
UNMOUNTING The unmount life cycle method is invoked. After the call is successful, the state changes to NOT_MOUNTED
SKIP_BECAUSE_BROKEN Applying the change state failed and the next state change will not be made
LOAD_ERROR The application fails to load and is reloaded the next time you reroute more than 200 milliseconds
## Reroute executes the process
So let’s analyze thisrerouteThe execution process of
export function reroute(pendingPromises = [], eventArguments) {
  /** * If an application is in the state of state change, this trigger is temporarily saved. When performAppChanges completes, this variable will be false */
  if (appChangeUnderway) {
    return new Promise((resolve, reject) = > {
      peopleWaitingOnAppChange.push({
        resolve,
        reject,
        eventArguments,
      });
    });
  }
  // appsToUnload: The current state is NOT_BOOTSTRAPPED or NOT_MOUNTED, and the current route is not matched, that is, the application is not mounted
  // appsToUnmount: The state is MOUNTED and routes do not match
  // appsToLoad: The current route matches NOT_LOADED, LOADING_SOURCE_CODE, and LOAD_ERROR. If the value is LOAD_ERROR, the time since the last loading failure must exceed 200 milliseconds
  // appsToMount: The current status is NOT_BOOTSTRAPPED or NOT_MOUNTED, and the current route matches
  const {
    appsToUnload,
    appsToUnmount,
    appsToLoad,
    appsToMount,
  } = getAppChanges();

  let appsThatChanged,
    navigationIsCanceled = false,
    oldUrl = currentUrl,
    newUrl = (currentUrl = window.location.href);
  const stared = isStarted();
  if (stared) {
    appChangeUnderway = true;
    appsThatChanged = appsToUnload.concat(
      appsToLoad,
      appsToUnmount,
      appsToMount
    );
    return performAppChanges();
  } else {
    appsThatChanged = appsToLoad;
    // The application is not loaded yet. There is no mount, only NOT_BOOTSTRAPPED state
    return loadApps();
  }
 // omit the code
Copy the code

As you can see, reroute has a queuing mechanism that only performs one application state change at a time. The logic for applying state variables is in performAppChanges

Js function performAppChanges() {return promise.resolve ().then() => {// https://github.com/single-spa/single-spa/issues/545 / / trigger some custom event, omit code... // If resources need to be unmounted, perform some operations on the unload Lifecycle function // 2. Const unloadPromises = AppstounLoad.map (toUnloadPromise); const unmountUnloadPromises = appsToUnmount .map(toUnmountPromise) .map((unmountPromise) => unmountPromise.then(toUnloadPromise)); const allUnmountPromises = unmountUnloadPromises.concat(unloadPromises); const unmountAllPromise = Promise.all(allUnmountPromises); /** */ const loadThenMountPromises = appstoload.map ((app) => {return toLoadPromise(app).then((app) => tryToBootstrapAndMount(app, unmountAllPromise) ); }); const mountPromises = appsToMount .filter((appToMount) => appsToLoad.indexOf(appToMount) < 0) .map((appToMount) => { return tryToBootstrapAndMount(appToMount, unmountAllPromise); }); return unmountAllPromise .catch((err) => { callAllEventListeners(); throw err; }).then(() => {// Events registered with window.addeventListener will be raised. CallAllEventListeners will use a try/cash package. return Promise.all(loadThenMountPromises.concat(mountPromises)) .catch((err) => { pendingPromises.forEach((promise) => promise.reject(err)); throw err; }) /** * In finishUpAndReturn, reroute */. Then (finishUpAndReturn) is reroute */. Then (finishUpAndReturn); }); }); }Copy the code