Good happy holiday is over cry, come on the car to learn a wave of micro front-end buzz ~

If you want to see Module Federation analysis, I will drop an article juejin.cn/post/694979…

Of course, our correct operation is:

Wrong, again: 👇 start text

Single – What the spa did

Let’s take a look at what single-SPA has done

First think about if you achieve a routing change can switch sub-application system need to do what (my habit, first think about how to do it, and then go to the source code to find their own ideas), is roughly listening to the change of URL, switch our sub-application, how to obtain sub-application? () => import() or fetch resources can be inserted under our corresponding DOM and removed when the application is uninstalled.

Rather, the challenge is to manage the sequence of resource loading, application loading, and unloading, which is the life cycle.

So look at the source code, introduce a few core methods and ideas

After execution, the configuration and initialization status of the child application are stored. The third parameter is the activation state, which can be simply interpreted as being visible on the interface.

Life cycle: The subsystem must export bootstrap, mount, and unmount life cycle functions, and unload is optional. There are the following states

  • startTo start, we have to reroute in addition to indicating that the application has started.
  • rerouteExecution timing: Either manually,registerAppandstart, or in the route switchover, judge the current state, the life cycle function.
    • If you haven’t alreadystartThe first time, the state will be activatedappsperformloadApps, the loadjs entryAfter that, the status is changedNOT_BOOTSTRAPPED.
    • ifstartThat will beappstoUnmontThe uninstall willapptoloadLoad and mount.LOADING_SOURCE_CODE -> NOT_BOOTSTRAPPEDTo get the lifecycle methods exposed by the user, save them, and execute the bootstrap lifecycle.BOOTSTRAPPING -> NOT_MOUNTED, the initialization is complete. The next step is to mount, modify the state and execute the mount lifecycle,MOUNTING -> MOUNTED, the mounting is complete. forinactive, that is, routes do not match, indicating that this time should be unloaded, run the UNmout life cycle,UNMOUNTING -> NOT_MOUNTED.

For a single page application like react-Router, however, you’ll also need to use the native history.pushState history.replacestate implementation, so we’ve enhanced both methods so that they call reroute in addition to the native functionality. I’ve closed the loop.

Various states:

  • NOT_LOADED: The app has not been loaded or the app has been removed;
  • LOADING_SOURCE_CODE: indicates that the child application source code is being loaded.
  • NOT_BOOTSTRAPPEDFinish: performapp.loadApp, that is, the state after the child application is loaded
  • BOOTSTRAPPING: : Initializing the app and executing the bootstrap lifecycle;
  • NOT_MOUNTED: Initialization is complete. Bootstrap or unmout execution is complete.
  • MOUNTING: Mounting is in progress. Run mount.
  • MOUNTEDAfter the app is mounted, render can be executed;
  • UNMOUNTING: The unmout life cycle is executed and changes to NOT_MOUNTED.
  • UNLOADING: Remove the application, the application no longer has the life cycle, but is not really removed, later activation does not need to re-download resources, just make some changes in the state;
  • SKIP_BECAUSE_BROKEN: Loading failed

Improved window.history.pushState, replaceState, reroute in addition to native.

Single-spa events use the microtask return promise.resolve ().then(() => actions()), which does not affect the main task and does not interrupt the main task.

Single-spa still needs a lot of work

Defects caused by JS Entry

Remember that the second parameter to registerApplication, which is the load resource entry, is going to be executed in loadApps, and in our Webpack-packed project we’re going to unpack it with split chunk, But single Entry had to give up split chunk; Sub-applications have no priority, entries are loaded together, and there is a limit on the number of sub-applications that can be concurrent.

How to communicate

Although the split system should avoid communication, the communication between systems is still inevitable. User information, such as shared information, is modified in A and needs to be synchronized in B, which requires user-defined event communication. A creates custom events and registers custom event listeners in Window. Public data is stored to localstorage. Not very friendly management.

