As MENTIONED in my previous article, the essence of micro-front-end is to deal with front-end applications and the relationship between applications. Then, to implement a micro-front-end framework will involve three core elements:

  • Sub-application loading;
  • Runtime isolation between applications;
  • Interapplication communication
  • Route hijacking;

For Qiankun, route hijacking was done on single-SPA, while the main capabilities offered by Qiankun were sub-application loading and sandbox isolation.

Continuing from the above, this is the second topic in the series. This article mainly tells us how to achieve sandbox isolation based on the source code of Qiankun.

Qiankun made sandbox isolation mainly divided into three kinds:

  • legacySandBox
  • proxySandBox
  • SnapshotSandBox.

LegacySandBox and proxySandBox are implemented based on the Proxy API. In earlier browsers that do not support the Proxy API, legacySandBox and proxySandBox are degraded to snapshotSandBox. In the current version, legacySandBox is used only for Singular single instance mode, while multi-instance mode uses proxySandBox.

legacySandBox

What is the core idea of legacySandBox? The legacySandBox essentially operates on the Window object, but it has three pools of state that are used to return the parent app state when the child app is unloaded and the atomic app state when the child app is loaded:

  • AddedPropsMapInSandbox: Stores global variables added during the child application runtime to restore the master application global variables when the child application is uninstalled;
  • ModifiedPropsOriginalValueMapInSandbox: stored in child application runtime update global variables, used to unload the son also the owner when the application global variables;
  • CurrentUpdatedPropsValueMap: update stored in application of global variables, atomic application used to run when switching from state;

Let’s first look at the Proxy getter/setter:

const rawWindow = window;
const fakeWindow = Object.create(null) as Window;
// Create a hijack of fakeWindow, which is the window object we pass to the self-executing function
const proxy = new Proxy(fakeWindow, {
  set(_: Window, p: PropertyKey, value: any): boolean {
    // Runtime judgment
    if (sandboxRunning) {
      // If the window object does not have this property, the new state is recorded in the state pool;
      if(! rawWindow.hasOwnProperty(p)) { addedPropsMapInSandbox.set(p, value);// If the current window object has the property and the state pool does not have the property, then prove that the changed property is the value updated during runtime, recorded in the state pool for the last window object restore
      } else if(! modifiedPropsOriginalValueMapInSandbox.has(p)) {const originalValue = (rawWindow as any)[p];
        modifiedPropsOriginalValueMapInSandbox.set(p, originalValue);
      }

      // Record the global object modification value, which is used to apply atomic applications when the face application is activated
      currentUpdatedPropsValueMap.set(p, value);
      (rawWindow as any)[p] = value;

      return true;
    }

    return true;
  },

  get(_: Window, p: PropertyKey): any {
    // Iframe window context
    if (p === "top" || p === "window" || p === "self") {
      return proxy;
    }

    const value = (rawWindow as any)[p];
    returngetTargetValue(rawWindow, value); }});Copy the code

Let’s look at the following application sandbox activation/uninstallation:

  // Child application sandbox activation
  active() {
    // Through the state pool, the atom also applies the last written state
    if (!this.sandboxRunning) {
      this.currentUpdatedPropsValueMap.forEach((v, p) = > setWindowProp(p, v));
    }

    this.sandboxRunning = true;
  }

  // Subapplication sandbox uninstall
  inactive() {
    // Restore global variables modified during runtime
    this.modifiedPropsOriginalValueMapInSandbox.forEach((v, p) = > setWindowProp(p, v));
    // Delete global variables added during runtime
    this.addedPropsMapInSandbox.forEach((_, p) = > setWindowProp(p, undefined.true));

    this.sandboxRunning = false;
  }
Copy the code

So, in summary, legacySandBox still operates on Window objects, but it does this by returning the atomic application state when it is activated and the host application state when it is uninstalled.

proxySandBox

In Qiankun, proxySandBox is used for multi-instance scenarios. What is a multi-instance scenario, and I’ll mention it briefly here, is that typically our background system only loads the runtime of one child application at a time. However, there are also scenarios where a sub-application aggregates multiple business domains, and such sub-application often experiences the joint maintenance of its own business modules by multiple students in multiple teams. In this case, multi-instance mode can be adopted to aggregate sub-modules (this mode can also be called micro front-end module).

Getting back to the point, the most direct difference between proxySandBox and legacySandBox is that in order to support multi-instance scenarios, proxySandBox does not operate directly on window objects. In order to avoid child application operation or modification of the main application such as window, document, location and other important attributes, will be traversed to the child application window copy (fakeWindow), let’s first look at creating child application window copy:

