This article is from OPPO Internet technology team, please note the author. At the same time, welcome to follow our official account: OPPO_tech, share with you OPPO cutting-edge Internet technology and activities.

A microfront end is a microservice that exists in a browser and is typically made up of many components that are rendered using frameworks like React, Vue, and Angular. Each microfront can be managed by a different team, with a choice of frameworks.

Each microfront has its own Git repository, package.json, and build tool configuration. Therefore, some megalithic applications can be separated into multiple independent modules and then combined together. Applications can be independently maintained and online without interference.

This paper introduces the principle of the micro front-end framework Qiankun and some practices of OPPO cloud in this way through some simplified codes.

Note: This article is used by defaultqiankunFramework, and used in this articleqiankunVersion is:2.0.9.

1. The former single-SPA of Qiankun

Qiankun is a micro-front-end implementation library based on Single-SPA. Before the birth of Qiankun, users usually used single-SPA to solve the problems of micro-front-end, so we will first learn about single-SPA.

Let’s start with an example and take a step-by-step look at what happens at each step.

import { registerApplication, start } from "single-spa";
registerApplication(
  "foo".() = > System.import("foo"),
  (location) = > location.pathname.startsWith("foo")); registerApplication({name: "bar".loadingFn: () = > import("bar.js"),
  activityFn: (location) = > location.pathname.startsWith("bar")}); start();Copy the code
  • AppName: string The name of the application will be registered and referenced in single-SPA and marked in development tools
  • LoadingFn: () => must be a load function that returns either an application or a Promise
  • ActivityFn: (location) => Boolean Method to determine whether the current application is active
  • customProps? : Object Optional Transfer of user-defined parameters

1.1 Metadata processing

First, single-SPA normalizes the above data and adds state, which is eventually converted to a metadata array. For example, the above data is converted to:

[{
  name: 'foo'.loadApp: () = > System.import('foo'),
  activeWhen: location= > location.pathname.startsWith('foo'),
  customProps: {},
  status: 'NOT_LOADED'}, {name: 'bar'.loadApp: () = > import('bar.js'),
  activeWhen: location= > location.pathname.startsWith('bar')
  customProps: {},
  status: 'NOT_LOADED'
}]
Copy the code

1.2 Route Hijacking

Single-spa internally hijacks the browser route, and all routing methods and routing events ensure that single-SPA is first entered for unified scheduling.

// We will trigger an app change for any routing events.
window.addEventListener("hashchange", urlReroute);
window.addEventListener("popstate", urlReroute);
// Monkeypatch addEventListener so that we can ensure correct timing
const originalAddEventListener = window.addEventListener;
window.addEventListener = function(eventName, fn) {
  if (typeof fn === "function") {
    if(["hashchange"."popstate"].indexOf(eventName) >= 0 &&
      !find(capturedEventListeners[eventName], (listener) = > listener === fn)
    ) {
      capturedEventListeners[eventName].push(fn);
      return; }}return originalAddEventListener.apply(this.arguments);
};
Copy the code
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) { urlReroute(createPopStateEvent(window.history.state, methodName)); }}; }window.history.pushState = patchedUpdateState(
  window.history.pushState,
  "pushState"
);
window.history.replaceState = patchedUpdateState(
  window.history.replaceState,
  "replaceState"
);
Copy the code

The above is a condensed version of the hijacking code, and you can see that all the hijacking points to an exit function, urlReroute.

1.3 urlReroute unified processing function

Each time a route changes, it enters the same function for processing:

let appChangeUnderway = false,
  peopleWaitingOnAppChange = [];
export async function reroute(pendingPromises = [], eventArguments) {
  // Separate applications into different arrays according to different conditions
  const {
    appsToUnload,
    appsToUnmount,
    appsToLoad,
    appsToMount,
  } = getAppChanges();

  // If a new route jump is made while the change is in progress, a queue is entered.
  if (appChangeUnderway) {
    return new Promise((resolve, reject) = > {
      peopleWaitingOnAppChange.push({ resolve, reject, eventArguments });
    });
  }
  // mark that the change is in progress,
  appChangeUnderway = true;

  await Promise.all(appsToUnmount.map(toUnmountPromise)); // Run unmount for the application to be unmounted
  await Promise.all(appsToUnload.map(toUnloadPromise)); // The application to be destroyed should be destroyed first
  await Promise.all(appsToLoad.map(toLoadPromise)); // Perform load first for the application to be loaded
  await Promise.all(appsToBootstrap.map(toBootstrapPromise)); // Perform bootstrap for the bootstrap application
  await Promise.all(appsMount.map(toMountPromise)); // Mount the application to be mounted

  appChangeUnderway = false;
  // If there are still route changes in the queued queue, a new reroute loop is performed
  reroute(peopleWaitingOnAppChange);
}
Copy the code

