• Micro front End 01: Js isolation mechanism analysis (Snapshot sandbox, two kinds of proxy sandbox)
  • Micro Front End 02: Analysis of microapplication loading process (from registration of microapplication to internal implementation of loadApp method)
  • Micro front End 03: Sandbox Container Analysis of Universe (concrete application of Js sandbox mechanism after establishment)
  • Microfront 04: Resource loading mechanism for The Universe (internal implementation of import-HTml-Entry)
  • Micro front End 05: Implementation of loadMicroApp method and analysis of data communication mechanism
  • Microfront 06: Registration mechanism for single-SPA
  • Microfront 07: Analysis of single-SPA route management and microapplication state management

Apart from manual loading and mounting of microapplications, the main application scenario of single-SPA is automatic management of registered microapplications based on route switching. There are two important links in this process, one is to monitor and control the changes of routes, and the other is to change some states of micro-applications according to the changes of routes. This paper is divided into two parts, from the source level of single-SPA route management and microapplication state management are analyzed.

Route Management Mechanism

The primary logic for routing management initialization

Please browse the flow chart first:

From the flowchart, single-SPA does four things with regard to the initialization of routing management. Let’s also look at the code:

// Snippet 1
if (isInBrowser) {
  // Corresponds to step 1 of the flowchart
  window.addEventListener("hashchange", urlReroute);
  window.addEventListener("popstate", urlReroute);
  // Corresponding to step 2 of the flowchart
  // A lot of code is omitted here...
  window.addEventListener = function (eventName, fn) {
    if (typeof fn === "function") {
      // A lot of code is omitted here...
      // If eventName is hashchange or popState, store fn in an array variable and return directly
    }
    return originalAddEventListener.apply(this.arguments);
  };

  window.removeEventListener = function (eventName, listenerFn) {
    if (typeof listenerFn === "function") {
      // A lot of code is omitted here...
      // If eventName is hashchange or popState, remove listenerFn from the array variable and return directly
    }
    return originalRemoveEventListener.apply(this.arguments);
  };
  // Corresponding to step 3 of the flowchart
  window.history.pushState = patchedUpdateState(
    window.history.pushState,
    "pushState"
  );
  window.history.replaceState = patchedUpdateState(
    window.history.replaceState,
    "replaceState"
  );
  // Save a lot of code...
  // This corresponds to step 4 of the flowchart
  window.singleSpaNavigate = navigateToUrl;
}
Copy the code

With the flowchart and the code, each step is probably understandable, but why is the source code written this way? We can describe these four steps this way. First, listen for the Hashchange, popState event itself; Second, intercept listener functions that set hashchange and popState listener events. Third, intercept API methods that might change the state of the route; Finally, a global utility function is provided to facilitate users to change the routing state. There are two things worth noting here. One isto understand that calling history.pushState or history.replaceState does not trigger a popState event. Second, if we attempt to hijack a behavior in century coding, we need to hijack not only the corresponding listening event, but also the corresponding API that may change the behavior without triggering the listening event. In addition, it is important to note that the code segment 1 is the code in the file SRC/navigation/navigation – events. The outermost layer of js, not in function and call in, although the single – spa is an excellent open source libraries, But I still think such code structure is not worth learning and should be avoided in actual coding. Below is a description of the MDN documentation for the History API.

Note that just calling history.pushState() or history.replaceState() won’t trigger a popstate event. The popstate event will be triggered by doing a browser action such as a click on the back or forward button (or calling history.back() or history.forward() in JavaScript).

From the code in step 1 of the flowchart corresponding to snippet 1 above, we know that single-spa registers listening events for popState and hashchange, and that the urlReroute function is executed when the event is triggered. The urlReroute function simply calls reroute([], arguments). The reroute function is important because almost all (not all, because single-SPA also has some routing unrelated mechanisms) state changes and management of microapplications are closely related to this function, which will be covered in a future article. Now let’s continue with the initialization of route management.

Hijacking monitor event

Let’s take a look at the implementation of the hijacking hashchange, popState event:

// Snippet 2
const originalAddEventListener = window.addEventListener;
const originalRemoveEventListener = window.removeEventListener;
window.addEventListener = function (eventName, fn) {
    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) {
    // The internal implementation is similar to window.addeventListener and will not be described here
};
Copy the code

