Simple use case for single-SPA
<! DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, Initial - scale = 1.0 "> < title > Document < / title > < / head > < body > < a href =" # / a > a application < / a > < a href = "# / b > b application < / a > < script SRC = "https://cdn.bootcdn.net/ajax/libs/single-spa/5.9.3/umd/single-spa.min.js" > < / script > < script > let {registerApplication, start} = singleSpa; // Let app1 = {boostrap: [async () = > {the console. The log (' app1 start - 1)}, async () = > {the console. The log (' app1 start - 2 ')}], mount: async () => { console.log('app1 mount') }, unmount: async () => { console.log('app1 unmount') }, }; Let app2 = {boostrap: [async () => {console.log('app2 boot-1 ')}, async () => {console.log('app2 boot-2 ')}], mount: async () => { console.log('app2 mount') }, unmount: async () => { console.log('app2 unmount') }, } const customProps = {name: 'wq'} registerApplication( 'app1', async () => app1, location => location.hash == '#/a', // customProps // customProps), registerApplication('app2', async () => app2, location => location.hash == '#/b', // After the path matches, the application customProps will be loaded. </script> </body> </html>Copy the code
You need to register the child application in the parent application and load the child application when the path matches. The child application exposes three hook functions: boostrap,mount, and unmount.
Single-spa source code analysis
The state machine
Single-spa loads, mounts, unloads and other operations based on the state of the child application. After the corresponding operation, the state needs to be changed and the state flow is carried out continuously.
Let’s start with a state flow diagram
At different stages of a subapplication, there are different states.
RegisterApplication method
Call this method to register the application, that is, save the application
const apps = []; // This is used to store all applications
function registerApplication(appName, loadApp, activeWhen, customProps) {
// Register child applications
const registeraction = {
name: appName,
loadApp,
activeWhen,
customProps,
status: NOT_LOADED
}
// Save the app to the array, then you can filter the array to load, uninstall or mount the app
apps.push(registeraction);
// Load the application. After the application is registered, load the application
// Switch routes later, to do this again, the core of single-SPA
reroute();
}
Copy the code
The Reroute method is at the heart of single-SPA. Take a look at how the Reoute method is implemented:
function reroute() {
// First, we need to know which applications need to be loaded, mounted, and unmounted
const {appsToLoad, appsToMount, appsToUnmount} = getAppChanges();
// Load the app after confirming the app to be loaded
return loadApps()
function loadApps() {
const loadPromises = appsToLoad.map(toLoadPromise);
return Promise.all(loadPromises); }}function toLoadPromise() {
// Return a Promise
return Promise.resolve().then(() = > {
if(app.status ! == NOT_LOADED) {// Only if the application is NOT_LOADED
return app;
}
return app.loadApp().then((val) = > {
// After the application is loaded
let { boostrap, mount, unmount } = val;
app.status = NOT_BOOTSTRAPPED; // Change the application state
// Get the application hook method, access protocol
// Because the hook function can be an array, it needs to be flattened
app.boostrap = flattenFnArray(boostrap);
app.mount = flattenFnArray(mount);
app.unmount = flattenFnArray(unmount);
returnapp; })})}function flattenFnArray() {
fns = Array.isArray(fns) ? fns : [fns]
// The promises in the array need to be called sequentially
return function(customProps) {
// asynchronous serial
return fns.reduce((resultPromise, fn) = > resultPromise.then(() = > fn(customProps)), Promise.resolve())
}
}
function shouldBeActive(app) {
// Determine whether the application should be activated
return app.activeWhen(window.location) // If the route matches, it needs to be activated
}
function getAppChanges() {
const appsToLoad = [];
const appsToMount = [];
const appsToUnmount = [];
// Apps is where all child applications are stored in the registerApplication
apps.forEach(app= > {
const appShouldBeActive = shouldBeActive(app);
switch(app.status) {
case NOT_LOADED: // Not loaded, need to be loaded
case LOADING_SOURCE_CODE:
if(appShouldBeActive) {
appsToLoad.push(app);
}
break;
case NOT_BOOTSTRAPPED: // Not started, not mounted, need to mount
case NOT_MOUNTED:
if(appShouldBeActive) {
appsToMount.push(app);
}
break;
case MOUNTED:
if(! appShouldBeActive) {// Paths do not match
appsToUnmount.push(app); // Mounting, but the path does not match, need to unmount
}
break;
default:
break; }});return {appsToLoad, appsToMount, appsToUnmount}
}
Copy the code
Summary: The registerApplication method simply saves the application and preloads the activated child application
The start method
// Start the main application
let start = false;
function start() {
start = true;
reroute();
}
Copy the code
The start method gets the loaded application and executes the corresponding lifecycle hooks
Return to the Reroute method
function reroute() {
// First, we need to know which applications need to be loaded, mounted, and unmounted
const {appsToLoad, appsToMount, appsToUnmount} = getAppChanges();
// During startup, the child application's lifecycle hooks are executed
if(start) {
return perfromAppChanges()
}
// Load the app after confirming the app to be loaded
return loadApps()
function loadApps() {
const loadPromises = appsToLoad.map(toLoadPromise);
return Promise.all(loadPromises);
}
function perfromAppChanges() {
// Call bootrap, mount and unmount
ToLoadPromise state LOADING_SOURCE_CODE to avoid repeated loading
// The tryBootstrapAndMount method executes the hook function
appsToLoad.map(app= > toLoadPromise(app).then(app, tryBootstrapAndMount(app)))
}
}
function toLoadPromise(app) {
return Promise.resolve().then(() = > {
// Get the application hook method, access protocol
if(app.status ! == NOT_LOADED) {// It only needs to be loaded if it is NOT_LOADED
return app;
}
app.status = LOADING_SOURCE_CODE;
return app.loadApp().then((val) = > {
let { boostrap, mount, unmount } = val;
app.status = NOT_BOOTSTRAPPED;
app.boostrap = flattenFnArray(boostrap);
app.mount = flattenFnArray(mount);
app.unmount = flattenFnArray(unmount);
returnapp; })})}function tryBootstrapAndMount(app) {
return Promise.resolve().then(() = > {
if(shouldBeActive(app)) {
// Perform bootrap first and then mount
return toBoostrapPromise(app).then(toMountPromise)
}
})
}
function toBoostrapPromise() {
return Promise.resolve().then(() = > {
if(app.status ! == NOT_BOOTSTRAPPED) {return app;
}
app.status = BOOSTRAPPING; // Starting
// Execute the hook function
return app.boostrap(app.customProps).then(() = > {
app.status = NOT_MOUNTED;
returnapp; })})}function toMountPromise(app) {
// Mount the application
return Promise.resolve().then(() = > {
if(app.status ! == NOT_MOUNTED) {return app;
}
return app.mount(app.customProps).then(() = > {
app.status = MOUNTED;
returnapp; })})}Copy the code
The start() method is done
During route switchover, you must be able to mount and uninstall sub-applications. The Hashchange event is emitted when the fragment identifier changes (the fragment identifier is the # in the URL and the part after it), and the popState event is emitted when history.back(),history.go() are executed (note: History.pushstate (), history.replacestate () do not trigger popState, need to trigger manually)
In addition, a routing system may exist in a child application. Ensure that the parent application is loaded first and then the child application. So you need to hijack window.addEventListener and save the popState,hashchange processing event first.
function urlRoute() {
reroute();
}
window.addEventListener('hashchange', urlRoute)
window.addEventListener('popstate', urlRoute)
const routerEventListeningTo = ['hashchange'.'popstate'];
const capturedEventListener = {
hashchange: [].// When to call? Called after the parent application loads the child application
popstate: []}const originalAddEventListener = window.addEventListener;
const originalRemoveListener = window.removeEventListener;
window.addEventListener = function (eventName, fn) {
if(routerEventListeningTo.includes(eventName) && ! capturedEventListener[eventName].some(l= > l === fn)) {
// Avoid repeated listening
return capturedEventListener[eventName].push(fn);
}
return originalAddEventListener.apply(this.arguments)}window.removeEventListener = function(eventName, fn) {
if(routerEventListeningTo.includes(eventName)) {
return capturedEventListener[eventName] = capturedEventListener[eventName].filter(l= >l ! == fn); }return originalRemoveListener.apply(this.arguments)}// If history.pushState is used, the page can be jumped, but popState is not triggered
// PopState can be triggered when historyApi calls are resolved
history.pushState = function() {
// Trigger the popState event
window.dispatchEvent(new PopStateEvent('popstate'))}Copy the code
Going back to reroute, reroute also unloads the deactivated child application during route switching
function reroute() {
// First, we need to know which applications need to be loaded, mounted, and unmounted
const {appsToLoad, appsToMount, appsToUnmount} = getAppChanges();
// During startup, the child application's lifecycle hooks are executed
if(start) {
return perfromAppChanges()
}
// Load the app after confirming the app to be loaded
return loadApps()
function loadApps() {
const loadPromises = appsToLoad.map(toLoadPromise);
return Promise.all(loadPromises);
}
function perfromAppChanges() {
// Call bootrap, mount and unmount
// When the application starts, unmount what is not needed and mount what is needed
const unmountPromises = Promise.all(appsToUnmount.map(toUnmountPromise)); // Uninstall the application first
// The tryBootstrapAndMount method executes the hook function
appsToLoad.map(app= > toLoadPromise(app).then(app, tryBootstrapAndMount(app, unmountPromises)))
// Start () may be called asynchronously. If the load is completed and the mount is in the stage, mount it directly
appsToMount.map((app) = > tryBootstrapAndMount(app, unmountPromises))
}
}
function toUnmountPromise() {
return Promise.resolve().then(() = > {
// Exit if it is not mounted
if(app.status ! == MOUNTED) {return app;
}
app.status = UNMOUNTING; // Uninstalling
return app.unmount(app.customProps).then(() = > {
app.status = NOT_MOUNTED;
returnapp; })})}//tryBootstrapAndMount also needs to be modified so that new applications can be mounted after the old child applications are unmounted
function tryBootstrapAndMount(app, unmountPromises) {
return Promise.resolve().then(() = > {
if(shouldBeActive(app)) {
// Perform bootrap first and then mount
return toBoostrapPromise(app).then((app) = > {
return unmountPromises.then(() = > {
// Execute the saved event handler
capturedEventListener.hashchange.forEach(fn= > fn())
capturedEventListener.popstate.forEach(fn= > fn())
return toMountPromise(app)
})
})
}
})
}
Copy the code
At this point, the general working principle of single-SPA is complete, and promise knowledge is used extensively in the loading and unloading process