Change the prefix of the runtime subsystem

If you want to use the systemjs-webpack-interop, you can match the configuration file according to the project name. If you want to use the systemjs-webpack-interop, you can match the configuration file according to the project name. Modify \_\_webpack_public_path\_\_ (webpack is written in global, you can click \_\_webpack_require\_\_) to configure as shown in figure

CSS isolation, JS isolation, and DOM isolation

Only for style isolation, you can scope the body to isolate the global style, and remove the scope when unloading, but for jQ items, the selection of selectors will still be confused. Window methods, timers, global variables, etc. Code constraints are not enough.

Too lazy to change then go straight to Qiankun

Most of the above problems can be solved by plug-ins, coding constraints, etc. But it is inevitable to use the need to double encapsulation, no energy to repeat the wheel will take a look at qiankun source code.

What qiankun has done for us

Take a look at the official website

If single-SPA is Lego, need combination to use, then Qiankun is a accelerator can run 🚘. The effect is that I can load as needed, I have the order of application pull, variable pollution and style pollution are resolved, and I can run my application in just a few lines

I will not allow you not to know how he works

Start with a few questions, what is happy planet ~ now I take you to explore ~

Public-path addresses sub-application resource issues at run time

After reading the document, I began to suspect two possibilities. One is to modify \_\_webpack_require\_\_. E, and change the fetch resource path according to the child application name when loading the file. The other is to modify the run-time global variable \_\_webpack_public_path\_\_ to see what’s going on.

Let’s take a look at using

  1. Add public-path.js to SRC directory:

    if (window.__POWERED_BY_QIANKUN__) {
      __webpack_public_path__ = window.__INJECTED_PUBLIC_PATH_BY_QIANKUN__;
    }
    Copy the code
  2. The entry file main.js is modified.

  import './public-path';
Copy the code

When you see the use, you understand the use of modification__webpack_public_path__This code is imported into the entry file,__webpack_public_path__Be modified into__INJECTED_PUBLIC_PATH_BY_QIANKUN__.You can see that this variable is assigned in the lifecycle hook,beforeloadInject variables before the loader applies,beformountThis cycle may be executed multiple times. If it has been mounted before, it will be executed. If it has not been mounted, it has beenbeforeloadHas been injected into),beforeUnmountDelete variables before the child application is uninstalled.publicPathIt’s passed in by this method. The default is'/'To see who called itgetAddOnWhat’s going in. Tracing the actual incomingpublicpathThe place to post paragraph deleted version.

export async function loadApp<T extends ObjectType> (app: LoadableApp
       
        , configuration: FrameworkConfiguration = {}, lifeCycles? : FrameworkLifeCycles
        
         ,
        ) :Promise<ParcelConfigObjectGetter> {
  const { entry, name: appName } = app; .// We return publicPath. ImportEntry resolves the entry and returns the resolved object
    const { template, execScripts, assetPublicPath } = awaitimportEntry(entry, importEntryOpts); . omit// getAddon is actually an empty method. GetAddOns calls it
    getAddOns(global, assetPublicPath);
  }
Copy the code

I’m curious to see thisentryWhat is it? Grab itentryThe type of tsLooking at the name, I figured it wasregisterMicroAppsAppconfig entry, so the problem is, I pass in an address you are how to parse it. I changed my entry and spelled it out in the back. It was normal to pull the script from the HTML, but I couldn’t find the dynamic images and chunks.The script tag is the absolute path, for example, our path ishttp://localhost:7777/subapp/sub-vue/dacongming/Even if the spelling is wrong and the domain name and port are correct, the document for the single-page application is still there. The absolute path is replaced by domain name + port + path, so there is no problem. The problem with this is that we dynamically insert our images, chunks, etc. Webpack has global variables when packaged__webpack_public_path__When we pull the entry of the child application under the main application, if we do not change__webpack_public_path__, will not get the correct resource, so need to replace this variable, code typing log also confirm, the output of assetPath is the entry passed in, if the wrong match then naturally can not get the resource.