Now let’s see what the grouping function is doing.

1.4 getAppChanges Application group changes

Each route change is first grouped according to the activeRule rule of the application.

export function getAppChanges() {
  const appsToUnload = [],
    appsToUnmount = [],
    appsToLoad = [],
    appsToMount = [];
  apps.forEach((app) = > {
    constappShouldBeActive = app.status ! == SKIP_BECAUSE_BROKEN && shouldBeActive(app);switch (app.status) {
      case LOAD_ERROR:
      case NOT_LOADED:
        if (appShouldBeActive) appsToLoad.push(app);
      case NOT_BOOTSTRAPPED:
      case NOT_MOUNTED:
        if(! appShouldBeActive) { appsToUnload.push(app); }else if (appShouldBeActive) {
          appsToMount.push(app);
        }
      case MOUNTED:
        if (!appShouldBeActive) appsToUnmount.push(app);
    }
  });
  return { appsToUnload, appsToUnmount, appsToLoad, appsToMount };
}
Copy the code

1.5 Enumeration of status fields

Single-spa divides the status of applications

export const NOT_LOADED = "NOT_LOADED"; // Not yet loaded
export const LOADING_SOURCE_CODE = "LOADING_SOURCE_CODE"; // Load the source code
export const NOT_BOOTSTRAPPED = "NOT_BOOTSTRAPPED"; // Bootstrap is not yet loaded
export const BOOTSTRAPPING = "BOOTSTRAPPING"; / / the bootstrap
export const NOT_MOUNTED = "NOT_MOUNTED"; // The bootstrap is complete
export const MOUNTING = "MOUNTING"; / / in the mount
export const MOUNTED = "MOUNTED"; / / end of the mount
export const UPDATING = "UPDATING"; / / the updata
export const UNMOUNTING = "UNMOUNTING"; / / unmount to
export const UNLOADING = "UNLOADING"; / / unload
export const LOAD_ERROR = "LOAD_ERROR"; // Failed to load the source code
export const SKIP_BECAUSE_BROKEN = "SKIP_BECAUSE_BROKEN"; / / in the load, the bootstrap, mount and unmount to stage a script error
Copy the code

We can use official debugging tools during development to quickly see the status of each application after each route change:

Single-spa uses finite-state machine design ideas:

  • Things have multiple states, and can only be in one state at any time, not in multiple states;

  • An action can change the state of things. An action can be judged by conditions to change things to different states, but it cannot point to multiple states at the same time.

  • The total number of states is finite.

Other examples of finite state machines: Promises, traffic lights

1.6 Single-SPA event system

Browser-based native event system, no framework coupling, global out-of-box availability.

// Receive mode
window.addEventListener("single-spa:before-routing-event".(evt) = > {
  const {
    originalEvent,
    newAppStatuses,
    appsByNewStatus,
    totalAppChanges,
  } = evt.detail;
  console.log(
    "original event that triggered this single-spa event",
    originalEvent
  ); // PopStateEvent | HashChangeEvent | undefined
  console.log(
    "the new status for all applications after the reroute finishes",
    newAppStatuses
  ); // { app1: MOUNTED, app2: NOT_MOUNTED }
  console.log(
    "the applications that changed, grouped by their status",
    appsByNewStatus
  ); // { MOUNTED: ['app1'], NOT_MOUNTED: ['app2'] }
  console.log(
    "number of applications that changed status so far during this reroute",
    totalAppChanges
  ); / / 2
});
Copy the code

1.7 Highlights and disadvantages of single-SPA

