“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 thisreroute The 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