But what if registerMicroApps enters the AppConfig entry that does not match the publicPath of my HTML resource? If my HTML resource entry is not the root path or hash mode, then it will not be used as the resource address. This needs to be modified, don’t panic, start in support of passing parameters to modify, configure getPublicPath, so you can get the resources smoothly. (Mention a mouth this method is called get because it is used for Qiankun, reasonable)

start({
// This has the highest priority and will kill your entry. Pass in the method to determine whether the child application returns a different public_path
  getPublicPath: () = > 'http://localhost:7777'
})
// Webpack output parameter configuration
output: {
  ..omit
  assetModuleFilename: 'static/media/[name].[hash:8][ext]'.filename: 'static/js/[name].[contenthash:8].js'.chunkFilename: 'static/js/[name].[contenthash:8].chunk.js',},Copy the code

The sequential loading of resources

Start function was introduced into the first parameter can configure load strategy true is to load the first hit, other resources preload, can understand into lazy loading The above analysis of the performance of the single – spa know, resources will be Promise. All loaded together, how to realize the order

export function registerMicroApps<T extends ObjectType> (
  apps: Array<RegistrableApp<T>>, lifeCycles? : FrameworkLifeCycles<T>,) {
  // Each app only needs to be registered once
  const unregisteredApps = apps.filter((app) = >! microApps.some((registeredApp) = > registeredApp.name === app.name));

  microApps = [...microApps, ...unregisteredApps];
RegisterApplication = registerApplication = registerApplication = registerApplication
  unregisteredApps.forEach((app) = > {
    const{ name, activeRule, loader = noop, props, ... appConfig } = app; registerApplication({ name,app: async () => {
        loader(true);
        // This is the first time that I have been able to do this
        await frameworkStartedDefer.promise;
        // Highlight 2
        const{ mount, ... otherMicroAppConfigs } = (awaitloadApp({ name, props, ... appConfig }, frameworkConfiguration, lifeCycles) )();return {
          mount: [async () => loader(true), ...toArray(mount), async () => loader(false)],
          ...otherMicroAppConfigs,
        };
      },
      activeWhen: activeRule,
      customProps: props,
    });
  });
}
Copy the code
  • Highlight 1 : awaitForget about the backawaitWhat? What if Iawaitnew Promise(res => window.res = res)In this way, the resource loading of single-SPA was not stopped by me, only the follow-upres()Only then can we continue. In QiankunstartMethodreslove.
  • Highlight 2It looks like Universe is tuning his loadapp to pull resources. Let’s go ahead and seestartmethods
export function start(opts: FrameworkConfiguration = {}) {
 frameworkConfiguration = { prefetch: true.singular: true.sandbox: true. opts }const{ prefetch, sandbox, singular, urlRerouteOnly, ... importEntryOpts } = frameworkConfigurationif (prefetch) {
  doPrefetchStrategy(microApps, prefetch, importEntryOpts)
 }

 / /... Omit before sandbox

 startSingleSpa({ urlRerouteOnly })
 frameworkStartedDefer.resolve() // This is where the above defer Promise res will be, after which you can start loading the resource.
}
Copy the code

DoPrefetchStrategy preloading strategy, if prefetchStrategy is true on the preload, calls the prefetchAfterFirstMounted method, then have a look at the preload qiankun how to do

function prefetchAfterFirstMounted(apps: AppMetadata[], opts? : ImportEntryOpts) :void {
 window.addEventListener('single-spa:first-mount'.function listener() {
  const notLoadedApps = apps.filter((app) = > getAppStatus(app.name) === NOT_LOADED)
  notLoadedApps.forEach(({ entry }) = > prefetch(entry, opts))
  window.removeEventListener('single-spa:first-mount', listener)
 })
}
Copy the code

Single-spa dispatches the custom method single-spa:first-mount when first applied,