function createFakeWindow(global: Window) {
  Map has better performance under has and Check scenarios
  const propertiesWithGetter = new Map<PropertyKey, boolean>();
  const fakeWindow = {} as FakeWindow;

  // Copy non-configurable properties from the window object
  // For example: Window, document, location are all properties that hang on window and are not configurable
  // Copy it to fakeWindow, which indirectly avoids the child application from directly manipulating these property methods on the global object
  Object.getOwnPropertyNames(global)
    .filter((p) = > {
      const descriptor = Object.getOwnPropertyDescriptor(global, p);
      // If the attribute does not exist or the attribute descriptor works without any additional information
      return! descriptor? .configurable; }) .forEach((p) = > {
      const descriptor = Object.getOwnPropertyDescriptor(global, p);
      if (descriptor) {
        // Determine whether the current property has a getter
        const hasGetter = Object.prototype.hasOwnProperty.call(
          descriptor,
          "get"
        );

        // Set the query index for properties with getters
        if (hasGetter) propertiesWithGetter.set(p, true);

        // freeze the descriptor to avoid being modified by zone.js
        // zone.js will overwrite Object.defineProperty
        // const rawObjectDefineProperty = Object.defineProperty;
        // Copy the attributes to the fakeWindow object
        rawObjectDefineProperty(fakeWindow, p, Object.freeze(descriptor)); }});return {
    fakeWindow,
    propertiesWithGetter,
  };
}
Copy the code

Now look at the getter/setter for proxySandBox:

const rawWindow = window;
// A copy of the window and the index of the property with the getter mentioned above
const { fakeWindow, propertiesWithGetter } = createFakeWindow(rawWindow);

const descriptorTargetMap = new Map<PropertyKey, SymbolTarget>();
const hasOwnProperty = (key: PropertyKey) = >
  fakeWindow.hasOwnProperty(key) || rawWindow.hasOwnProperty(key);

const proxy = new Proxy(fakeWindow, {
  set(target: FakeWindow, p: PropertyKey, value: any): boolean {
    if (sandboxRunning) {
      // Set the property value on fakeWindow
      target[p] = value;
      // Record the change of the attribute value
      updatedValueSet.add(p);

      // SystemJS property interceptor
      interceptSystemJsProps(p, value);

      return true;
    }

    // In strict-mode, Proxy handler.set returns false and raises TypeError, which should be ignored in sandbox unload cases
    return true;
  },

  get(target: FakeWindow, p: PropertyKey): any {
    if (p === Symbol.unscopables) return unscopables;

    // Avoid window.window or window.self or window.top penetrating the sandbox
    if (p === "top" || p === "window" || p === "self") {
      return proxy;
    }

    if (p === "hasOwnProperty") {
      return hasOwnProperty;
    }

    // Scenarios are used in batch scenarios, which will not be described here
    const proxyPropertyGetter = getProxyPropertyGetter(proxy, p);
    if (proxyPropertyGetter) {
      return getProxyPropertyValue(proxyPropertyGetter);
    }

    / / value
    const value = propertiesWithGetter.has(p)
      ? (rawWindow as any)[p]
      : (target as any)[p] || (rawWindow as any)[p];
    return getTargetValue(rawWindow, value);
  },

  // There are some attributes to do the operation of the code I will not list, you can check the source code
});
Copy the code

Let’s look at proxySandBox activation/uninstallation:

  active() {
    this.sandboxRunning = true;
    // Number of currently active child application sandbox instances
    activeSandboxCount++;
  }

  inactive() {
    clearSystemJsProps(this.proxy, --activeSandboxCount === 0);

    this.sandboxRunning = false;
  }
Copy the code

As you can see, since proxySandBox does not operate directly on Windows, there is no need to operate on the state pool to update/restore the state of the master and child application during activation and uninstallation. In comparison, proxySandBox is the most complete sandbox mode currently available in Qiankun. It completely isolates the state of the master application and does not pollute Windows during runtime like legacySandBox.

snapshotSandBox

The last type of sandbox is the snapshotSandBox, which is demoted to snapshotSandBox in scenarios where Proxy is not supported, as its name indicates, The principle of snapshotSandBox is to record/restore the status of child applications in the form of snapshots when they are activated/uninstalled.

Source code is very simple, directly look at the source code:

  active() {
    if (this.sandboxRunning) {
      return;
    }


    this.windowSnapshot = {} as Window;
    // the iter method iterates through the properties of the target object and executes the respective callback functions
    // Record the current snapshot
    iter(window.prop= > {
      this.windowSnapshot[prop] = window[prop];
    });

    // Restore previous runtime state changes
    Object.keys(this.modifyPropsMap).forEach((p: any) = > {
      window[p] = this.modifyPropsMap[p];
    });

    this.sandboxRunning = true;
  }

  inactive() {
    this.modifyPropsMap = {};

    iter(window.prop= > {
      if (window[prop] ! = =this.windowSnapshot[prop]) {
        // Record the changes and restore the environment
        this.modifyPropsMap[prop] = window[prop];
        window[prop] = this.windowSnapshot[prop]; }});this.sandboxRunning = false;
  }
Copy the code

In summary, the sandbox is implemented by diff the snapshot of the current window and record.

CSS isolation

This is actually a heavy topic, since I do micro front-end to now for CSS processing is not too good way, here I directly summarized the two current projects used in the scheme you can refer to.

Reductive programming

Here we can apply certain programming constraints:

  • Try not to use classes that might conflict globally or define styles for tags directly;
  • Define a unique class prefix. Today’s projects use component libraries such as ANTD, which support custom component class prefixes.
  • The main application must have a custom class prefix;

css in js

In fact, this approach is debatable, because full CSS in JS will certainly achieve CSS isolation, but in fact, such programming is not conducive to our later project maintenance and it is difficult to extract some common CSS.

Recommended reading

  • Official Qiankun Document
  • Deciphering the Micro Front end: The Birth of the Boulder App
  • Deciphering micro Front-end: Sub-Application Loading from Qiankun