Core logic code snippet 2, we can be summed up, the original window. AddEventListener and window removeEventListner saved, and then to rewrite the two monitoring function. If the event to be listened for is hashchange or popState, store the corresponding callback function in an array and wait for batch execution at the appropriate time, while listening for the function to return. If need to monitor function not hashchange or popstate, call the original window. The addEventListener or Windows. The removeEventListner register corresponding to monitor events. This mechanism for changing the behavior of the original function is called Monkeypatch, as noted in this article:

// Monkeypatch addEventListener so that we can ensure correct timing
Copy the code

In actual coding, if there is a similar scenario, this can be used for reference.

Hijack history apis

// Snippet 3
window.history.pushState = patchedUpdateState(
    window.history.pushState,
    "pushState"
);
window.history.replaceState = patchedUpdateState(
    window.history.replaceState,
    "replaceState"
);
Copy the code

Code snippet 3 is intercept window. History. PushState and window history. ReplaceState two apis.

patchedUpdateState

The specific handler function is patchedUpdateState with the following code:

// Snippet 4
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()) {
        window.dispatchEvent(
          createPopStateEvent(window.history.state, methodName)
        );
      } else{ reroute([]); }}return result;
  };
}
Copy the code

Internal logic is relatively simple, mainly to do three things, one is the call has not been rewritten window. History. PushState or window. History. ReplaceState, and record the results as a result. The value of window.location.href before and after the API call is recorded. The third is to trigger a custom event, which is generated by createPopStateEvent.

createPopStateEvent

// Snippet 5
function createPopStateEvent(state, originalMethodName) {
  let evt;
  try {
    evt = new PopStateEvent("popstate", { state });
  } catch (err) {
    evt = document.createEvent("PopStateEvent");
    evt.initPopStateEvent("popstate".false.false, state);
  }
  evt.singleSpa = true;
  evt.singleSpaTrigger = originalMethodName;
  return evt;
}
Copy the code

In code snippet 5, three main things are done: first, initialize an event object, second, add some properties to the event object to identify the nature, and third, do some compatibility handling. For reasons why you should do this, read the address of the relevant issues indicated in the source code.

window.singleSpaNavigate

Corresponding to step 4 in the flowchart above, there is the following line of code.

window.singleSpaNavigate = navigateToUrl;
Copy the code

The main function of this function is shown in the comments in the source code:

For convenience in onclick attributes, we expose a global function for navigating to whatever an <a> tag’s href is.

Let’s see how navigateToUrl handles it:

export function navigateToUrl(obj) {
  let url;
  if (typeof obj === "string") {
    url = obj;
  } else if (this && this.href) {
    url = this.href;
  } else if (
    obj &&
    obj.currentTarget &&
    obj.currentTarget.href &&
    obj.preventDefault
  ) {
    url = obj.currentTarget.href;
    obj.preventDefault();
  } else {
    // omit some code... An error is thrown here
  }

  const current = parseUri(window.location.href);
  const destination = parseUri(url);

  if (url.indexOf("#") = = =0) {
    window.location.hash = destination.hash;
  } else if(current.host ! == destination.host && destination.host) {// omit some code... Some conditional judgment
    window.location.href = url;
  } else if (
    destination.pathname === current.pathname &&
    destination.search === current.search
  ) {
    window.location.hash = destination.hash;
  } else {
    // different path, host, or query params
    window.history.pushState(null.null, url); }}Copy the code

The above logic is relatively simple, so I won’t explain it line by line. Instead, I’m going to use the single-SPA documentation here to help understand why the source code was written that way. At the end of the day this is just a tool method and its absence does not affect the program itself, so it is not important to translate the document line by line here.

/*Use this utility function to easily perform url navigation between registered applications without needing to deal with event.preventDefault(), pushState, triggerAppChange(), etc. arguments navigationObj: string | context | DOMEvent navigationObj must be one of the following types: a url string. a context / thisArg that has an href property; useful for calling singleSpaNavigate.call(anchorElement) with a reference to the anchor element or other context. a DOMEvent object for a click event on a DOMElement that has an href attribute; ideal for the  use case.*/

// Three ways of using navigateToUrl
singleSpa.navigateToUrl('/new-url');
singleSpa.navigateToUrl(document.querySelector('a'));
document.querySelector('a').addEventListener(singleSpa.navigateToUrl); <! -- A fourth way to use navigateToUrl,this one inside of your HTML -->
<a href="/new-url" onclick="singleSpaNavigate(event)">My link</a>
Copy the code

