brief introduction
The implementation principle of JS sandbox and style sandbox in qiankun framework is explained in detail from the source code level.
preface
The word sandbox must be familiar to everyone. Even if it is strange, it will not be so strange after reading this article
Sandboxie, also known as sandbox, is a virtual system program that allows you to run browsers or other programs in the sandbox environment, so the changes caused by running can be deleted later. It creates a sandbox like independent working environment, and the programs running in it cannot have a permanent impact on the hard disk. In network security, Sandbox refers to a tool used to test the behavior of untrusted files or applications in an isolated environment
Today's Sandbox comes from the implementation of qiankun to solve the isolation problem in the micro front-end solution. qiankun is the best micro front-end implementation solution at present. It makes secondary packaging based on single spa and solves many problems left by single spa. Runtime sandbox is one of them
Why do you need it
Although single spa is good, there are some problems that need to be solved at the framework level but have not been solved, such as providing a clean and independent running environment for each micro application
JS global object pollution is a very common phenomenon. For example, micro application a adds its own unique attribute window A. At this time, switch to micro application B. how to ensure that the window object is clean? The answer is the runtime sandbox implemented by qiankun
summary
To sum up, the runtime sandbox is divided into JS sandbox and style sandbox
JS sandbox
JS sandbox, through proxy proxy window object, records the addition, deletion, modification and query of attributes on window object
Singleton mode
It directly represents the original window object and records the addition, deletion, modification and query of the original window object. When the window object is activated, the window object will be restored to the state when it was about to be deactivated last time, and when it is deactivated, the window object will be restored to the initial state
Multi case mode
A new object is represented. This object is a part of the copied window object and cannot be configured. All changes are based on this fakeWindow object, so as to ensure that the properties of multiple instances do not affect each other
Take this proxy as the global object of micro application, and all operations are on this proxy object, which is the principle of JS sandbox
Style sandbox
By enhancing the createElement method in multi instance mode, it is responsible for creating elements and hijacking the creation actions of script, link and style tags
Enhance the appendChild and insertBefore methods, which are responsible for adding elements, hijacking the adding actions of script, link and style tags, determining whether the tag is inserted into the main application or micro application according to whether it is called by the main application, and passing the proxy object to the micro application as its global object to achieve the purpose of JS isolation
After initialization, a free function is returned, which will be called when the micro application is unloaded, and is responsible for clearing the patch and caching the dynamically added style (because all relevant DOM elements will be deleted after the micro application is unloaded)
After the free function is executed, it returns the rebuild function, which will be called when the micro application is reloaded, and is responsible for adding the dynamic style just cached to the micro application
In fact, strictly speaking, this style sandbox is not worthy of its name. The real style isolation is provided by the strict style isolation mode and scoped css mode. Of course, if scoped css is enabled, the dynamically added styles in the style sandbox will also be processed by scoped css
Back to the point, what the style sandbox actually does is very simple, that is, insert the dynamically added script, link and style into the right position, insert the main application that belongs to the main application, and insert the micro application that belongs to the micro application into the corresponding micro application, so as to facilitate deletion when the micro application is unloaded,
Of course, the style sandbox also does two additional things:
- Cache the dynamically added styles before uninstallation and insert them into the micro application when the micro application is re mounted
- Pass the proxy object to the execScripts function and set it as the execution context of the micro application
The above content is a summary of the runtime sandbox. For a more detailed implementation process, you can continue to read the source code analysis section below
Source code analysis
Next, let's go to the dizzying source code analysis part. To be honest, the code of runtime sandbox is still a little difficult. When I read qiankun source code, I read this part repeatedly for several times, github
Entrance location - createSandbox
/** * Generate a runtime sandbox, which is actually composed of two parts = > JS sandbox (execution context) and style sandbox * * @param appName Micro application name * @param elementGetter getter Function, through which you can get < div id = "_qiankun_micro app_wrapper_for ${appinstanceid}_" data-name="${appName}">${template}</div> * @param singular Singleton mode * @param scopedCSS * @param excludeAssetFilter Specifies that some special dynamically loaded micro application resources (css/js) will not be hijacked by qiankun */ export function createSandbox( appName: string, elementGetter: () => HTMLElement | ShadowRoot, singular: boolean, scopedCSS: boolean, excludeAssetFilter?: (url: string) => boolean, ) { /** * JS Sandbox records the addition, deletion, modification and query of attributes on the window object through proxy proxy. The difference is: * The singleton mode directly represents the original window object, records the addition, deletion, modification and query of the original window object, and restores the window object to the state when it was about to be deactivated last time when the window object is activated, * Restore the window object to its initial state when it is deactivated * The multi instance mode represents a new object. This object is a part of the copied window object and cannot be configured. All changes are based on this fakeWindow object, so as to ensure multiple instances * The attributes do not affect each other * Sandbox Proxy is the global object of micro application. All operations are on this proxy object. This is the principle of JS sandbox */ let sandbox: SandBox; if (window.Proxy) { sandbox = singular ? new LegacySandbox(appName) : new ProxySandbox(appName); } else { // A sandbox implemented by diff for browsers that do not support proxy sandbox = new SnapshotSandbox(appName); } /** * Style sandbox * * The createElement method in enhanced multi instance mode is responsible for creating elements and hijacking the creation actions of script, link and style tags * Enhance the appendChild and insertBefore methods, which are responsible for adding elements, hijacking the addition of script, link and style tags, and doing some special processing = > * The tag is inserted into the main application or micro application according to whether it is called by the main application, and the proxy object is passed to the micro application as its global object to achieve the purpose of JS isolation * After initialization, return the free function, which will be called when the micro application is unloaded, and is responsible for clearing the patch and caching the dynamically added style (because all relevant DOM elements will be deleted after the micro application is unloaded) * free After the function is executed, it returns the rebuild function, which will be called when the micro application is reloaded, and is responsible for adding the dynamic style just cached to the micro application * * In fact, strictly speaking, this style sandbox is not worthy of its name. The real style isolation is provided by the strict style isolation mode and scoped css mode. Of course, if scoped css is enabled, * Styles added dynamically in the style sandbox will also be processed by scoped css; Back to the point, what the style sandbox actually does is very simple. It will dynamically add script, link and style * These three elements are inserted into the right position. Those belonging to the main application are inserted into the main application, and those belonging to the micro application are inserted into the corresponding micro application, so that they can be deleted together when the micro application is unloaded, * Of course, the style sandbox also does two additional things: first, cache the dynamically added styles before unloading, and insert them into the micro application when the micro application is re mounted; second, pass the proxy object to execScripts * Function to set it as the execution context of the micro application */ 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 you mount let mountingFreers: Freer[] = []; let sideEffectsRebuilders: Rebuilder[] = []; return { proxy: sandbox.proxy, /** * Sandbox mount ed * It may be the mount entered from the bootstrap state * It may also wake up from unmount and enter mount again * mount The side effect of rebuilding (rebuild function), that is, some things that the micro application wants to do when it is unloaded and reloaded, such as rebuilding the dynamic style of the cache */ async mount() { /* ------------------------------------------ Due to the context dependency (window), the following code execution order cannot be changed------------------------------------------ */ /* ------------------------------------------ 1. Start / restore 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 micro application is mounted again, the dynamic style just cached is rebuilt sideEffectsRebuildersAtBootstrapping.forEach(rebuild => rebuild()); } /* ------------------------------------------ 2. Open global variable patch------------------------------------------*/ // When the render sandbox starts, it starts to hijack all kinds of global monitoring. Try not to have side effects such as event monitoring / timer in the application initialization stage mountingFreers = patchAtMounting(appName, elementGetter, sandbox, singular, scopedCSS, excludeAssetFilter); /* ------------------------------------------ 3. Reset some side effects during initialization------------------------------------------*/ // The presence of a rebuild indicates that some side effects need to be rebuilt // Now I only see that the patchHistoryListener for umi has a rebuild operation if (sideEffectsRebuildersAtMounting.length) { sideEffectsRebuildersAtMounting.forEach(rebuild => rebuild()); } // clean up rebuilders, which will be filled back when uninstalling sideEffectsRebuilders = []; }, /** * Restore the global state so that it can return to the state before the application is loaded */ // Cancel the patch in the initialization and mount phases; Some things that cache micro applications need to do when they want to be mounted again (rebuild), such as rebuilding dynamic style sheets; Inactivated micro application 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 uninstalling, execute the free function, release the patch hit during initialization and mounting, store all rebuild functions, and rebuild what is done through the patch when the micro application is mounted again (side effects) sideEffectsRebuilders = [...bootstrappingFreers, ...mountingFreers].map(free => free()); sandbox.inactive(); }, }; }
JS sandbox
SingularProxySandbox single case JS sandbox
/** * The sandbox in the singleton mode based on Proxy implementation directly operates the original window object, records the addition, deletion, modification and query of the window object, and initializes the window object every time the micro application is switched; * On activation: restores the window object to the state when it was about to be deactivated last time * Deactivation: restore the window object to its original state * * TODO: In order to be compatible, the sandbox is still used in singular mode. Switch after the new Sandbox is stable */ export default class SingularProxySandbox implements SandBox { // Global variables added during sandbox 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 updated (newly added and modified) global variable map, which is used to take a snapshot at any time private currentUpdatedPropsValueMap = new Map<PropertyKey, any>(); name: string; proxy: WindowProxy; type: SandBoxType; sandboxRunning = true; // Activate sandbox active() { // If the sandbox is deactivated - > activated, the window object will be restored to the state when it was last deactivated if (!this.sandboxRunning) { this.currentUpdatedPropsValueMap.forEach((v, p) => setWindowProp(p, v)); } // Toggle sandbox status to active this.sandboxRunning = true; } // Deactivation sandbox inactive() { // Development environment, print the changed global attributes if (process.env.NODE_ENV === 'development') { console.info(`[qiankun:sandbox] ${this.name} modified global properties restore...`, [ ...this.addedPropsMapInSandbox.keys(), ...this.modifiedPropsOriginalValueMapInSandbox.keys(), ]); } // restore global props to initial snapshot // Change the changed global attribute back this.modifiedPropsOriginalValueMapInSandbox.forEach((v, p) => setWindowProp(p, v)); // Delete the new attribute this.addedPropsMapInSandbox.forEach((_, p) => setWindowProp(p, undefined, true)); // Switch sandbox status 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)) { // Property does not exist, add addedPropsMapInSandbox.set(p, value); } else if (!modifiedPropsOriginalValueMapInSandbox.has(p)) { // If the attribute exists in the current window object and has not been recorded in the record map, the initial value of the attribute is recorded, indicating that the existing attribute is changed const originalValue = (rawWindow as any)[p]; modifiedPropsOriginalValueMapInSandbox.set(p, originalValue); } currentUpdatedPropsValueMap.set(p, value); // Set the native window object directly, because it is in singleton mode and will not have other effects // 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 handler of Proxy If set returns false, TypeError will be thrown. In the case of sandbox unloading, the error should be ignored 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 in rawWindow; }, }); this.proxy = proxy; } } /** * Set the key value or delete the specified attribute (key) on the window object * @param prop key * @param value value * @param toDelete Delete */ function setWindowProp(prop: PropertyKey, value: any, toDelete?: boolean) { if (value === undefined && toDelete) { // Delete window[key] delete (window as any)[prop]; } else if (isPropConfigurable(window, prop) && typeof prop !== 'symbol') { // window[key] = value Object.defineProperty(window, prop, { writable: true, configurable: true }); (window as any)[prop] = value; } }
ProxySandbox multi case JS sandbox
// Record the number of sandboxes activated let activeSandboxCount = 0; /** * Sandbox in multi instance mode based on Proxy implementation * proxy the fakeWindow object. All changes are based on fakeWindow, which is different from the singleton (very important), * This ensures that the properties of each ProxySandbox instance do not affect each other */ export default class ProxySandbox implements SandBox { /** window Value change record */ private updatedValueSet = new Set<PropertyKey>(); name: string; type: SandBoxType; proxy: WindowProxy; sandboxRunning = true; // activation active() { // Number of sandboxes activated + 1 if (!this.sandboxRunning) activeSandboxCount++; this.sandboxRunning = true; } // Inactivation inactive() { if (process.env.NODE_ENV === 'development') { console.info(`[qiankun:sandbox] ${this.name} modified global properties restore...`, [ ...this.updatedValueSet.keys(), ]); } // Number of sandboxes activated - 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 the properties with getter properties are still in propertyswithgetter map, and value is true const { fakeWindow, propertiesWithGetter } = createFakeWindow(rawWindow); const descriptorTargetMap = new Map<PropertyKey, SymbolTarget>(); // Judge whether the specified attribute exists in the global object 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, update the property value and record the changed property if (self.sandboxRunning) { // Set attribute value // @ts-ignore target[p] = value; // Record changed properties updatedValueSet.add(p); // Never mind, 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 handler of Proxy If set returns false, TypeError will be thrown. In the case of sandbox unloading, the error should be ignored return true; }, // Gets 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; } // The above contents are the processing of some special attributes // Get a specific attribute. If the attribute has a getter, it indicates the attributes of the native object. Otherwise, it is the attribute on 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); }, // Determine 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; } }
createFakeWindow
/** * Copy all non configurable attributes on the global object to the fakeWindow object, change the attribute descriptors of these attributes to configurable, and then freeze them * Store the property with getter property in propertiesWithGetter map * @param global Global object = > window */ function createFakeWindow(global: Window) { // Record the getter attribute on the window object. The native ones are window, document, location and top, such as object getOwnPropertyDescriptor(window, 'window') => {set: undefined, enumerable: true, configurable: false, 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) // Traverse all non configurable properties of the window object .filter(p => { const descriptor = Object.getOwnPropertyDescriptor(global, p); return !descriptor?.configurable; }) .forEach(p => { // Get attribute descriptor const descriptor = Object.getOwnPropertyDescriptor(global, p); if (descriptor) { // Get its get property 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 top, parent, self and window attributes from non configurable 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 these attributes do not have getter s, it means that the writeable attribute sets them to writable descriptor.writable = true; } } // If there is a getter, take the property as the key and true as the value and store it in the propertiesWithGetter map 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, }; }
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); } } } /** * Sandbox based on diff mode is used for low version browsers that do not support Proxy */ export default class SnapshotSandbox implements SandBox { proxy: WindowProxy; name: string; type: SandBoxType; sandboxRunning = true; private windowSnapshot!: Window; private modifyPropsMap: Record<any, any> = {}; constructor(name: string) { this.name = name; this.proxy = window; this.type = SandBoxType.Snapshot; } active() { // Record current snapshot this.windowSnapshot = {} as Window; iter(window, prop => { this.windowSnapshot[prop] = window[prop]; }); // Restore 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 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] ${this.name} origin window restore...`, Object.keys(this.modifyPropsMap)); } this.sandboxRunning = false; } }
Style sandbox
patchAtBootstrapping
/** * In the initialization phase, make a patch for the createElement, appendChild and insertBefore methods * @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[] { // Basic patch, enhanced createElement, appendChild and insertBefore methods const basePatchers = [ () => patchDynamicAppend(appName, elementGetter, sandbox.proxy, false, singular, scopedCSS, excludeAssetFilter), ]; // Each kind of sandbox needs to lay a foundation patch const patchersInSandbox = { [SandBoxType.LegacyProxy]: basePatchers, [SandBoxType.Proxy]: basePatchers, [SandBoxType.Snapshot]: basePatchers, }; // Returns an array whose elements are the execution result of patch = > free function return patchersInSandbox[sandbox.type]?.map(patch => patch()); }
patch
/** * The createElement method in enhanced multi instance mode is responsible for creating elements and hijacking the creation actions of script, link and style tags * Enhance the appendChild and insertBefore methods, which are responsible for adding elements, hijacking the addition of script, link and style tags, and doing 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 for the purpose of packaging JS isolation * After initialization, return the free function, which is responsible for clearing the patch and caching the dynamically added styles (because all relevant DOM elements will be deleted after the micro application is unloaded) * free After the function is executed, it returns the rebuild function, which adds the cached dynamic style to the micro application when the micro application is reloaded * * 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 getter Function, through which you can get < div id = "_qiankun_micro app_wrapper_for ${appinstanceid}_" data-name="${appName}">${template}</div> * @param proxy window agent * @param mounting Is it the mounting stage * @param singular Is it a single instance * @param scopedCSS Discard scoped css * @param excludeAssetFilter Specifies that some special dynamically loaded micro application resources (css/js) will not be hijacked by qiankun */ export default function patch( appName: string, appWrapperGetter: () => HTMLElement | ShadowRoot, proxy: Window, mounting = true, singular = true, scopedCSS = false, excludeAssetFilter?: CallableFunction, ): Freer { // Add style sheet, dynamic storage, all let dynamicStyleSheetElements: Array<HTMLLinkElement | HTMLStyleElement> = []; // In the multi instance mode, the createElement method is enhanced so that it can not only create elements, but also hijack the creation of script, link and style elements const unpatchDocumentCreate = patchDocumentCreateElement( appName, appWrapperGetter, singular, proxy, dynamicStyleSheetElements, ); // Enhance the three elements of appendChild, insertBefore and removeChild; In addition to their own work, appendChild and insertBefore can also handle script, style and link // The insertion of three tags can determine whether the elements are inserted into the micro application template space or the main application template space according to the situation. removeChild can also remove the elements of the main application or the three elements in the micro application according to the situation const unpatchDynamicAppendPrototypeFunctions = patchHTMLDynamicAppendPrototypeFunctions( appName, appWrapperGetter, proxy, singular, scopedCSS, dynamicStyleSheetElements, excludeAssetFilter, ); // Record the number of initializations if (!mounting) bootstrappingPatchCount++; // Record the number of mounts if (mounting) mountingPatchCount++; // After initialization, return to the free function, which is responsible for clearing the patch, caching the dynamically added style, and returning to the rebuild function. The rebuild function adds the cached dynamic style to the micro application when the micro application is reloaded return function free() { // bootstrap patch just called once but its freer will be called multiple times if (!mounting && bootstrappingPatchCount !== 0) bootstrappingPatchCount--; if (mounting) mountingPatchCount--; // Determine whether all micro applications have been uninstalled const allMicroAppUnmounted = mountingPatchCount === 0 && bootstrappingPatchCount === 0; // Release the overwrite prototype after all the micro apps unmounted unpatchDynamicAppendPrototypeFunctions(allMicroAppUnmounted); unpatchDocumentCreate(allMicroAppUnmounted); // Because the dynamically added styles will be deleted when the micro application is unloaded. The dynamically added styles are cached here and can be used when the micro application is unloaded and re mounted 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 as CSSStyleSheet).cssRules); } } }); // Returns a rebuild function, which is called when the micro application is re mounted return function rebuild() { // Traverse dynamic style sheets dynamicStyleSheetElements.forEach(stylesheetElement => { // Like adding style nodes to the micro application container document.head.appendChild.call(appWrapperGetter(), stylesheetElement); // Add the style content to the style node. The style content is found from the cache just now 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 as CSSStyleSheet).insertRule(cssRule.cssText); } } } }); // As the hijacker will be invoked every mounting phase, we could release the cache for gc after rebuilding if (mounting) { dynamicStyleSheetElements = []; } }; }; }
patchDocumentCreateElement
/** * In the multi instance mode, the createElement method is enhanced so that in addition to the function of creating elements, it can also hijack the creation of script, link and style elements * @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 in singleton mode, return directly if (singular) { return noop; } // Take the proxy of the micro application runtime as the key to store some information of the micro application, such as name, proxy, micro application template, custom style sheet, etc proxyContainerInfoMapper.set(proxy, { appName, proxy, appWrapperGetter, dynamicStyleSheetElements, singular }); // This section will be executed during the initialization of the first micro application. The createElement method is enhanced so that it can not only create elements, but also hijack the creation actions of script, link and style tags if (Document.prototype.createElement === rawDocumentCreateElement) { Document.prototype.createElement = function createElement<K extends keyof HTMLElementTagNameMap>( this: Document, tagName: K, options?: ElementCreationOptions, ): HTMLElement { // Create element const element = rawDocumentCreateElement.call(this, tagName, options); // Hijack script, link and style tags if (isHijackingTag(tagName)) { // The following paragraph seems useless, because I didn't find any place to execute the setting, proxycontainerinfomapper set(this[attachDocProxySysbol]) // Get the value of this thing, and then add the value to the element object with attachElementContainerSymbol as the key const proxyContainerInfo = proxyContainerInfoMapper.get(this[attachDocProxySymbol]); if (proxyContainerInfo) { Object.defineProperty(element, attachElementContainerSymbol, { value: proxyContainerInfo, enumerable: false, }); } } // Returns the created element return element; }; } // This function is returned directly during subsequent micro application initialization, and is responsible for restoring the createElement method return function unpatch(recoverPrototype: boolean) { proxyContainerInfoMapper.delete(proxy); if (recoverPrototype) { Document.prototype.createElement = rawDocumentCreateElement; } }; }
patchTHMLDynamicAppendPrototypeFunctions
// Enhance the appendChild, insertBefore and removeChild methods, return the unpatch method, and cancel the enhancement 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 ) { // Enhanced 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; } }; }
getOverwrittenAppendChildOrInsertBefore
/** * Enhance the appendChild and insertBefore methods, so that in addition to the function of adding elements, they also have some other logic, such as: * Determine whether the insertion position of link, style and script elements is in the main application or micro application according to whether it is a micro application or a special element * The addition of hijacking script tag supports remote loading of scripts and setting the execution context (proxy) of scripts * @param opts */ function getOverwrittenAppendChildOrInsertBefore(opts: { appName: string; proxy: WindowProxy; singular: boolean; dynamicStyleSheetElements: HTMLStyleElement[]; appWrapperGetter: CallableFunction; rawDOMAppendOrInsertBefore: <T extends Node>(newChild: T, refChild?: Node | null) => T; scopedCSS: boolean; excludeAssetFilter?: CallableFunction; }) { return function appendChildOrInsertBefore<T extends Node>( this: HTMLHeadElement | HTMLBodyElement, newChild: T, refChild?: Node | null, ) { // Element to insert let element = newChild as any; // Original method const { rawDOMAppendOrInsertBefore } = opts; if (element.tagName) { // Analytical parameters // eslint-disable-next-line prefer-const let { appName, appWrapperGetter, proxy, singular, dynamicStyleSheetElements } = opts; const { scopedCSS, excludeAssetFilter } = opts; // A section of logic that the multi instance mode will follow 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: { // Assert that newChild is a style or link tag const stylesheetElement: HTMLLinkElement | HTMLStyleElement = newChild as any; // href attribute const { href } = stylesheetElement as HTMLLinkElement; if (!invokedByMicroApp || (excludeAssetFilter && href && excludeAssetFilter(href))) { // It is explained that the action of creating the element is not called by the micro application, or it is a special link tag that specifies that you do not want to be hijacked by qiankun // Create it under the main application return rawDOMAppendOrInsertBefore.call(this, element, refChild) as T; } // DOM application micro container const mountDOM = appWrapperGetter(); // scoped css if (scopedCSS) { css.process(mountDOM, stylesheetElement, appName); } // Save the element to the style sheet // eslint-disable-next-line no-shadow dynamicStyleSheetElements.push(stylesheetElement); // Reference element const referenceNode = mountDOM.contains(refChild) ? refChild : null; // Create this element in the micro application space so that it can be deleted directly when uninstalling the micro application return rawDOMAppendOrInsertBefore.call(mountDOM, stylesheetElement, referenceNode); } // script tag 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))) { // Similarly, create the label under the main application return rawDOMAppendOrInsertBefore.call(this, element, refChild) as T; } // Micro application container DOM const mountDOM = appWrapperGetter(); // User provided fetch method const { fetch } = frameworkConfiguration; // Reference node const referenceNode = mountDOM.contains(refChild) ? refChild : null; // If src exists, it is an outreach script if (src) { // Execute remote loading and set proxy as the global object of the script to achieve the purpose of 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 to indicate that the script tag has been hijacked by qiankun const dynamicScriptCommentElement = document.createComment(`dynamic script ${src} replaced by qiankun`); return rawDOMAppendOrInsertBefore.call(mountDOM, dynamicScriptCommentElement, referenceNode); } // Description the script is an inline script execScripts(null, [`<script>${text}</script>`], proxy, { strictGlobal: !singular, success: element.onload, error: element.onerror, }); // Create a comment element to indicate that the script tag has been 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); }; }
getNewRemoveChild
/** * Enhance removeChild so that it can decide whether to remove the specified elements from the main application or script, style and link elements from the micro application according to the situation * If it is a hijacked element, it is removed from the micro application, otherwise it is removed 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) { // Original removeChild const { headOrBodyRemoveChild } = opts; try { const { tagName } = child as any; // Special handling when the removed element is one of script, link and style if (isHijackingTag(tagName)) { // Micro application container space let { appWrapperGetter } = opts; // The storedContainerInfo set when creating a new application contains some information of the micro application, but the storedContainerInfo should always be undefeind, because the code for setting the location seems never to be executed const storedContainerInfo = (child as any)[attachElementContainerSymbol]; if (storedContainerInfo) { // eslint-disable-next-line prefer-destructuring // The wrapping element of micro application can also be called micro application template appWrapperGetter = storedContainerInfo.appWrapperGetter; } // Remove the element from the micro application 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) as T; } } } catch (e) { console.warn(e); } // Remove elements from main application return headOrBodyRemoveChild.call(this, child) as T; }; }
patchAtMounting
It will be called in the mounting phase of the micro application, which is mainly responsible for patch ing 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 listening patch () => patchWindowListener(sandbox.proxy), // fix umi bug () => patchHistoryListener(), // The patch in the initialization phase () => patchDynamicAppend(appName, elementGetter, sandbox.proxy, true, singular, scopedCSS, excludeAssetFilter), ]; const patchersInSandbox = { [SandBoxType.LegacyProxy]: [...basePatchers], [SandBoxType.Proxy]: [...basePatchers], [SandBoxType.Snapshot]: basePatchers, }; return patchersInSandbox[sandbox.type]?.map(patch => patch()); }
patch => patchInterval
/** * Timer patch: automatically record the timer id when setting the timer, automatically delete the cleared timer id when clearing the timer, automatically clear all the timers that have not been cleared when releasing the patch, and restore the timer method * @param global = windowProxy */ export default function patch(global: Window) { let intervals: number[] = []; // Clear the timer and clear the cleared timer id from the intervals global.clearInterval = (intervalId: number) => { intervals = intervals.filter(id => id !== intervalId); return rawWindowClearInterval(intervalId); }; // Set the timer and record the id of the timer global.setInterval = (handler: Function, timeout?: number, ...args: any[]) => { const intervalId = rawWindowInterval(handler, timeout, ...args); intervals = [...intervals, intervalId]; return intervalId; }; // Clear all timers and restore timer methods return function free() { intervals.forEach(id => global.clearInterval(id)); global.setInterval = rawWindowInterval; global.clearInterval = rawWindowClearInterval; return noop; }; }
patch => patchWindowListener
/** * Listener patch: add a callback function that automatically records events when listening for events, automatically delete the callback function when removing events, automatically delete all event listeners when releasing patch, and restore the listening function * @param global windowProxy */ export default function patch(global: WindowProxy) { // Callback function for recording each event const listenerMap = new Map<string, EventListenerOrEventListenerObject[]>(); // Set listener global.addEventListener = ( type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions, ) => { // Get the existing callback function of this event from the listener map const listeners = listenerMap.get(type) || []; // Save all callback functions for this event listenerMap.set(type, [...listeners, listener]); // Set listening return rawAddEventListener.call(window, type, listener, options); }; // Remove listener global.removeEventListener = ( type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions, ) => { // Removes the specified callback function for this event from the listener map 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 the patch and remove all event listeners return function free() { // Remove all event listeners listenerMap.forEach((listeners, type) => [...listeners].forEach(listener => global.removeEventListener(type, listener)), ); // Resume listening function global.addEventListener = rawAddEventListener; global.removeEventListener = rawRemoveEventListener; return noop; }; }
link
- qiankun of micro front-end framework from introduction to source code analysis
- HTML Entry source code analysis
- Single spa of micro front-end framework from entry to mastery
- github
Thank you for your likes, collections and comments. I'll see you next time.
When learning becomes habit, knowledge becomes commonsense. Scanning code concerns WeChat official account, learning and progress together. The article has been included in github , welcome to Watch and Star.