PrefetchAfterFirstMounted method to monitor the single – spa: first – mount, at the time of the first application of mount trigger, to determine whether state did not load resources namely NOT_LOADED, Refetch notLoadedApps (like Appstoload).

Prefetch uses the requesTidlecallback method, which lazily loads the resource. If this method is not available, it is simulated with setTimeout. (RequesTidlecallback makes the callback execute after rendering is complete. Requestidlecallback passes getExternalStyleSheets and getExternalScripts that match the script tag. Fetch pulls the code string, saves the result, and executes it directly in loadApp.

Ps: If single-SPA does not push the resource of the matched route into appstoload, it is possible to control the resource without using single-SPA’s load. Determine whether active will resolve active alone. The following post is their own ideas >-<

class Defer {
 constructor() {
  this.promise = new Promise((res, rej) = > {
   this.resolve = res
   this.reject = rej
  })
 }
}

function register(entry, opts) {
 const defer = new Defer()
 const app = {
  name: ' '.loadApp() {
   await defer.promise()
   fetch(entry, opts)
  },
  defer: defer
 }
 apps.push(app)
}
function judgeRoute() {
 return apps.filter((i) = > i.route === location.path)
}
function start() {
 const apps = judgeRoute()
 apps.forEach((i) = > i.defer.resolve())
 singleSpa.start()
}
Copy the code

What a sandbox

In loadApp, where the actual resource scripts are executed, a sandbox is created to implement JS isolation.

ProxySandbox is used in single instance (only one micro application can be mounted at the same time) and in multi-instance.

SingularProxySandbox modifies the new property in the real window. If the property does not exist in the current window, record it with addedPropsMap. If the property exists in the current window object and is not recorded in modifiedMap, Use modifiedMap to record the initial value of the property; Regardless of whether the record is recorded to currentUpdatedPropsValueMap, how every time the sandbox is unloaded, had some use modifiedMap to restore on the window, the window had no property, The value of the key of the addedPropsMap is set to undefined, which is deleted; Next time sandbox activation, reoccupy currentUpdatedPropsValueMap give back, if there is multiple applications at the same time is wrong.

ProxySandbox proxySandbox proxySandbox proxySandbox proxySandbox proxySandbox proxySandbox proxySandbox proxySandbox proxySandbox At value, if not

If fakeWindow has this property, it takes it from FakeWindow, if it doesn’t, it takes it from true Window, and when you set it, you set it directly to FakeWindow. Fakewindow creates a new object and copies the properties of the window that can be redefined.

let activeSandboxCount = 0
class ProxySandbox {
 active() {
  this.sandboxRunning = true
 }
 inactive() {
  this.sandboxRunning = false
 }
 constructor() {
  const rawWindow = window
  const fakeWindow = {}
  const proxy = new Proxy(fakeWindow, {
   set: (target, prop, value) = > {
    if (this.sandboxRunning) {
     target[prop] = value
     return true}},get: (target, prop) = > {
    let value = prop in target ? target[prop] : rawWindow[prop]
    return value
   }
  })
  this.proxy = proxy
 }
}
Copy the code

hijacked

The createSandboxContainer create sandbox method returns the current sandbox instance, along with the method mount and unmount that will be executed along with the life cycle of the child application. After the sandbox is started, it starts to hijack all kinds of global monitors. Focus on analyzing patchAtMounting.

return {
 instance: sandbox,

 /** * The sandbox may be mounted from the bootstrap state, or it may be mounted from the unmount state
 async mount() {
  / * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- because there are context dependent (window), the following code execution order cannot be changed -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * /

  / * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - 1. Start/resume sandbox -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * /
  sandbox.active()

  const sideEffectsRebuildersAtBootstrapping = sideEffectsRebuilders.slice(0, bootstrappingFreers.length)
  const sideEffectsRebuildersAtMounting = sideEffectsRebuilders.slice(bootstrappingFreers.length)

  // must rebuild the side effects which added at bootstrapping firstly to recovery to nature state
  if (sideEffectsRebuildersAtBootstrapping.length) {
   sideEffectsRebuildersAtBootstrapping.forEach((rebuild) = > rebuild())
  }

  / * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 2. Open the global variable patch -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * /
  // The render sandbox starts to hijack all global listeners. Try not to have side effects such as event listeners/timers during the application initialization phase
  // After the sandbox starts to hijack all kinds of global monitor
  mountingFreers = patchAtMounting(appName, elementGetter, sandbox, scopedCSS, excludeAssetFilter)

  / * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 3. The side effects of some reset initialization time -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * /
  // The presence of a rebuilder indicates that some side effects need to be rebuilt
  if (sideEffectsRebuildersAtMounting.length) {
   sideEffectsRebuildersAtMounting.forEach((rebuild) = > rebuild())
  }

  // clean up rebuilders
  sideEffectsRebuilders = []
 },

 /** * restore the global state to the state before the application was loaded */
 async unmount() {
  // record the rebuilders of window side effects (event listeners or timers)
  // note that the frees of mounting phase are one-off as it will be re-init at next mounting
  sideEffectsRebuilders = [...bootstrappingFreers, ...mountingFreers].map((free) = > free())

  sandbox.inactive()
 }
}
Copy the code

Methods that may contaminate the whole world, such as timers, methods on Windows, dynamic stylesheets, and JS, will be hijacked.

const basePatchers = [
 () = > patchInterval(sandbox.proxy), // Timer hijacking
 () = > patchWindowListener(sandbox.proxy), // Window event listener hijack
 () = > patchHistoryListener() / / the history
]
const patchersInSandbox = {
 [SandBoxType.LegacyProxy]: [...basePatchers, () = > patchLooseSandbox(appName, elementGetter, sandbox.proxy, true, scopedCSS, excludeAssetFilter)],
 [SandBoxType.Proxy]: [...basePatchers, () = > patchStrictSandbox(appName, elementGetter, sandbox.proxy, true, scopedCSS, excludeAssetFilter)],
 [SandBoxType.Snapshot]: [...basePatchers, () = > patchLooseSandbox(appName, elementGetter, sandbox.proxy, true, scopedCSS, excludeAssetFilter)]
}
Copy the code

The idea of timer hijacking is to override the setInterval method to collect the timer ID of the current application window and clear the timer when the current application is uninstalled. The same idea applies to the window listening method. When executing, specify the window of the proxy.

Stylesheets are harder to deal with, as styles are removed when the DOM is unmounted, and CSS that are not dynamically inserted need to be collected when they are to be released and rebuilt on the next mount. Dynamically inserted stylesheets are intercepted and inserted into their own scope, head.

Interesting for style sheet reconstruction, if HTMLStyleElement is not inserted into the document, it cannot read the sheet and returns null. If the link is inserted into the document, the cssRule of the link cannot be read directly, the error will be reported, you need to convert the link to style.

fetch(temp1.sheet.href)
 .then((res) = > res.text())
 .then((res) = > {
  const styleElement = document.createElement('style')
  //console.log(res)
  styleElement.appendChild(document.createTextNode(res))
  document.body.appendChild(styleElement)
  console.log(styleElement.sheet.cssRules) // Get CSS rules to rebuild
 })
Copy the code

For js inserted into the body that will be intercepted, use fetch to specify proxy Window. This intercepts the appendChild method and also handles removeChild. For example, React. CreatePortal creates a style that is inserted into the child div container, but when uninstalled React will look for it in the body. So you also need to handle removeChild, make a judgment on the child container, and remove the style.

conclusion

Apart from the fact that I had to find multiple HTML hosting resources to do application splitting, the cost was very low and a good generalisation solution.

O (^▽^) commercial commercial commercial commercial claw claw ღ(´ · ᴗ · ‘)