The preface
Sandbox this word presumably everyone should not be strange, even if strange, after reading this article is not so strange
Sandboxie, also known as a sandbox, is a virtual system program that allows you to run a browser or other program in a sandbox environment, so that changes made by the run can be removed later. It creates a sandbox-like environment in which programs run without permanent impact on the hard disk. In network security, a sandbox is a tool used to test behavior, such as untrusted files or applications, in an isolated environment
Today’s sandbox comes from the implementation of Qiankun, which is to solve the isolation problem in the micro front end solution. Currently, Qiankun can say that it is the best micro front end implementation solution. It is based on the secondary encapsulation of single-SPA, which solves many problems left by single-SPA. Runtime sandboxes are one of them
Why do we need it
Single-spa is great, but there are some issues that need to be addressed at the framework level that are not addressed, such as providing a clean, independent operating environment for each microapplication
JS global object pollution is A very common phenomenon, for example: microapplication A added A unique property on the global object window.A, this time switch to microapplication B, this time how to ensure that the window object is clean? The answer is the runtime sandbox implemented by Qiankun
conclusion
To summarize, the runtime sandbox is divided into JS sandbox and style sandbox
JS sandbox
JS sandbox, through proxy window object, record the window object on the property of the add, delete, change and check
-
The singleton pattern
It directly represents the native Window object, records the addition, deletion, modification and check of the native Window object. When the window object is activated, the window object is restored to the state when it is about to be deactivated last time, and when it is deactivated, the window object is restored to the initial state
-
Many cases of pattern
It represents a new object that is part of the non-configurable properties of the copied Window object, and all changes are based on the fakeWindow object to ensure that the properties of multiple instances do not affect each other
This is how the JS sandbox works, using the proxy as the global object of the microapplication. All operations are performed on the proxy object
Style sandbox
Create the element and hijack the creation action of script, link, and style tags by enhancing the createElement method in the multi-instance mode
AppendChild and insertBefore methods are enhanced that add elements, hijack the script, link, and style tags, determine whether the tags are inserted into the main or micro application depending on whether the main application is called, and pass the proxy object to the micro application. As its global object, to achieve the purpose of JS isolation
After initialization, return a free function, which is called when the microapplication is uninstalled. It is responsible for clearing the patch and caching the dynamically added styles (because all relevant DOM elements will be deleted after the microapplication is uninstalled).
The free function returns the rebuild function, which is called when the microapplication is remounted. It adds the cached dynamic style to the microapplication
Strictly speaking, this style sandbox is a bit of a failure to live up to its name. The real style isolation is provided by the strict style isolation mode and the Scoped CSS mode. Of course, if scoped CSS is enabled, dynamically added styles in the style sandbox will also be scoped
Back to the topic, what the style sandbox actually does is very simple. The script, link and style elements that are dynamically added are inserted into the main application, and the elements that belong to the micro application are inserted into the corresponding micro application, so that the micro application can be deleted together when it is unloaded.
Of course the style sandbox does two additional things:
- Cache dynamically added styles before unmounting and then insert them into the microapplication when the microapplication is remounted
- Pass the proxy object to the execScripts function to set it as the execution context for the microapplication
This is a summary of the run-time sandbox. For more details, read the source code analysis section below
Source code analysis
The next step is to get into the head-numbing source code analysis section. To be honest, the sandbox code is a bit difficult to run. I read the source code of Qiankun over and over again several times, github
Entry location – createSandbox
/** * generate the runtime sandbox, which is actually made up of two parts => JS sandbox (execution context), style sandbox **@param AppName Micro application name *@param The elementGetter, the getter function, <div id="__qiankun_microapp_wrapper_for_${appInstanceId}__" data-name="${appName}">${template}</div> *@param Singular is a singleton pattern *@param scopedCSS
* @param ExcludeAssetFilter specifies part of the special dynamically loaded microapplication resources (CSS/JS) are not being held back */
export function createSandbox(
appName: string,
elementGetter: () => HTMLElement | ShadowRoot,
singular: boolean,
scopedCSS: boolean, excludeAssetFilter? : (url:string) = >boolean.) {
/** * JS: /** * JS: /** * JS: /** * JS: /** * JS * The singleton mode directly represents the native Window object, records the addition, deletion, modification and query of the native Window object, and when the window object is activated, restores the window object to the state when it was last deactivated. * The multi-instance pattern represents a completely new object that is part of the non-configurable properties of the copied Window object. All changes are made based on the fakeWindow object. Sandbox.proxy is the global object of the microapplication. All operations are performed on this object. This is how JS sandbox.proxy works */
let sandbox: SandBox;
if (window.Proxy) {
sandbox = singular ? new LegacySandbox(appName) : new ProxySandbox(appName);
} else {
// The browser does not support proxy, and implements the sandbox in diff mode
sandbox = new SnapshotSandbox(appName);
}
/** * style sandbox ** enhances the createElement method in multiple cases, which creates elements and hijacks the script, link, and style tags * enhances the appendChild and insertBefore methods, which add elements, And hijack the add action of script, link and style tags to do some special processing => * Determine whether the tag is inserted into the main application or micro application according to whether it is called by the main application, and pass the proxy object to the micro application as its global object. To achieve the purpose of JS isolation * Free function is returned after initialization, which will be called when the microapplication is uninstalled. It is responsible for clearing the Patch and caching the dynamically added styles (because all relevant DOM elements will be deleted after the microapplication is uninstalled) * Free function is returned after execution. When the microapplication is remounted, it is called to add the cached dynamic style to the microapplication. * * Technically this style sandbox is a bit of a misname. The real style isolation is provided by the strict style isolation mode and scoped CSS mode. * Styles added dynamically in the style sandbox will also be scoped CSS; Back to the point, what the style sandbox actually does is very simple. The script, link and style * that are dynamically added are inserted into the main application, and the elements that belong to the micro application are inserted into the corresponding micro application, so that the micro application can be deleted together when the micro application is unloaded. * Of course, the style sandbox does two additional things: first, it caches the dynamic addition of styles before uninstallation, and then inserts them into the microapplication when the microapplication is remounted. Second, it passes the proxy object to the execScripts function and sets it as the microapplication's execution context */
const bootstrappingFreers = patchAtBootstrapping(
appName,
elementGetter,
sandbox,
singular,
scopedCSS,
excludeAssetFilter,
);
// mounting freers are one-off and should be re-init at every mounting time
Mounting Freers is one-time and should be reinitialized each time a mount is mounted
let mountingFreers: Freer[] = [];
let sideEffectsRebuilders: Rebuilder[] = [];
return {
proxy: sandbox.proxy,
/** * The sandbox is mounted from the bootstrap state. /** * The sandbox is mounted from the bootstrap state. These are things that microapplications do when they are unmounted and want to be remounted, such as reconstructing the dynamic style of the cache */
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) {
// When the microapplication mounts again, it reconstructs the cached dynamic style
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
mountingFreers = patchAtMounting(appName, elementGetter, sandbox, singular, 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
// Now only the patchHistoryListener for UMI has the rebuild action
if (sideEffectsRebuildersAtMounting.length) {
sideEffectsRebuildersAtMounting.forEach(rebuild= > rebuild());
}
// Clean up rebuilders, which will be filled back when uninstalled
sideEffectsRebuilders = [];
},
/** * restore the global state to the state before the application was loaded */
// Undo the patch made during initialization and mount; Cache some of the things the microapplication wants to do when it is mounted again (rebuild), such as rebuilding dynamic stylesheets; Deactivate microapplications
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
// When unmounting, execute the free function, release the patch made during initialization and mounting, store all rebuild functions, and rebuild what was done through patch when the microapplication is mounted again (side effect)
sideEffectsRebuilders = [...bootstrappingFreers, ...mountingFreers].map(free= >free()); sandbox.inactive(); }}; }Copy the code
JS sandbox
SingularProxySandbox JS sandbox
/** * Sandbox based on the singleton mode implemented by Proxy, directly operate the native Window object, and record the addition, deletion, change and check of the window object, initialize the window object when each microapplication switch; * When activated: restores the window object to the state it was in when it was last deactivated * When deactivated: restores the window object to its original state * *TODO:For compatibility use the same sandbox in Singular mode and wait until the new sandbox is stable before switching */
export default class SingularProxySandbox implements SandBox {
// Global variables added during sandbosting
private addedPropsMapInSandbox = new Map<PropertyKey, any> ();// Global variables updated during sandbox.key is the updated attribute and value is the updated value
private modifiedPropsOriginalValueMapInSandbox = new Map<PropertyKey, any> ();// Continuously record the map of updated (new and modified) global variables for snapshot at any time
private currentUpdatedPropsValueMap = new Map<PropertyKey, any> (); name:string;
proxy: WindowProxy;
type: SandBoxType;
sandboxRunning = true;
// Activate the sandbox
active() {
// If the sandbox is activated by deactivation ->, restore the Window object to the state it was in when it was last deactivated
if (!this.sandboxRunning) {
this.currentUpdatedPropsValueMap.forEach((v, p) = > setWindowProp(p, v));
}
// Switch the sandbox status to active
this.sandboxRunning = true;
}
// Deactivate the sandbox
inactive() {
// Development environment, print the global properties that were changed
if (process.env.NODE_ENV === 'development') {
console.info(`[qiankun:sandbox] The ${this.name}modified global properties restore... `, [
...this.addedPropsMapInSandbox.keys(),
...this.modifiedPropsOriginalValueMapInSandbox.keys(),
]);
}
// restore global props to initial snapshot
// Change the global properties that were changed back
this.modifiedPropsOriginalValueMapInSandbox.forEach((v, p) = > setWindowProp(p, v));
// Delete the new attribute
this.addedPropsMapInSandbox.forEach((_, p) = > setWindowProp(p, undefined.true));
// Switch the state of the sandbox to inactive
this.sandboxRunning = false;
}
constructor(name: string) {
this.name = name;
this.type = SandBoxType.LegacyProxy;
const { addedPropsMapInSandbox, modifiedPropsOriginalValueMapInSandbox, currentUpdatedPropsValueMap } = this;
const self = this;
const rawWindow = window;
const fakeWindow = Object.create(null) as Window;
const proxy = new Proxy(fakeWindow, {
set(_: Window, p: PropertyKey, value: any) :boolean {
if (self.sandboxRunning) {
if(! rawWindow.hasOwnProperty(p)) {// If the attribute does not exist, add it
addedPropsMapInSandbox.set(p, value);
} else if(! modifiedPropsOriginalValueMapInSandbox.has(p)) {// If this property exists in the current Window object and is not recorded in the Record Map, record the initial value of this property, indicating that the existing property is changed
const originalValue = (rawWindow as any)[p];
modifiedPropsOriginalValueMapInSandbox.set(p, originalValue);
}
currentUpdatedPropsValueMap.set(p, value);
// Set the native Window object directly, since it is a singleton, there is no other effect
// eslint-disable-next-line no-param-reassign
(rawWindow as any)[p] = value;
return true;
}
if (process.env.NODE_ENV === 'development') {
console.warn(`[qiankun] Set window.${p.toString()} while sandbox destroyed or inactive in ${name}! `);
}
// In strict-mode, the Proxy's handler.set returns false and raises TypeError, which should be ignored in the case of sandbox uninstallation
return true;
},
get(_: Window, p: PropertyKey): any {
// avoid who using window.window or window.self to escape the sandbox environment to touch the really window
// or use window.top to check if an iframe context
// see https://github.com/eligrey/FileSaver.js/blob/master/src/FileSaver.js#L13
if (p === 'top' || p === 'parent' || p === 'window' || p === 'self') {
return proxy;
}
// Get data directly from the native Window object
const value = (rawWindow as any)[p];
return getTargetValue(rawWindow, value);
},
// trap in operator
// see https://github.com/styled-components/styled-components/blob/master/packages/styled-components/src/constants.js#L12
has(_: Window, p: string | number | symbol): boolean {
return p inrawWindow; }});this.proxy = proxy; }}/** * Set the key value on the window object or delete the specified property (key) *@param prop key
* @param value value
* @param ToDelete whether toDelete */
function setWindowProp(prop: PropertyKey, value: any, toDelete? :boolean) {
if (value === undefined && toDelete) {
/ / remove the window [key]
delete (window as any)[prop];
} else if (isPropConfigurable(window, prop) && typeofprop ! = ='symbol') {
// window[key] = value
Object.defineProperty(window, prop, { writable: true.configurable: true });
(window as any)[prop] = value; }}Copy the code
ProxySandbox Multiple examples of JS sandbox
// Record the number of activated sandboxes
let activeSandboxCount = 0;
The fakeWindow object is represented by the Proxy. All changes are made to the fakeWindow object, which is different from singleton (which is important). * this ensures that the properties of each ProxySandbox instance do not affect each other */
export default class ProxySandbox implements SandBox {
/** Change record of window value */
private updatedValueSet = new Set<PropertyKey>();
name: string;
type: SandBoxType;
proxy: WindowProxy;
sandboxRunning = true;
/ / activation
active() {
// Number of active sandboxes + 1
if (!this.sandboxRunning) activeSandboxCount++;
this.sandboxRunning = true;
}
/ / the deactivation
inactive() {
if (process.env.NODE_ENV === 'development') {
console.info(`[qiankun:sandbox] The ${this.name}modified global properties restore... `, [
...this.updatedValueSet.keys(),
]);
}
// The number of activated sandboxes is -1
clearSystemJsProps(this.proxy, --activeSandboxCount === 0);
this.sandboxRunning = false;
}
constructor(name: string) {
this.name = name;
this.type = SandBoxType.Proxy;
const { updatedValueSet } = this;
const self = this;
const rawWindow = window;
// All non-configurable properties on the global object are in fakeWindow, and properties that have getter properties also exist in propertesWithGetter Map, with value set to true
const { fakeWindow, propertiesWithGetter } = createFakeWindow(rawWindow);
const descriptorTargetMap = new Map<PropertyKey, SymbolTarget>();
// Check whether the global object has a specified attribute
const hasOwnProperty = (key: PropertyKey) = > fakeWindow.hasOwnProperty(key) || rawWindow.hasOwnProperty(key);
const proxy = new Proxy(fakeWindow, {
set(target: FakeWindow, p: PropertyKey, value: any) :boolean {
// If the sandbox is running, the property values are updated and the changed properties are recorded
if (self.sandboxRunning) {
// Set the property value
// @ts-ignore
target[p] = value;
// Record the properties that were changed
updatedValueSet.add(p);
// Don't worry, it's related to systemJs
interceptSystemJsProps(p, value);
return true;
}
if (process.env.NODE_ENV === 'development') {
console.warn(`[qiankun] Set window.${p.toString()} while sandbox destroyed or inactive in ${name}! `);
}
// In strict-mode, the Proxy's handler.set returns false and raises TypeError, which should be ignored in the case of sandbox uninstallation
return true;
},
// Get the value of the execution property
get(target: FakeWindow, p: PropertyKey): any {
if (p === Symbol.unscopables) return unscopables;
// avoid who using window.window or window.self to escape the sandbox environment to touch the really window
// see https://github.com/eligrey/FileSaver.js/blob/master/src/FileSaver.js#L13
if (p === 'window' || p === 'self') {
return proxy;
}
if (
p === 'top' ||
p === 'parent' ||
(process.env.NODE_ENV === 'test' && (p === 'mockTop' || p === 'mockSafariTop'))) {// if your master app in an iframe context, allow these props escape the sandbox
if (rawWindow === rawWindow.parent) {
return proxy;
}
return (rawWindow as any)[p];
}
// proxy.hasOwnProperty would invoke getter firstly, then its value represented as rawWindow.hasOwnProperty
if (p === 'hasOwnProperty') {
return hasOwnProperty;
}
// mark the symbol to document while accessing as document.createElement could know is invoked by which sandbox for dynamic append patcher
if (p === 'document') {
document[attachDocProxySymbol] = proxy;
// remove the mark in next tick, thus we can identify whether it in micro app or not
// this approach is just a workaround, it could not cover all the complex scenarios, such as the micro app runs in the same task context with master in som case
// fixme if you have any other good ideas
nextTick(() = > delete document[attachDocProxySymbol]);
return document;
}
// All of the above are special attributes
// Get the specific properties, if the properties have getters, the properties of the native object, otherwise the properties of the fakeWindow object (native or user-set)
// eslint-disable-next-line no-bitwise
const value = propertiesWithGetter.has(p) ? (rawWindow as any)[p] : (target as any)[p] || (rawWindow as any)[p];
return getTargetValue(rawWindow, value);
},
// Check whether the specified attribute exists
// see https://github.com/styled-components/styled-components/blob/master/packages/styled-components/src/constants.js#L12
has(target: FakeWindow, p: string | number | symbol): boolean {
return p in unscopables || p in target || p in rawWindow;
},
getOwnPropertyDescriptor(target: FakeWindow, p: string | number | symbol): PropertyDescriptor | undefined {
/* as the descriptor of top/self/window/mockTop in raw window are configurable but not in proxy target, we need to get it from target to avoid TypeError see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/getOwnPropertyDescriptor > A property cannot be reported as non-configurable, if it does not exists as an own property of the target object or if it exists as a configurable own property of the target object. */
if (target.hasOwnProperty(p)) {
const descriptor = Object.getOwnPropertyDescriptor(target, p);
descriptorTargetMap.set(p, 'target');
return descriptor;
}
if (rawWindow.hasOwnProperty(p)) {
const descriptor = Object.getOwnPropertyDescriptor(rawWindow, p);
descriptorTargetMap.set(p, 'rawWindow');
// A property cannot be reported as non-configurable, if it does not exists as an own property of the target object
if(descriptor && ! descriptor.configurable) { descriptor.configurable =true;
}
return descriptor;
}
return undefined;
},
// trap to support iterator with sandbox
ownKeys(target: FakeWindow): PropertyKey[] {
return uniq(Reflect.ownKeys(rawWindow).concat(Reflect.ownKeys(target)));
},
defineProperty(target: Window, p: PropertyKey, attributes: PropertyDescriptor): boolean {
const from = descriptorTargetMap.get(p);
/* Descriptor must be defined to native window while it comes from native window via Object.getOwnPropertyDescriptor(window, p), otherwise it would cause a TypeError with illegal invocation. */
switch (from) {
case 'rawWindow':
return Reflect.defineProperty(rawWindow, p, attributes);
default:
return Reflect.defineProperty(target, p, attributes);
}
},
deleteProperty(target: FakeWindow, p: string | number | symbol): boolean {
if (target.hasOwnProperty(p)) {
// @ts-ignore
delete target[p];
updatedValueSet.delete(p);
return true;
}
return true; }});this.proxy = proxy; }}Copy the code
createFakeWindow
/** * copy all non-configurable properties from the global object to the fakeWindow object, change the property descriptors of these properties to configurable and freeze * start propertiesWithGetter properties and store them in the propertiesWithGetter map *@param Global Global object => window */
function createFakeWindow(global: Window) {
// Record the getter property on the window object. Native: window, document, location, top. Object. GetOwnPropertyDescriptor (Windows, 'window') = > {set: undefined, enumerable: true, configurable: false, a get: ƒ}
// propertiesWithGetter = {"window" => true, "document" => true, "location" => true, "top" => true, "__VUE_DEVTOOLS_GLOBAL_HOOK__" => true}
const propertiesWithGetter = new Map<PropertyKey, boolean> ();// Store all non-configurable properties and values in the Window object
const fakeWindow = {} as FakeWindow;
/* copy the non-configurable property of global to fakeWindow see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/getOwnPropertyDescriptor > A property cannot be reported as non-configurable, if it does not exists as an own property of the target object or if it exists as a configurable own property of the target object. */
Object.getOwnPropertyNames(global)
// Iterate over all the non-configurable properties of the window object
.filter(p= > {
const descriptor = Object.getOwnPropertyDescriptor(global, p);
return! descriptor? .configurable; }) .forEach(p= > {
// Get the attribute descriptor
const descriptor = Object.getOwnPropertyDescriptor(global, p);
if (descriptor) {
// Get its get attribute
const hasGetter = Object.prototype.hasOwnProperty.call(descriptor, 'get');
/* make top/self/window property configurable and writable, otherwise it will cause TypeError while get trap return. see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/get > The value reported for a property must be the same as the value of the corresponding target object property if the target object property is a non-writable, non-configurable data property. */
if (
p === 'top' ||
p === 'parent' ||
p === 'self' ||
p === 'window' ||
(process.env.NODE_ENV === 'test' && (p === 'mockTop' || p === 'mockSafariTop'))) {// Change the properties of top, parent, self, window from unconfigurable to configurable
descriptor.configurable = true;
/* The descriptor of window.window/window.top/window.self in Safari/FF are accessor descriptors, we need to avoid adding a data descriptor while it was Example: Safari/FF: Object.getOwnPropertyDescriptor(window, 'top') -> {get: function, set: undefined, enumerable: true, configurable: false} Chrome: Object.getOwnPropertyDescriptor(window, 'top') -> {value: Window, writable: false, enumerable: true, configurable: false} */
if(! hasGetter) {// If there is no getter for these properties, then the writeable property is used to set them to writeable
descriptor.writable = true; }}// If there is a getter, the propertiesWithGetter map is stored with that property as the key and true as the value
if (hasGetter) propertiesWithGetter.set(p, true);
// Set the property and description to the fakeWindow object and freeze the property descriptor, otherwise it may be changed, such as zone.js
// freeze the descriptor to avoid being modified by zone.js
// see https://github.com/angular/zone.js/blob/a5fe09b0fac27ac5df1fa746042f96f05ccb6a00/lib/browser/define-property.ts#L71
rawObjectDefineProperty(fakeWindow, p, Object.freeze(descriptor)); }});return {
fakeWindow,
propertiesWithGetter,
};
}
Copy the code
SnapshotSandbox
function iter(obj: object, callbackFn: (prop: any) = >void) {
// eslint-disable-next-line guard-for-in, no-restricted-syntax
for (const prop in obj) {
if(obj.hasOwnProperty(prop)) { callbackFn(prop); }}}/** * Diff-based sandbox for earlier versions of browsers that do not support Proxy */
export default class SnapshotSandbox implements SandBox {
proxy: WindowProxy;
name: string;
type: SandBoxType;
sandboxRunning = true;
privatewindowSnapshot! : Window;private modifyPropsMap: Record<any.any> = {};
constructor(name: string) {
this.name = name;
this.proxy = window;
this.type = SandBoxType.Snapshot;
}
active() {
// Record the current snapshot
this.windowSnapshot = {} as Window;
iter(window.prop= > {
this.windowSnapshot[prop] = window[prop];
});
// Restore the previous 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]; }});if (process.env.NODE_ENV === 'development') {
console.info(`[qiankun:sandbox] The ${this.name}origin window restore... `.Object.keys(this.modifyPropsMap));
}
this.sandboxRunning = false; }}Copy the code
Style sandbox
patchAtBootstrapping
/** * Add a patch * to createElement, appendChild, insertBefore during initialization@param appName
* @param elementGetter
* @param sandbox
* @param singular
* @param scopedCSS
* @param excludeAssetFilter
*/
export function patchAtBootstrapping(
appName: string,
elementGetter: () => HTMLElement | ShadowRoot,
sandbox: SandBox,
singular: boolean,
scopedCSS: boolean, excludeAssetFilter? :Function.) :Freer[] {
// Base patch: add createElement, appendChild, insertBefore
const basePatchers = [
() = > patchDynamicAppend(appName, elementGetter, sandbox.proxy, false, singular, scopedCSS, excludeAssetFilter),
];
// Each sandbox requires a basic patch
const patchersInSandbox = {
[SandBoxType.LegacyProxy]: basePatchers,
[SandBoxType.Proxy]: basePatchers,
[SandBoxType.Snapshot]: basePatchers,
};
// Return an array whose elements are the results of patch execution => free
returnpatchersInSandbox[sandbox.type]? .map(patch= > patch());
}
Copy the code
patch
AppendChild (); insertBefore (); /** * add (); /** add (); And hijack the add action of script, link and style tags to do some special processing => * Determine whether the tag is inserted into the main application or micro application according to whether it is called by the main application, and pass the proxy object to the micro application as its global object. * After initialization, return the free function, which is responsible for clearing the Patch and caching the dynamically added style (because all relevant DOM elements will be deleted after the microapplication is uninstalled) * After the free function is executed, return the rebuild function. * * Just hijack dynamic head append, that could avoid accidentally hijacking the insertion of elements except in head. * Such a case: ReactDOM.createPortal(<style>.test{color:blue}</style>, container), * this could made we append the style element into app wrapper but it will cause an error while the react portal unmounting, as ReactDOM could not find the style in body children list. *@param AppName Micro application name *@param AppWrapperGetter the getter function, <div id="__qiankun_microapp_wrapper_for_${appInstanceId}__" data-name="${appName}">${template}</div> *@param Proxy Window Proxy *@param Mounting Whether to mount *@param Singular is a singleton *@param Scoped CSS Specifies whether to deprecate scopedCSS *@param ExcludeAssetFilter specifies part of the special dynamically loaded microapplication resources (CSS/JS) are not being held back */
export default function patch(
appName: string,
appWrapperGetter: () => HTMLElement | ShadowRoot,
proxy: Window,
mounting = true,
singular = true,
scopedCSS = false, excludeAssetFilter? : CallableFunction,) :Freer {
// A dynamic style sheet that stores all dynamically added styles
let dynamicStyleSheetElements: Array<HTMLLinkElement | HTMLStyleElement> = [];
// Add the createElement method to the multi-instance mode so that it can create elements, but also hijack the script, link, and style elements
const unpatchDocumentCreate = patchDocumentCreateElement(
appName,
appWrapperGetter,
singular,
proxy,
dynamicStyleSheetElements,
);
AppendChild, insertBefore, removeChild; AppendChild and insertBefore can handle scripts, styles, and links in addition to their own work
RemoveChild removeChild removeChild removeChild removeChild removeChild removeChild removeChild removeChild removeChild removeChild removeChild removeChild removeChild removeChild removeChild
const unpatchDynamicAppendPrototypeFunctions = patchHTMLDynamicAppendPrototypeFunctions(
appName,
appWrapperGetter,
proxy,
singular,
scopedCSS,
dynamicStyleSheetElements,
excludeAssetFilter,
);
// Record the number of initialization times
if(! mounting) bootstrappingPatchCount++;// Record the number of mounts
if (mounting) mountingPatchCount++;
// After initialization, return the free function, which is responsible for clearing the patch and caching the dynamically added style. Return the rebuild function. The rebuild function adds the cached dynamic style to the micro application when the micro application is remounted
return function free() {
// bootstrap patch just called once but its freer will be called multiple times
if(! mounting && bootstrappingPatchCount ! = =0) bootstrappingPatchCount--;
if (mounting) mountingPatchCount--;
// Check whether all microapplications have been uninstalled
const allMicroAppUnmounted = mountingPatchCount === 0 && bootstrappingPatchCount === 0;
// Remove the patch and release the overwrite prototype after all the micro apps are unmounted
unpatchDynamicAppendPrototypeFunctions(allMicroAppUnmounted);
unpatchDocumentCreate(allMicroAppUnmounted);
// Since the microapplication is uninstalled and the dynamically added styles are removed, the dynamically added styles are cached here and can be used when the microapplication is unmounted again
dynamicStyleSheetElements.forEach(stylesheetElement= > {
if (stylesheetElement instanceof HTMLStyleElement && isStyledComponentsLike(stylesheetElement)) {
if (stylesheetElement.sheet) {
// record the original css rules of the style element for restore
setCachedRules(stylesheetElement, (stylesheetElement.sheet asCSSStyleSheet).cssRules); }}});// Return a rebuild function to be called when the microapplication is remounted
return function rebuild() {
// Iterate over the dynamic stylesheet
dynamicStyleSheetElements.forEach(stylesheetElement= > {
// Add a style node to the microapplication container
document.head.appendChild.call(appWrapperGetter(), stylesheetElement);
// Add the style content from the previous cache to the style node
if (stylesheetElement instanceof HTMLStyleElement && isStyledComponentsLike(stylesheetElement)) {
const cssRules = getCachedRules(stylesheetElement);
if (cssRules) {
// eslint-disable-next-line no-plusplus
for (let i = 0; i < cssRules.length; i++) {
const cssRule = cssRules[i];
(stylesheetElement.sheet asCSSStyleSheet).insertRule(cssRule.cssText); }}}});// As the hijacker will be invoked every mounting phase, we could release the cache for gc after rebuilding
if(mounting) { dynamicStyleSheetElements = []; }}; }; }Copy the code
patchDocumentCreateElement
/** * createElement (); /** * createElement (); /** * createElement ()@param AppName Micro application name *@param appWrapperGetter
* @param singular
* @param proxy
* @param dynamicStyleSheetElements
*/
function patchDocumentCreateElement(
appName: string,
appWrapperGetter: () => HTMLElement | ShadowRoot,
singular: boolean,
proxy: Window,
dynamicStyleSheetElements: HTMLStyleElement[],
) {
// If it is a singleton, return it directly
if (singular) {
return noop;
}
// Use the runtime proxy as the key to store some information about the microapplication, such as name, proxy, microapplication template, custom style sheet, etc
proxyContainerInfoMapper.set(proxy, { appName, proxy, appWrapperGetter, dynamicStyleSheetElements, singular });
// The first microapplication initializes by executing this section, enhancing the createElement method so that it can hijack the creation of script, link, and style tags in addition to creating elements
if (Document.prototype.createElement === rawDocumentCreateElement) {
Document.prototype.createElement = function createElement<K extends keyof HTMLElementTagNameMap> (
this: Document, tagName: K, options? : ElementCreationOptions,) :HTMLElement {
// Create the element
const element = rawDocumentCreateElement.call(this, tagName, options);
// Hijack script, link and style tags
if (isHijackingTag(tagName)) {
/ / this seems futile, because didn't find any place to perform Settings, proxyContainerInfoMapper. Set (this [attachDocProxySysbol])
/ / get the things of value, and then add the value to the element object, is the key to attachElementContainerSymbol
const proxyContainerInfo = proxyContainerInfoMapper.get(this[attachDocProxySymbol]);
if (proxyContainerInfo) {
Object.defineProperty(element, attachElementContainerSymbol, {
value: proxyContainerInfo,
enumerable: false}); }}// Return the created element
return element;
};
}
// Subsequent microapplication initializations return this function directly and restore the createElement method
return function unpatch(recoverPrototype: boolean) {
proxyContainerInfoMapper.delete(proxy);
if(recoverPrototype) { Document.prototype.createElement = rawDocumentCreateElement; }}; }Copy the code
patchTHMLDynamicAppendPrototypeFunctions
AppendChild, insertBefore, and removeChild methods are enhanced, and the unpatch method is returned
function patchHTMLDynamicAppendPrototypeFunctions(
appName: string,
appWrapperGetter: () => HTMLElement | ShadowRoot,
proxy: Window,
singular = true,
scopedCSS = false, dynamicStyleSheetElements: HTMLStyleElement[], excludeAssetFilter? : CallableFunction,) {
// Just overwrite it while it have not been overwrite
if (
HTMLHeadElement.prototype.appendChild === rawHeadAppendChild &&
HTMLBodyElement.prototype.appendChild === rawBodyAppendChild &&
HTMLHeadElement.prototype.insertBefore === rawHeadInsertBefore
) {
// Augment the appendChild method
HTMLHeadElement.prototype.appendChild = getOverwrittenAppendChildOrInsertBefore({
rawDOMAppendOrInsertBefore: rawHeadAppendChild,
appName,
appWrapperGetter,
proxy,
singular,
dynamicStyleSheetElements,
scopedCSS,
excludeAssetFilter,
}) as typeof rawHeadAppendChild;
HTMLBodyElement.prototype.appendChild = getOverwrittenAppendChildOrInsertBefore({
rawDOMAppendOrInsertBefore: rawBodyAppendChild,
appName,
appWrapperGetter,
proxy,
singular,
dynamicStyleSheetElements,
scopedCSS,
excludeAssetFilter,
}) as typeof rawBodyAppendChild;
HTMLHeadElement.prototype.insertBefore = getOverwrittenAppendChildOrInsertBefore({
rawDOMAppendOrInsertBefore: rawHeadInsertBefore as any,
appName,
appWrapperGetter,
proxy,
singular,
dynamicStyleSheetElements,
scopedCSS,
excludeAssetFilter,
}) as typeof rawHeadInsertBefore;
}
// Just overwrite it while it have not been overwrite
if (
HTMLHeadElement.prototype.removeChild === rawHeadRemoveChild &&
HTMLBodyElement.prototype.removeChild === rawBodyRemoveChild
) {
HTMLHeadElement.prototype.removeChild = getNewRemoveChild({
appWrapperGetter,
headOrBodyRemoveChild: rawHeadRemoveChild,
});
HTMLBodyElement.prototype.removeChild = getNewRemoveChild({
appWrapperGetter,
headOrBodyRemoveChild: rawBodyRemoveChild,
});
}
return function unpatch(recoverPrototype: boolean) {
if(recoverPrototype) { HTMLHeadElement.prototype.appendChild = rawHeadAppendChild; HTMLHeadElement.prototype.removeChild = rawHeadRemoveChild; HTMLBodyElement.prototype.appendChild = rawBodyAppendChild; HTMLBodyElement.prototype.removeChild = rawBodyRemoveChild; HTMLHeadElement.prototype.insertBefore = rawHeadInsertBefore; }}; }Copy the code
getOverwrittenAppendChildOrInsertBefore
AppendChild and insertBefore methods are enhanced to have some logic in addition to adding elements, such as: * Hijack the addition of script tags, enabling remote loading of scripts and setting the execution context of scripts *@param opts
*/
function getOverwrittenAppendChildOrInsertBefore(opts: {
appName: string;
proxy: WindowProxy;
singular: boolean;
dynamicStyleSheetElements: HTMLStyleElement[];
appWrapperGetter: CallableFunction;
rawDOMAppendOrInsertBefore: <T extendsNode>(newChild: T, refChild? : Node |null) => T;
scopedCSS: boolean; excludeAssetFilter? : CallableFunction; }) {
return function appendChildOrInsertBefore<T extends Node> (
this: HTMLHeadElement | HTMLBodyElement, newChild: T, refChild? : Node |null.) {
// The element to insert
let element = newChild as any;
// The original method
const { rawDOMAppendOrInsertBefore } = opts;
if (element.tagName) {
// Parse the parameters
// eslint-disable-next-line prefer-const
let { appName, appWrapperGetter, proxy, singular, dynamicStyleSheetElements } = opts;
const { scopedCSS, excludeAssetFilter } = opts;
// The multi-example pattern will go through a section of logic
const storedContainerInfo = element[attachElementContainerSymbol];
if (storedContainerInfo) {
// eslint-disable-next-line prefer-destructuring
appName = storedContainerInfo.appName;
// eslint-disable-next-line prefer-destructuring
singular = storedContainerInfo.singular;
// eslint-disable-next-line prefer-destructuring
appWrapperGetter = storedContainerInfo.appWrapperGetter;
// eslint-disable-next-line prefer-destructuring
dynamicStyleSheetElements = storedContainerInfo.dynamicStyleSheetElements;
// eslint-disable-next-line prefer-destructuring
proxy = storedContainerInfo.proxy;
}
const invokedByMicroApp = singular
? // check if the currently specified application is active
// While we switch page from qiankun app to a normal react routing page, the normal one may load stylesheet dynamically while page rendering,
// but the url change listener must to wait until the current call stack is flushed.
// This scenario may cause we record the stylesheet from react routing page dynamic injection,
// and remove them after the url change triggered and qiankun app is unmouting
// see https://github.com/ReactTraining/history/blob/master/modules/createHashHistory.js#L222-L230
checkActivityFunctions(window.location).some(name= > name === appName)
: // have storedContainerInfo means it invoked by a micro app in multiply mode!!!!! storedContainerInfo;switch (element.tagName) {
/ / link and style
case LINK_TAG_NAME:
case STYLE_TAG_NAME: {
// predicate, newChild is either style or link tag
const stylesheetElement: HTMLLinkElement | HTMLStyleElement = newChild as any;
/ / href attribute
const { href } = stylesheetElement as HTMLLinkElement;
if(! invokedByMicroApp || (excludeAssetFilter && href && excludeAssetFilter(href))) {// The action to create the element is not called by the microapplication, or it is a special link tag that you don't want to be hijacked by qiankun
// Create it under the main application
return rawDOMAppendOrInsertBefore.call(this, element, refChild) as T;
}
// The microapplication container DOM
const mountDOM = appWrapperGetter();
// scoped css
if (scopedCSS) {
css.process(mountDOM, stylesheetElement, appName);
}
// Store the element in the stylesheet
// eslint-disable-next-line no-shadow
dynamicStyleSheetElements.push(stylesheetElement);
// Reference element
const referenceNode = mountDOM.contains(refChild) ? refChild : null;
// Create this element in the microapplication space so that it can be removed together when the microapplication is uninstalled
return rawDOMAppendOrInsertBefore.call(mountDOM, stylesheetElement, referenceNode);
}
/ / script tags
case SCRIPT_TAG_NAME: {
// Links and text
const { src, text } = element as HTMLScriptElement;
// some script like jsonp maybe not support cors which should't use execScripts
if(! invokedByMicroApp || (excludeAssetFilter && src && excludeAssetFilter(src))) {// In the same way, create the tag under the main application
return rawDOMAppendOrInsertBefore.call(this, element, refChild) as T;
}
// The microapplication container DOM
const mountDOM = appWrapperGetter();
// Fetch method provided by the user
const { fetch } = frameworkConfiguration;
// Reference node
const referenceNode = mountDOM.contains(refChild) ? refChild : null;
// If SRC exists, it is an external script
if (src) {
// Perform a remote load and set the proxy as a global object of the script to achieve JS isolation
execScripts(null, [src], proxy, {
fetch,
strictGlobal: !singular,
beforeExec: () = > {
Object.defineProperty(document.'currentScript', {
get(): any {
return element;
},
configurable: true}); },success: () = > {
// we need to invoke the onload event manually to notify the event listener that the script was completed
// here are the two typical ways of dynamic script loading
// 1. element.onload callback way, which webpack and loadjs used, see https://github.com/muicss/loadjs/blob/master/src/loadjs.js#L138
// 2. addEventListener way, which toast-loader used, see https://github.com/pyrsmk/toast/blob/master/src/Toast.ts#L64
const loadEvent = new CustomEvent('load');
if (isFunction(element.onload)) {
element.onload(patchCustomEvent(loadEvent, () = > element));
} else {
element.dispatchEvent(loadEvent);
}
element = null;
},
error: () = > {
const errorEvent = new CustomEvent('error');
if (isFunction(element.onerror)) {
element.onerror(patchCustomEvent(errorEvent, () = > element));
} else {
element.dispatchEvent(errorEvent);
}
element = null; }});// Create a comment element indicating that the script tag was hijacked by qiankun
const dynamicScriptCommentElement = document.createComment(`dynamic script ${src} replaced by qiankun`);
return rawDOMAppendOrInsertBefore.call(mountDOM, dynamicScriptCommentElement, referenceNode);
}
// This script is an inline script
execScripts(null[`<script>${text}</script>`], proxy, {
strictGlobal: !singular,
success: element.onload,
error: element.onerror,
});
// Create a comment element indicating that the script tag was hijacked by qiankun
const dynamicInlineScriptCommentElement = document.createComment('dynamic inline script replaced by qiankun');
return rawDOMAppendOrInsertBefore.call(mountDOM, dynamicInlineScriptCommentElement, referenceNode);
}
default:
break; }}// Call the original method and insert the element
return rawDOMAppendOrInsertBefore.call(this, element, refChild);
};
}
Copy the code
getNewRemoveChild
/** * Enhance removeChild so that it can determine whether to remove script, style, or link elements from the main application or from the micro application * if they are hijacked, remove them from the micro application, otherwise remove them from the main application *@param opts
*/
function getNewRemoveChild(opts: {
appWrapperGetter: CallableFunction;
headOrBodyRemoveChild: typeof HTMLElement.prototype.removeChild;
}) {
return function removeChild<T extends Node> (this: HTMLHeadElement | HTMLBodyElement, child: T) {
// The original removeChild
const { headOrBodyRemoveChild } = opts;
try {
const { tagName } = child as any;
// If the element to be removed is one of script, link, or style
if (isHijackingTag(tagName)) {
// Microapplication container space
let { appWrapperGetter } = opts;
// storedContainerInfo contains some information about the microapplication, but storedContainerInfo should always be undefeind because the code setting the location never seems to be executed
const storedContainerInfo = (child as any)[attachElementContainerSymbol];
if (storedContainerInfo) {
// eslint-disable-next-line prefer-destructuring
// A micro application wrapper element, also known as a micro application template
appWrapperGetter = storedContainerInfo.appWrapperGetter;
}
// Remove this element from the microapplication container space
// container may had been removed while app unmounting if the removeChild action was async
const container = appWrapperGetter();
if (container.contains(child)) {
return rawRemoveChild.call(container, child) asT; }}}catch (e) {
console.warn(e);
}
// Remove elements from the main application
return headOrBodyRemoveChild.call(this, child) as T;
};
}
Copy the code
patchAtMounting
It is called during the mounting phase of the microapplication and is mainly responsible for patching each global variable (method)
export function patchAtMounting(
appName: string,
elementGetter: () => HTMLElement | ShadowRoot,
sandbox: SandBox,
singular: boolean,
scopedCSS: boolean, excludeAssetFilter? :Function.) :Freer[] {
const basePatchers = [
// Timer patch
() = > patchInterval(sandbox.proxy),
// Event listener patch
() = > patchWindowListener(sandbox.proxy),
// fix umi bug
() = > patchHistoryListener(),
// The patch used during initialization
() = > patchDynamicAppend(appName, elementGetter, sandbox.proxy, true, singular, scopedCSS, excludeAssetFilter),
];
const patchersInSandbox = {
[SandBoxType.LegacyProxy]: [...basePatchers],
[SandBoxType.Proxy]: [...basePatchers],
[SandBoxType.Snapshot]: basePatchers,
};
returnpatchersInSandbox[sandbox.type]? .map(patch= > patch());
}
Copy the code
patch => patchInterval
/** * Timer patch. When the timer is set, the timer ID is automatically recorded; when the timer is cleared, the cleared timer ID is automatically deleted; when the patch is released, all uncleared timers are automatically cleared and the timer is restored@param global = windowProxy
*/
export default function patch(global: Window) {
let intervals: number[] = [];
// Clear the timer, and clear the timer ID that has been cleared from the intervals
global.clearInterval = (intervalId: number) = > {
intervals = intervals.filter(id= >id ! == intervalId);return rawWindowClearInterval(intervalId);
};
// Set the timer and record the timer ID
global.setInterval = (handler: Function, timeout? :number. args:any[]) = > {
constintervalId = rawWindowInterval(handler, timeout, ... args); intervals = [...intervals, intervalId];return intervalId;
};
// Clear all timers and restore the timer method
return function free() {
intervals.forEach(id= > global.clearInterval(id));
global.setInterval = rawWindowInterval;
global.clearInterval = rawWindowClearInterval;
return noop;
};
}
Copy the code
patch => patchWindowListener
/** * listener patch, add the callback function that automatically records the event when the event listener is added, automatically delete the callback function when the event listener is removed, automatically delete all the event listeners when the patch is released, and restore the listening function *@param global windowProxy
*/
export default function patch(global: WindowProxy) {
// The callback function that records each event
const listenerMap = new Map<string, EventListenerOrEventListenerObject[]>();
// Set the listener
global.addEventListener = (
type: string, listener: EventListenerOrEventListenerObject, options? :boolean | AddEventListenerOptions,
) = > {
// Retrieve the existing callback function for this event from listenerMap
const listeners = listenerMap.get(type) | | [];// Save all callbacks for this event
listenerMap.set(type, [...listeners, listener]);
// Set the listener
return rawAddEventListener.call(window.type, listener, options);
};
// Remove the listener
global.removeEventListener = (
type: string, listener: EventListenerOrEventListenerObject, options? :boolean | AddEventListenerOptions,
) = > {
// Remove the specified callback function for this event from listenerMap
const storedTypeListeners = listenerMap.get(type);
if(storedTypeListeners && storedTypeListeners.length && storedTypeListeners.indexOf(listener) ! = = -1) {
storedTypeListeners.splice(storedTypeListeners.indexOf(listener), 1);
}
// Remove event listener
return rawRemoveEventListener.call(window.type, listener, options);
};
// Release patch and remove all event listeners
return function free() {
// Remove all event listeners
listenerMap.forEach((listeners, type) = >
[...listeners].forEach(listener= > global.removeEventListener(type, listener)),
);
// Restore the listener function
global.addEventListener = rawAddEventListener;
global.removeEventListener = rawRemoveEventListener;
return noop;
};
}
Copy the code
portal
- Micro front-end framework qiankun from the introduction to source analysis, detailed interpretation of the sandbox implementation of Qiankun 2.x version
- HTML Entry source analysis, a detailed interpretation of the PRINCIPLE of HTML Entry and the application in Qiankun
- Single – SPA micro front end framework from beginner to master
- github