A link to the
This paper is a recent analysis of single-SPA, and all the articles are as follows:
- An in-depth analysis of single-SPA — navigation events and Reroute
- In-depth analysis of Single-SPA — Startup and application management
- An in-depth look at single-SPA — the event mechanism
- Additional in-depth analysis of module mechanisms, life cycles, and microfront-end types is ongoing
Open single-Spa on Github and the About section reads:
The router for easy microfrontends.
Sing-spa is a route-driven micro front-end framework, so we will start with the Router and then gradually analyze the routing mechanism inside single-SPA.
Front-end knowledge – About front-end routing
Whether we use React, Vue or Angular to develop SPA applications, Router is indispensable. In the browser environment, common routers fall into two categories:
- Browser Router
- Hash Router
Browser Router
In HTML5, the DOM’s window object provides access to the browser’s session history through history, allowing for forward and backward jumps in the user’s browsing history. We can:
// Jump backwards in history
window.history.back()
// Jump forward in history
window.history.forward()
// Or use go to load a specific interface in the session history
window.history.go(-1) // Same as back
window.history.go(1) // Equivalent to forward
Copy the code
In addition, the history API provides pushState/replaceState/popState event, used to add and modify items in a historical record.
, which uses these events to keep the UI and URL consistent.
window.onpopstate = function(event) {
// ...
}
history.pushState({page: 1}, "title 1"."? page=1")
history.pushState({page: 2}, "title 2"."? page=2")
history.replaceState({page: 3}, "title 3"."? page=3")
Copy the code
Hash Router
The Hash Router listens for hashchange events to keep the UI and URL consistent based on changes in location. Hash. It is also a Router that we often encounter and has good browser compatibility.
window.onhashchange = function (event) {
// ...
}
window.addEventListener('hashchange'.function (event) {
// ...
})
Copy the code
For more information on these types of events, see the History API and Window: HashChange Event.
Routing mechanisms in single-SPA
Navigation-events
Single-spa implements application-level routing navigation and provides Browser Router and Hash Router support. The main implementation is as follows:
- Listening to the
hashchange
andpopstate
Event to implement reroute - rewrite
window.addEventListener
andwindow.removeEventListener
To realize the hijacking of custom events - right
pushState
andreplaceState
Event custom processing
The basic flow
forhashchange
andpopstate
Listening processing of
const capturedEventListeners = {
hashchange: [].popstate: [],};// Route events to listen on, i.e. Hashchange and popState
export const routingEventsListeningTo = ["hashchange"."popstate"];
// The implementation of the event listening call
export function callCapturedEventListeners(eventArguments) {
if (eventArguments) {
const eventType = eventArguments[0].type;
if (routingEventsListeningTo.indexOf(eventType) >= 0) {
capturedEventListeners[eventType].forEach((listener) = > {
try {
listener.apply(this, eventArguments);
} catch (e) {
setTimeout(() = > {
throwe; }); }}); }}}if (isInBrowser) {
// Register listening for hashchange and popState events
window.addEventListener("hashchange", urlReroute);
window.addEventListener("popstate", urlReroute);
// ...
}
Copy the code
rewritewindow.addEventListener
andwindow.removeEventListener
After registering for listening on hashchange and popState events, we override the addEventListener and removeEventListener methods as follows:
if (isInBrowser) {
// ...
// Override addEventListener and removeEventListener
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) {
if (typeof listenerFn === "function") {
if (routingEventsListeningTo.indexOf(eventName) >= 0) {
capturedEventListeners[eventName] = capturedEventListeners[
eventName
].filter((fn) = >fn ! == listenerFn);return; }}return originalRemoveEventListener.apply(this.arguments);
};
// ...
}
// urlReroute is simply called reroute to implement the corresponding route navigation operation
function urlReroute() {
reroute([], arguments);
}
Copy the code
forpushState
andreplaceState
Custom processing of
Single-spa implements a patchUpdateState method to add some custom logic to the pushState and replaceState events in window.history:
if (isInBrowser) {
// ...
// For pushState and replaceState events, add some custom logic via patchedUpdateState
window.history.pushState = patchedUpdateState(
window.history.pushState,
"pushState"
);
window.history.replaceState = patchedUpdateState(
window.history.replaceState,
"replaceState"
);
/ /...
}
Copy the code
In patchUpdateState, the following processing is mainly done:
- Determine whether urlRerouteOnly or the URL has changed
- Determine if single-SPA has been started
- If single-SPA has been started, a corresponding event will be sent
- If it is not started, it is triggered
reroute
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()) {
// Send the corresponding event
window.dispatchEvent(
createPopStateEvent(window.history.state, methodName)
);
} else {
/ / call reroutereroute([]); }}return result;
};
}
// cratePopStateEvent is used to create a PopStateEvent and add singleSpa and singleSpaTrigger identifiers
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
reroute
In the introduction of navigation-events, we can find:
- Have stated
callCapturedEventListeners
, but does not call; The actual call is inreroute.js
The trigger - in
patchUpdateState
Is triggered if single-SPA is not startedreroute
- for
hashchange
andpopstate
“, registered oneurlReroute
Method, which is also triggered herereroute
So let’s look at Reroute.
About the reroute
Reroute is the core method of single-SPA. The method updates the state of the microapplication, triggers the life cycle function of the microfront-end application, and issues a series of custom events.
trigger
- Manual invocation: Reroute execution is triggered when the micro front-end application is registered and the start method is called
- Automatic trigger: Listen for changes in route events in navigation-Events and automatically trigger reroute execution
The basic flow
- Determine appChangealtering, if it is true, route changes after reroute is executed are stored and processed after reroute is executed
- Use getAppChanges to get the status of each application in the app and classify it
- Determine if single-SPA has been started, and if so, call performAppChanges; Otherwise, call loadApps
export function reroute(pendingPromises = [], eventArguments) {
/* The variable appChangeconspicuous is used to determine whether the execution is ongoing, and its initial value is false; * If true, store the route changes since reroute started with peopleWaitingOnAppChange; And returns a Promise to wait until the reroute execution completes */
if (appChangeUnderway) {
return new Promise((resolve, reject) = > {
peopleWaitingOnAppChange.push({
resolve,
reject,
eventArguments,
});
});
}
// Use getAppChanges to get the microfront-end application status, which is divided into four categories
const {
appsToUnload,
appsToUnmount,
appsToLoad,
appsToMount,
} = getAppChanges();
let appsThatChanged,
navigationIsCanceled = false,
oldUrl = currentUrl,
newUrl = (currentUrl = window.location.href);
// Check if single-SPA has been started
if (isStarted()) {
// Change appChangeconspicuous to true, get the appThatChanged list, and execute performAppChanges
appChangeUnderway = true;
appsThatChanged = appsToUnload.concat(
appsToLoad,
appsToUnmount,
appsToMount
);
return performAppChanges();
} else {
// If single-spa is not started, loadApps is executed
appsThatChanged = appsToLoad;
returnloadApps(); }}Copy the code
Get the microfront-end application that needs to change — getAppChanges
In the previous code, we saw that the getAppChanges method was called in Reroute to get the microfront-end application with a state change. First we can look at the definition of application state:
// App statuses, defined in app.helpers.js, explains the state used in getAppChanges
// The initial state indicates that the resources of the micro front-end application are not loaded
export const NOT_LOADED = "NOT_LOADED";
// represents loading the source code for the microfront-end application
export const LOADING_SOURCE_CODE = "LOADING_SOURCE_CODE";
// the next state of NOT_LOADED, indicating that it is not initialized
export const NOT_BOOTSTRAPPED = "NOT_BOOTSTRAPPED";
export const BOOTSTRAPPING = "BOOTSTRAPPING";
// The next state of NOT_BOOTSTRAPPED, which means that the microfront-end application code is not executed/loaded on the interface
export const NOT_MOUNTED = "NOT_MOUNTED";
export const MOUNTING = "MOUNTING";
// Indicates that the micro front-end application code has been executed/loaded to the interface
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
GetAppChanges defines four categories of applications and classifies them accordingly:
- AppsToUnload: Applies to the
NOT_BOOTSTRAPPED
andNOT_MOUNTED
Status, and does not match the current URL - AppsToUnmount: Indicates the status
MOUNTED
Status, and does not match the current URL - AppsToLoad: Specifies the status
NOT_LOADED
andLOADING_SOURCE_CODE
Turntable, and the current URL matches the application - AppsToMount: As opposed to appsToUnload, for
NOT_BOOTSTRAPPED
andNOT_MOUNTED
State, and the application matches the current URL
The specific source code is as follows:
export function getAppChanges() {
const appsToUnload = [],
appsToUnmount = [],
appsToLoad = [],
appsToMount = [];
// We re-attempt to download applications in LOAD_ERROR after a timeout of 200 milliseconds
const currentTime = new Date().getTime();
apps.forEach((app) = > {
constappShouldBeActive = app.status ! == SKIP_BECAUSE_BROKEN && shouldBeActive(app);switch (app.status) {
case LOAD_ERROR:
if (appShouldBeActive && currentTime - app.loadErrorTime >= 200) {
appsToLoad.push(app);
}
break;
case NOT_LOADED:
case LOADING_SOURCE_CODE:
if (appShouldBeActive) {
appsToLoad.push(app);
}
break;
case NOT_BOOTSTRAPPED:
case NOT_MOUNTED:
if(! appShouldBeActive && getAppUnloadInfo(toName(app))) { appsToUnload.push(app); }else if (appShouldBeActive) {
appsToMount.push(app);
}
break;
case MOUNTED:
if(! appShouldBeActive) { appsToUnmount.push(app); }break;
// all other statuses are ignored}});return { appsToUnload, appsToUnmount, appsToLoad, appsToMount };
}
Copy the code
After capturing the microfront-end application with a state change, it is time to perform specific operations.
Perform the micro front-end application changes — performAppChanges
Execute changes mainly through CustomEvent for CustomEvent distribution and subsequent processing:
- Based on the number of appsThatChanged
single-spa:before-no-app-change
orsingle-spa:before-app-change
The event - distributed
single-spa:before-routing-event
The event - If navigation has been cancelled
- distributed
single-spa:before-mount-routing-event
The event - Call finishUpAndReturn
- Call naviagteToUrl to return the previous URL
- distributed
- Microfront-end applications of various states are processed
- Finally, finishUpAndReturn is called
function performAppChanges() {
return Promise.resolve().then(() = > {
// Send custom events based on the number of appsThatChanged
window.dispatchEvent(
new CustomEvent(
appsThatChanged.length === 0
? "single-spa:before-no-app-change"
: "single-spa:before-app-change",
getCustomEventDetail(true)));// Dispatch the single-spa:before-routing-event event
window.dispatchEvent(
new CustomEvent(
"single-spa:before-routing-event",
getCustomEventDetail(true, { cancelNavigation })
)
);
// Handle navigation cancellations
if (navigationIsCanceled) {
window.dispatchEvent(
new CustomEvent(
"single-spa:before-mount-routing-event",
getCustomEventDetail(true))); finishUpAndReturn(); navigateToUrl(oldUrl);return;
}
// Handle applications in various states
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);
unmountAllPromise.then(() = > {
window.dispatchEvent(
new CustomEvent(
"single-spa:before-mount-routing-event",
getCustomEventDetail(true))); });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(() = > {
callAllEventListeners();
return Promise.all(loadThenMountPromises.concat(mountPromises))
.catch((err) = > {
pendingPromises.forEach((promise) = > promise.reject(err));
throw err;
})
.then(finishUpAndReturn); // Finally call finishUpAndReturn
});
});
}
Copy the code
The handling of microfront-end applications in different states, as well as custom events, will be analyzed in a separate article later, which will not be expanded here.
The final processing — finishUpAndReturn
In the previous reroute process analysis, there were two other points:
- Appchangeconspicuous is recorded with an initial value of false and set to true after isStarted() === true
- According to appChangevariation === true, route changes after reroute are recorded in peopleWaitingOnAppChange for subsequent processing
FinishUpAndReturn acts as the end code to reroute and issues some custom end events, revalues appChangevariation, and handles the route events previously recorded in peopleWaitingOnAppChange. The return value is Mounted apps. The source code is as follows:
function finishUpAndReturn() {
// Obtain Mounted apps
const returnValue = getMountedApps();
pendingPromises.forEach((promise) = > promise.resolve(returnValue));
// Publish custom events
try {
const appChangeEventName =
appsThatChanged.length === 0
? "single-spa:no-app-change"
: "single-spa:app-change";
window.dispatchEvent(
new CustomEvent(appChangeEventName, getCustomEventDetail())
);
window.dispatchEvent(
new CustomEvent("single-spa:routing-event", getCustomEventDetail())
);
} catch (err) {
setTimeout(() = > {
throw err;
});
}
// Reset appChangealtering to false for subsequent calls to reroute
appChangeUnderway = false;
// Call reroute to process the previous record in peopleWaitingOnAppChange
if (peopleWaitingOnAppChange.length > 0) {
const nextPendingPromises = peopleWaitingOnAppChange;
peopleWaitingOnAppChange = [];
reroute(nextPendingPromises);
}
// Return the mounted apps obtained before
return returnValue;
}
Copy the code
References:
-
History API
-
Window: hashchange event
-
single-spa