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