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