Bright spot

  • All asynchronous programming, the load will need to provide to the user, the bootstrap, mount and unmount to promise should be used in asynchronous form processing, no matter synchronous and asynchronous can hold

  • By hijacking routes, you can determine whether an application needs to be switched each time a route changes, and then send the sub-application to respond to the route

  • The mount and unload functions of each application should be standardized, and no framework should be coupled. Sub-applications can be connected to the system as long as they implement corresponding interfaces

insufficient

  • The load method needs to know the entry file for the subproject

  • Integrating multiple application runtimes requires inter-project management of memory leaks and style contamination

  • There is no way to provide parent-child data communication

2. Qiankun appearance

In order to solve some shortcomings of single-SPA and retain the excellent concepts in single-SPA, Qiankun further expanded on the basis of single-SPA.

The following is the official capability diagram of Qiankun:

Let’s take a look at how Qiankun is used

import { registerMicroApps, start } from "qiankun";
registerMicroApps([
  {
    name: "react app".// app name registered
    entry: "//localhost:7100".container: "#yourContainer".activeRule: "/yourActiveRule"}, {name: "vue app".entry: { scripts: ["//localhost:7100/main.js"]},container: "#yourContainer2".activeRule: "/yourActiveRule2",}]); start();Copy the code

Is it a bit like single-SPA registration?

2.1 Transfer of registration information to Single-SPA

Inside Qiankun, users’ app registration information will be packaged and sent to Single-SPA

import { registerApplication } from "single-spa";
export function registerMicroApps(apps) {
  apps.forEach((app) = > {
    const{ name, activeRule, loader = noop, props, ... appConfig } = app; registerApplication({ name,app: async () => {
        loader(true);
        const{ mount, ... otherMicroAppConfigs } =awaitloadApp( { name, props, ... appConfig }, frameworkConfiguration );return {
          mount: [
            async () => loader(true),
            ...toArray(mount),
            async () => loader(false),],... otherMicroAppConfigs, }; },activeWhen: activeRule,
      customProps: props,
    });
  });
}
Copy the code

You can see that the mount and unmount functions are returned by loadApp.

2.2 Implementation of loadApp