Now that we have a better understanding of single-SPA route management, let’s move on to state management of microapplications, another important topic of single-SPA.

State management mechanisms for microapplications

This part will be explained in two aspects:

  • The state types of microapplications and the transformation relationship between the states
  • The state switching process of microapplications

Know what states microapplications have, and then look at how the source code implements these state changes.

State types of microapplications

We look at the SRC/applications/app. Helpers. Js file constant statement about the state of the micro application:

// App statuses
export const NOT_LOADED = "NOT_LOADED";
export const LOADING_SOURCE_CODE = "LOADING_SOURCE_CODE";
export const NOT_BOOTSTRAPPED = "NOT_BOOTSTRAPPED";
export const BOOTSTRAPPING = "BOOTSTRAPPING";
export const NOT_MOUNTED = "NOT_MOUNTED";
export const MOUNTING = "MOUNTING";
export const MOUNTED = "MOUNTED";
export const UPDATING = "UPDATING";
export const UNMOUNTING = "UNMOUNTING";
export const UNLOADING = "UNLOADING";
export const LOAD_ERROR = "LOAD_ERROR";
export const SKIP_BECAUSE_BROKEN = "SKIP_BECAUSE_BROKEN";
Copy the code

From the code, you can intuitively see that there are 12 states. To make it easier to grasp the core functionality of single-SPA, the following two states will be omitted in this article, which will cover the descriptions of the other 10 states.

export const LOAD_ERROR = "LOAD_ERROR";
export const SKIP_BECAUSE_BROKEN = "SKIP_BECAUSE_BROKEN";
Copy the code

The state switching process of microapplications

Let’s start with a flow chart:

It can be seen intuitively from the figure that although there are many states, excluding the abnormal state, the number of microapplication states is up to 10, but can be summarized into four big states: load, start, mount and unload. The update involved state is a special state in MOUNTED state. Sounds like deja vu at this point. Remember when we in the registration application has a micro load application functions as a registered applicationOrLoadingFn registerApplication parameters: (a) = > < XSL: | Promise > incoming. The loading function returns an object with three functions: bootstrap, mount, and unmount. Yes, the four big states in the flowchart correspond to the above four functions. Below, we take a look at how single-SPA controls the state of a microapplication, using the loading phase as an example. As for the other state changes, they are not very different from the state changes during loading and will not be described in this article. Remember from our previous article that there was a line of code for registering a microapplication:

apps.push(
    assign(
      {
        loadErrorTime: null.status: NOT_LOADED,
        parcels: {},
        devtools: {
          overlays: {
            options: {},
            selectors: [],
          },
        },
      },
      registration
    )
  );
Copy the code

That’s right, so the initial state of our registered microapp is NOT_LOADED. During the loading phase, a toLoadPromise function is involved, with the following code:

Note: When toLoadPromise is called is not discussed in this article, but will be discussed later when reroute is used to explain some of the main logic of the reroute function.

export function toLoadPromise(app) {
  return Promise.resolve().then(() = > {
    // A lot of code is omitted here...
    app.status = LOADING_SOURCE_CODE;
    // A lot of code is omitted here...
    return (app.loadPromise = Promise.resolve()
      .then(() = > {
        // A lot of code is omitted here...
        return loadPromise.then((val) = > {
          // A lot of code is omitted here...
          app.status = NOT_BOOTSTRAPPED;
          // A lot of code is omitted here...
          return app;
        });
      })
      .catch((err) = > {
        // A lot of code is omitted here
        if (isUserErr) {
          // This is the error-related state not covered in this article
          newStatus = SKIP_BECAUSE_BROKEN;
        } else {
          // This is the error-related state not covered in this article
          newStatus = LOAD_ERROR;
          // A lot of code is omitted here
        }
        // A lot of code is omitted here
        return app;
      }));
  });
Copy the code

As you can see from the above code, the so-called state change is to change the value of the status property on the app object representing the micro-application at different stages.

After reading the article feel the harvest of friends hard to point a praise, increase digging force value, I hope this year can become the digging gold outstanding author.

Welcome to follow my wechat official account: Yang Yitao, you can get the latest news.