export async function loadApp(app, configuration) {
  const { template, execScripts } = await importEntry(entry); // Access the HTML, JS, and CSS content of the application through the entry link of the application
  const sandboxInstance = createSandbox(); // Create a sandbox instance
  const global = sandboxInstance.proxy; // Get a sandbox global context
  const mountSandbox = sandboxInstance.mount;
  const unmountSandbox = sandboxInstance.unmount;
  // Executes the js code for the subproject in this sandbox global context
  const scriptExports = await execScripts(global);
  // Get the bootstrap/mount/unmount exported by the subproject
  const { bootstrap, mount, unmount, update } = getLifecyclesFromExports(
    scriptExports,
    appName,
    global
  );
  // Initialize the event module
  const {
    onGlobalStateChange,
    setGlobalState,
    offGlobalStateChange,
  } = getMicroAppStateActions();
  // The mount, unmount method passed to single-SPA is actually a function wrapped by Qiankun
  return {
    bootstrap,
    mount: async () => {
      awaitrender(template); // Render the template to the mount area
      mountSandbox(); // Mount the sandbox
      await mount({ setGlobalState, onGlobalStateChange }); // Call the mount function of the application
    },
    ummount: async() = > {await ummount(); // Call the applied ummount function
      unmountSandbox(); // Uninstall the sandbox
      offGlobalStateChange(); // Remove event listening
      render(null); // Clear the render area}}; }Copy the code

2.3 importEntry implementation

Take a look at the use of importEntry, which is a separate package import-HTml-entry that parses HTML content and returns HTML, CSS, and JS separated content.

For example, the entry HTML of a subapplication is as follows

<! DOCTYPEhtml>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Here is the title</title>
    <link rel="stylesheet" href="./css/admin.css" />
    <style>
      .div {
        color: red;
      }
    </style>
  </head>
  <boyd>
    <div id="wrap">
      <div id="app"></div>
    </div>
    <script src="/static/js/app.12345.js"></script>
    <script>
      console.log("1");
    </script>
  </boyd>
</html>
Copy the code

After being loaded into the page by Qiankun, the HTML structure was finally generated as follows:

<meta charset="utf-8" />
<title>Here is the title</title>
<link rel="stylesheet" href="./css/admin.css" />
<style>
  .div {
    color: red;
  }
</style>
<div id="wrap">
  <div id="app"></div>
</div>
<! -- script /static/js/app.12345.js replaced by import-html-entry -->
<! -- inline scripts replaced by import-html-entry -->
Copy the code

Take a look at what importEntry returns

export function importEntry(entry, opts = {}) {...// Parse HTML procedure ignored
  return {
    // The content of the pure DOM element
    template,
    // A method to fetch 
    getExternalScripts: () = > getExternalScripts(scripts, fetch),
    // a method that can receive a 
    getExternalStyleSheets: () = > getExternalStyleSheets(styles, fetch),
    // an execution function that receives the global context. This execution method emulates the logic of the browser executing a script when the application loads
    execScripts: (proxy) = >{}}}Copy the code

Looking at the implementation of getExternalScripts, which actually simulates the browser loading

// scripts is an array of urls for the 
      
        tag after parsing the HTML
      
export getExternalScripts(scripts, fetch = defaultFetch) {
  return Promise.all(scripts.map(script= > {
    return fetch(scriptUrl).then(response= > {
        returnresponse.text(); })); }}))Copy the code

Then look at the implementation of execScripts, which can execute all

export async execScripts(proxy) {
  // getExternalScripts above loads the contents of the 
      
        tag
      
  const scriptsTexts = await getExternalScripts(scripts)
  window.proxy = proxy;
  // Emulate the browser and execute the script in sequence
  for (let scriptsText of scriptsTexts) {
    // Adjust the sourceMap address, otherwise sourceMap is invalid
    const sourceUrl = '//# sourceURL=${scriptSrc}\n';
    // Use iife to replace proxy with window and eval to execute this script
    eval(`
      ;(function(window, self){
        ;${scriptText}
        ${sourceUrl}
      }).bind(window.proxy)(window.proxy, window.proxy);
    `;) }}Copy the code

2.4 Global variable contamination and memory leaks

Before we look at the sandbox feature, the sandbox is mainly used to solve the global variable pollution and memory leak problems of the program.

  • Global variable contamination: Multiple applications use a global variable of the same name, such as Vue.

  • Memory leak: MEMORY leak is the failure of a program to free memory that is no longer in use due to negligence or error. A memory leak is not the physical disappearance of memory, but rather the loss of control of memory before it is released due to a design error after an application allocates the memory.

    Common memory leak scenarios:

    1. Unexpected global variables

    2. Leak to the global closure

    3. DOM leak

    4. The timer

    5. EventListener

    6. Console. log (Development environment)

Let’s take a look at how Qiankun could solve this problem.

2.5 How to use the sandbox

Using the logic of loadApp above, this article discusses the LegacySandbox sandbox.

export function createSandbox() {
  const sandbox = new LegacySandbox();
  // Contamination and leakage from the load or bootstrap phase
  const bootstrappingFreers = patchAtBootstrapping();
  let sideEffectsRebuilders = [];
  return {
    proxy: sandbox.proxy,
    // The sandbox is mounted either from the bootstrap state or from unmount after waking up again
    async mount() {
      / * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - 1. Start/resume sandbox -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * /
      sandbox.active();
      const sideEffectsRebuildersAtBootstrapping = sideEffectsRebuilders.slice(
        0,
        bootstrappingFreers.length
      );
      // Reconstructing side effects of the bootstrap phase of the application, such as dynamically inserting CSS
      sideEffectsRebuildersAtBootstrapping.forEach((rebuild) = > rebuild());
      / * -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- 2. Open global side listen -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- - * /
      // Render sandbox starts to hijack all kinds of global listeners when bootstrap is started. Try not to have side effects such as event listeners/timers during bootstrap initialization. These side effects cannot be removed
      mountingFreers = patchAtMounting(
        appName,
        elementGetter,
        sandbox,
        singular,
        scopedCSS,
        excludeAssetFilter
      );
      sideEffectsRebuilders = [];
    },
    // Restore the global state to the state before the application was loaded
    async unmount() {
      // Each Freers release returns a rebuild function, or an empty function if the Freers do not need to rebuild
      sideEffectsRebuilders = [...bootstrappingFreers].map((free) = >free()); sandbox.inactive(); }}; }Copy the code

Take a look at the implementation of the LegacySandbox sandbox. The main purpose of the sandbox is to deal with global variable contamination and hijack all window operations by replacing them with a proxy.

class SingularProxySandbox {
  // Global variables updated during sandbox
  addedPropsMapInSandbox = new Map(a);// Global variables updated during sandbox
  modifiedPropsOriginalValueMapInSandbox = new Map(a);// Keep a map of updated (new and modified) global variables for snapshot at any time
  currentUpdatedPropsValueMap = new Map(a); sandboxRunning =true;
  active() {
    // Restore a snapshot of the last time the sandbox was run
    this.currentUpdatedPropsValueMap.forEach((v, p) = > setWindowProp(p, v));
    this.sandboxRunning = true;
  }
  inactive() {
    // Change the value back when the sandbox is destroyed
    this.modifiedPropsOriginalValueMapInSandbox.forEach((v, p) = > setWindowProp(p, v));
    // Empty the new value when the sandbox is destroyed
    this.addedPropsMapInSandbox.forEach((_, p) = > setWindowProp(p, undefined.true));
    this.sandboxRunning = false;
  }
  constructor(name) {
    const proxy = new Proxy(window, {
      set(_, p, value) {
          // If the property does not exist in the current window object, the property is added
          if (!window.hasOwnProperty(p)) {
            addedPropsMapInSandbox.set(p, value);
          // If the property exists in the current Window object and is not recorded in map, record the value of the property before modification and save it
          } else if(! modifiedPropsOriginalValueMapInSandbox.has(p)) {const originalValue = window[p];
            modifiedPropsOriginalValueMapInSandbox.set(p, originalValue);
          }
          // This value is recorded as the latest snapshot regardless of whether it is added or modified
          currentUpdatedPropsValueMap.set(p, value);
          window[p] = value; }},get(_, p) {
        return window[p]
      },
    })
  }
}
Copy the code

In addition to the problem of global variable contamination, there are other leakage issues that need to be dealt with, which qiankun uses different patch functions to hijack.

// Handle leaks during the mount phase and the application run phase
export function patchAtMounting() {
  return [
    // Handle timer leaks
    patchInterval(),
    // Handle global event listening leaks
    patchWindowListener(),
    patchHistoryListener(),
    // This is strictly not a leak. It listens for the dom structure of the dynamically inserted page (including script and style).
    patchDynamicAppend(),
  ];
}
// Handle leaks generated during the Load and bootstrap phases
export function patchAtBootstrapping() {
  return [patchDynamicAppend()];
}
Copy the code

An example of patch is as follows:

const rawWindowInterval = window.setInterval;
const rawWindowClearInterval = window.clearInterval;
export default function patchInterval(global) {
  let intervals = [];
  global.clearInterval = (intervalId) = > {
    intervals = intervals.filter((id) = >id ! == intervalId);return rawWindowClearInterval(intervalId);
  };
  global.setInterval = (handler, timeout, ... arg) = > {
    constintervalId = rawWindowInterval(handler, timeout, ... args); intervals = [...intervals, intervalId];return intervalId;
  };
  // Returns the method to release these leaks
  return function free() {
    intervals.forEach((id) = > global.clearInterval(id));
    global.setInterval = rawWindowInterval;
    global.clearInterval = rawWindowClearInterval;
    // Does this patch have any scenes that need to be reconstructed? If not, it is an empty function
    return function rebuild() {};
  };
}
Copy the code

The design of returning to cancel is subtle and can be found in VUE as well.

// The listener returns a method to cancel the listener, which returns a method to re-listen
const unwatch = this.$watch("xxx".() = > {});
const rewatch = unwatch(); // Pseudo code, actually no
Copy the code

Let’s look at the most complex patchDynamicAppend implementation, which handles scenarios where scripts and links are dynamically inserted into code.

const rawHeadAppendChild = HTMLHeadElement.prototype.appendChild;
export default function patchDynamicAppend(mounting, proxy) {
  let dynamicStyleSheetElements = [];
  // hijack the insert function
  HTMLHeadElement.prototype.appendChild = function(element) {
    switch (element.tagName) {
      case LINK_TAG_NAME:
      // If the 
      case STYLE_TAG_NAME: {
        dynamicStyleSheetElements.push(stylesheetElement);
        return rawHeadAppendChild.call(appWrapperGetter(), stylesheetElement);
      }
      // If the 
      case SCRIPT_TAG_NAME: {
        const { src, text } = element;
        execScripts(null, [src ? src : `<script>${text}</script>`], proxy);
        const dynamicScriptCommentElement = document.createComment(
          src
            ? `dynamic script ${src} replaced by qiankun`
            : "dynamic inline script replaced by qiankun"
        );
        returnrawHeadAppendChild.call( appWrapperGetter(), dynamicScriptCommentElement ); }}return rawHeadAppendChild.call(this, element);
  };
  // Free doesn't need to release anything, because the style element will disappear as the content area is cleared
  return function free() {
    // We need to rebuild the style element the next time we continue to mount the application
    return function rebuild() {
      dynamicStyleSheetElements.forEach((stylesheetElement) = > {
        document.head.appendChild.call(appWrapperGetter(), stylesheetElement);
      });
      if (mounting) dynamicStyleSheetElements = [];
    };
  };
}
Copy the code

2.6 Parent-child Application Communication

The Qiankun implements a simple global data store that both parent and child applications can read and write to together as a supplement to single-SPA events.

let globalState = {};
export function getMicroAppStateActions(id, isMaster) {
  return {
    // Event change callback
    onGlobalStateChange(callback, fireImmediately) {
      deps[id] = callback;
      const cloneState = cloneDeep(globalState);
      if(fireImmediately) { callback(cloneState, cloneState); }},// Set the global status
    setGlobalState(state) {
      const prevGlobalState = cloneDeep(globalState);
      Object.keys(deps).forEach((id) = > {
        deps[id](cloneDeep(globalState), cloneDeep(prevGlobalState));
      });
      return true;
    },
    // Unregister the application dependencies
    offGlobalStateChange() {
      deletedeps[id]; }}; }Copy the code

2.7 About pre-request

Prerequest takes advantage of the point at which importEntry separates the acquisition and execution of resources to preload all child applications’ resources.

function prefetch(entry, opts) {
  if(! navigator.onLine || isSlowNetwork) {// Don't prefetch if in a slow network or offline
    return;
  }
  requestIdleCallback(async() = > {const { getExternalScripts, getExternalStyleSheets } = await importEntry(
      entry,
      opts
    );
    requestIdleCallback(getExternalStyleSheets);
    requestIdleCallback(getExternalScripts);
  });
}
apps.forEach(({ entry }) = > prefetch(entry, opts));
Copy the code

The principle of Qiankun and Single-SPA is shared above. In general, Qiankun is more oriented to scenarios where some sub-projects are uncontrollable and developers do not deliberately deal with pollution and memory leakage, while Single-SPA is more pure as a routing controller, and all pollution and leakage problems need to be controlled by developers themselves.

3. OPPO cloud practice

OPPO cloud also found some experience to share in the practice of qiankun micro-front-end landing process.

3.1 About the sandbox

The sandbox in Qiankun is not a panacea

  • The sandbox has only one level of hijacking, and changes such as date.prototype. XXX will not be restored

  • The current sandbox function for global variables is to mask, not clear, and this part of memory is reserved after masking, will open up the ability to customize the sandbox later

  • For the concept of memory leaks, take a look at the concept of resident memory

    Resident memory is an assistive program that can pretend to exit and still reside in memory, allowing you to run other applications that can be applied immediately when you respond, rather than having to spend time creating them again

  • Use traceless mode and do not use any Chrome extensions to troubleshoot memory problems. Production builds are also recommended

3.2 Extract the common library

  • Qiankun does not recommend shared dependency, fearing contamination of prototype chain and other issues. Single-spa recommends sharing large dependencies, which requires careful handling of contamination issues, and both recommend using WebPack’s external to share dependency libraries.

  • We also recommend sharing large public dependencies, using the External of Webpack to share dependent libraries. However, the library is loaded repeatedly for each sub-application, which saves the download time of the same library and ensures that there is no prototype chain pollution between different sub-applications. This is a compromise solution.

Refer to the link

  • qiankun

  • single-spa

  • The goal is the most complete micro front end solution