micro frontend Qiankun sandbox principle

Why do you need a micro frontend

In fact, under the two major backgrounds of the birth of micro frontend, in the front-end community that advocates embracing change, new frameworks, technologies, and concepts can be seen emerging one after another, and with the evolution of WEB standards, front-end applications have better performance and faster development efficiency. However, with the higher complexity of applications, the wider team size involved, and higher performance requirements, application complexity has become an important bottleneck blocking business development. How to make existing systems embrace the latest technologies to improve productivity and decouple single applications is a challenge that front-end engineering has to face now.

If you encounter the following situations, you may need micro frontend:

  • Your monolithic application evolves from an ordinary application to a boulder application over a relatively long period of time due to the increase and change of participating personnel and teams, which leads to the problem of unmaintainable applications
  • As a portal website, it needs to integrate many systems, which are maintained by different teams, have different styles of code, and have various technical stacks. In order to aggregate, it can only take the form of iframe or MPA for aggregation

Why do you need a sandbox

In the micro frontend scenario, since multiple independent applications are organized together, conflicts are bound to occur without native isolation like iframe, such as global variable conflicts and style conflicts, which may lead to abnormal application styles and even functions. Unavailable. Therefore, in order to make the micro frontend available for production, a sandbox mechanism that allows each sub-application to achieve a certain degree of isolation is essential.

How to create a sandbox

Manual executable code

Conventional script loading is executed through script tags. To achieve sandboxing, because we need to control the opening and closing of the sandbox, we need to accurately grasp the execution timing of the script, so we need to find a suitable method for manually executing code

First of all, we think of eval. Since eval has security, performance and other issues, and is not conducive to debugging, what we have heard before is that the API of eval is not recommended.

But in the sandbox scenario of micro frontend, eval is indeed a better solution. For example, qiankun uses eval as a code executor.

New Function returns a new function by passing in a string as the body of the function, which can be used as a substitute for eval

Compared to eval, there are two more important differences:

  • Cannot access variables of the current environment, but can access global variables, which is more secure
  • You only need to process the incoming string once, and the subsequent repeated execution is the same function, while eval needs to be processed every time, with higher performance

Snapshot Sandbox

As the name suggests, that is, at a certain stage to the current operating environment to take a snapshot, and then when needed to restore the snapshot, so as to achieve isolation.

The principle of this thing is very similar to the CPU process switching in the operating system. When each process obtains the CPU usage right, it loads the context in the PCB (Process Control Block) into the corresponding register, and then the CPU starts to continue execution, waiting until the usage right is lost. Then save the information in the current register back to the PCB. The same is true of the snapshot sandbox. For each sub-application, the runtime loads the context saved internally into the corresponding variable, and saves the values of each variable in the current browser environment to the snapshot when it is destroyed.

Let’s take a look at the source code of the snapshot sandbox in qiankun, and my comments have been added to the source code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/**
* @author Hydrogen
* @since 2020-3-8
*/
import type { SandBox } from '../interfaces';
import { SandBoxType } from '../interfaces';

function iter(obj: typeof window, callbackFn: (prop: any) => void) {
// eslint-disable-next-line guard-for-in, no-restricted-syntax
for (const prop in obj) {
// patch for clearInterval for compatible reason, see #1490
if (obj.hasOwnProperty(prop) || prop = 'clearInterval') {
callbackFn(prop);
}
}
}

/**
Sandbox based on diff method, used for low version browsers that do not support proxy
*/
export default class SnapshotSandbox implements SandBox {
proxy: WindowProxy;

name: string;

type: SandBoxType;

sandboxRunning = true;

//Snapshot of the windows object, used to save the state of the window object before the current sandbox active
private windowSnapshot!: Window;

//The value of the current sandbox modified on the window object between active and inactive
private modifyPropsMap: Record<any, any> = {};

constructor(name: string) {
this.name = name;
this.proxy = window;
this.type = SandBoxType.Snapshot;
}

active() {
Record the current snapshot to compare which properties have changed when inactive
this.windowSnapshot = {} as Window;
iter(window, (prop) => {
this.windowSnapshot[prop] = window[prop];
});

Restore the previous changes, that is, the variables modified between active and inactive last time
Object.keys(this.modifyPropsMap).forEach((p: any) => {
window[p] = this.modifyPropsMap[p];
});

this.sandboxRunning = true;
}

inactive() {
this.modifyPropsMap = {};

//Compare the properties on the current window object with the properties on the window object before active. If there is any difference, record it in modifyPropsMap, and reassign it back to window when active next time.
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;
}
}

Agent sandbox

The proxy sandbox mainly creates a fakeWindow for each sandbox, then sets the proxy for the fakeWindow, and accesses the fakeWindow through the proxy, so its source code is mainly divided into two parts:

  • Create a fakeWindow with the’createFakeWindow 'function, which clones only the modifiable properties of the window object
  • Create ProxySandbox class, internally using fakeWindow created by’createFakeWindow’

Let’s take a look at’createFakeWindow ‘first. Speaking of which, what this function does is what it says in the comment:’ copy the non-configurable property of global to fakeWindow ’

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
function createFakeWindow(globalContext: Window) {
// map always has the fastest performance in has check scenario
// see https://jsperf.com/array-indexof-vs-set-has/23
const propertiesWithGetter = new Map<PropertyKey, boolean>();
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 exist as an own property of the target object or if it exists as a configurable own property of the target object.
*/
Object.getOwnPropertyNames(globalContext)
.filter((p) => {
const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
return !descriptor?.configurable;
})
.forEach((p) => {
const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
if (descriptor) {
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'))
) {
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) {
descriptor.writable = true;
}
}

if (hasGetter) propertiesWithGetter.set(p, true);

// 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,
};
}

Then there is the implementation of ProxySandbox, the key is that its constructor hijacked the set method of fakeWindow through proxy, and then in this way to count those properties have changed, and if it is a member of’globalVariableWhiteList ‘, then its descriptor is stored in’globalWhitelistPrevDescriptor’ and restored when inactive

Let’s first take a look at what this’globalVariableWhiteList 'is

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const variableWhiteListInDev =
process.env.NODE_ENV = 'test' || process.env.NODE_ENV = 'development' || window.__QIANKUN_DEVELOPMENT__
? [
// for react hot reload
// see https://github.com/facebook/create-react-app/blob/66bf7dfc43350249e2f09d138a20840dae8a0a4a/packages/react-error-overlay/src/index.js#L180
'__REACT_ERROR_OVERLAY_GLOBAL_HOOK__',
]
: [];
// who could escape the sandbox
const globalVariableWhiteList: string[] = [
// FIXME System.js used a indirect call with eval, which would make it scope escape to global
// To make System.js works well, we write it back to global window temporary
// see https://github.com/systemjs/systemjs/blob/457f5b7e8af6bd120a279540477552a07d5de086/src/evaluate.js#L106
'System',

// see https://github.com/systemjs/systemjs/blob/457f5b7e8af6bd120a279540477552a07d5de086/src/instantiate.js#L357
'__cjsWrapper',
...variableWhiteListInDev,
];

Then take a look at the general structure of ProxySandbox, where I first hide the Proxy, and then introduce it one by one:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
export default class ProxySandbox implements SandBox {
/** window value change record */
private updatedValueSet = new Set<PropertyKey>();

name: string;

type: SandBoxType;

proxy: WindowProxy;
sandboxRunning = true;
latestSetProp: PropertyKey | null = null;

active() {
if (!this.sandboxRunning) activeSandboxCount++;
this.sandboxRunning = true;
}

inactive() {
if (process.env.NODE_ENV = 'development') {
console.info(`[qiankun:sandbox] ${this.name} modified global properties restore...`, [
...this.updatedValueSet.keys(),
]);
}

if (process.env.NODE_ENV = 'test' || --activeSandboxCount = 0) {
// reset the global value to the prev value
Object.keys(this.globalWhitelistPrevDescriptor).forEach((p) => {
const descriptor = this.globalWhitelistPrevDescriptor[p];
if (descriptor) {
Object.defineProperty(this.globalContext, p, descriptor);
} else {
// @ts-ignore
delete this.globalContext[p];
}
});
}

this.sandboxRunning = false;
}

// the descriptor of global variables in whitelist before it been modified
globalWhitelistPrevDescriptor: { [p in typeof globalVariableWhiteList[number]]: PropertyDescriptor | undefined } = {};
globalContext: typeof window;

constructor(name: string, globalContext = window) {
this.name = name;
this.globalContext = globalContext;
this.type = SandBoxType.Proxy;
const { updatedValueSet } = this;

const { fakeWindow, propertiesWithGetter } = createFakeWindow(globalContext);

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

const proxy = new Proxy(fakeWindow, {
// ...
});

this.proxy = proxy;

activeSandboxCount++;
}

private registerRunningApp(name: string, proxy: Window) {
if (this.sandboxRunning) {
const currentRunningApp = getCurrentRunningApp();
if (!currentRunningApp || currentRunningApp.name ! name) {
setCurrentRunningApp({ name, window: proxy });
}
// FIXME if you have any other good ideas
// 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 complex cases, such as the micro app runs in the same task context with master in some case
nextTask(() => {
setCurrentRunningApp(null);
});
}
}
}

Here we temporarily do not see the role of the’latestSetProp, globalWhitelistPrevDescriptor 'several variables, we continue to look at the various configurations in the Proxy:

set

Set interception which will detect whether fakeWindow and globalContext have the current property to be modified, if so, then directly to fakeWindow on the property assignment, if not, only on globalContext, see if this property is writable or set method, only these two have one, it shows that the property itself can be modified, we add this property to fakeWindow and modify it is meaningful

At the same time, the set method will also check whether the currently modified property is one of the globalVariableWhiteList, and if so, add its descriptor to the globalWhitelistPrevDescriptor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
set: (target: FakeWindow, p: PropertyKey, value: any): boolean => {
if (this.sandboxRunning) {
this.registerRunningApp(name, proxy);
// We must keep its description while the property existed in globalContext before
if (!target.hasOwnProperty(p) && globalContext.hasOwnProperty(p)) {
const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
const { writable, configurable, enumerable, set } = descriptor!;
// only writable property can be overwritten
// here we ignored accessor descriptor of globalContext as it makes no sense to trigger its logic(which might make sandbox escaping instead)
// we force to set value by data descriptor
if (writable || set) {
Object.defineProperty(target, p, { configurable, enumerable, writable: true, value });
}
} else {
target[p] = value;
}

// sync the property to globalContext
if (typeof p = 'string' && globalVariableWhiteList.indexOf(p) ! -1) {
this.globalWhitelistPrevDescriptor[p] = Object.getOwnPropertyDescriptor(globalContext, p);
// @ts-ignore
globalContext[p] = value;
}

updatedValueSet.add(p);

this.latestSetProp = p;

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.set of Proxy will throw a TypeError if it returns false, which should be ignored in the case of sandbox uninstallation
return true;
},

get

This get actually does a lot of testing. For example, if the property to be obtained is window or self, it returns the proxy itself. You can take a look at the following code for many specific branches

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
get: (target: FakeWindow, p: PropertyKey): any => {
this.registerRunningApp(name, proxy);

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;
}

// hijack globalWindow accessing with globalThis keyword
if (p = 'globalThis') {
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 (globalContext = globalContext.parent) {
return proxy;
}
return (globalContext as any)[p];
}

// proxy.hasOwnProperty would invoke getter firstly, then its value represented as globalContext.hasOwnProperty
if (p = 'hasOwnProperty') {
return hasOwnProperty;
}

if (p = 'document') {
return document;
}

if (p = 'eval') {
return eval;
}

const actualTarget = propertiesWithGetter.has(p) ? globalContext : p in target ? target : globalContext;
const value = actualTarget[p];

// frozen value should return directly, see https://github.com/umijs/qiankun/issues/2015
if (isPropertyFrozen(actualTarget, p)) {
return value;
}

/* Some dom api must be bound to native window, otherwise it would cause exception like 'TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation'
See this code:
const proxy = new Proxy(window, {});
const proxyFetch = fetch.bind(proxy);
proxyFetch('https://qiankun.com');
*/
const boundTarget = useNativeWindowForBindingsProps.get(p) ? nativeGlobal : globalContext;
return getTargetValue(boundTarget, value);
},

Other

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
has(target: FakeWindow, p: string | number | symbol): boolean {
return p in unscopables || p in target || p in globalContext;
},

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 (globalContext.hasOwnProperty(p)) {
const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
descriptorTargetMap.set(p, 'globalContext');
// 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): ArrayLike<string | symbol> {
return uniq(Reflect.ownKeys(globalContext).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 'globalContext':
return Reflect.defineProperty(globalContext, p, attributes);
default:
return Reflect.defineProperty(target, p, attributes);
}
},

deleteProperty: (target: FakeWindow, p: string | number | symbol): boolean => {
this.registerRunningApp(name, proxy);
if (target.hasOwnProperty(p)) {
// @ts-ignore
delete target[p];
updatedValueSet.delete(p);

return true;
}

return true;
},

// makes sure `window instanceof Window` returns truthy in micro app
getPrototypeOf() {
return Reflect.getPrototypeOf(globalContext);
},

patchers

Hijacking interval

This part of the function is mainly used to intercept the timers during the running of the sandbox, collect which timers, and be able to clear all timers when the sandbox exits

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { noop } from 'lodash';

const rawWindowInterval = window.setInterval;
const rawWindowClearInterval = window.clearInterval;

export default function patch(global: Window) {
let intervals: number[] = [];

global.clearInterval = (intervalId: number) => {
intervals = intervals.filter((id) => id ! intervalId);
return rawWindowClearInterval.call(window, intervalId as any);
};

global.setInterval = (handler: CallableFunction, timeout?: number, ...args: any[]) => {
const intervalId = rawWindowInterval(handler, timeout, ...args);
intervals = [...intervals, intervalId];
return intervalId;
};

return function free() {
intervals.forEach((id) => global.clearInterval(id));
global.setInterval = rawWindowInterval;
global.clearInterval = rawWindowClearInterval;

return noop;
};
}

Hijacking eventListener

This function is the same as hijacking interval, hijacking all eventListeners, collecting all event listeners, and providing a function to clear all event listeners when the sandbox exits

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { noop } from 'lodash';

const rawAddEventListener = window.addEventListener;
const rawRemoveEventListener = window.removeEventListener;

export default function patch(global: WindowProxy) {
const listenerMap = new Map<string, EventListenerOrEventListenerObject[]>();

global.addEventListener = (
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | AddEventListenerOptions,
) => {
const listeners = listenerMap.get(type) || [];
listenerMap.set(type, [...listeners, listener]);
return rawAddEventListener.call(window, type, listener, options);
};

global.removeEventListener = (
type: string,
listener: EventListenerOrEventListenerObject,
options?: boolean | AddEventListenerOptions,
) => {
const storedTypeListeners = listenerMap.get(type);
if (storedTypeListeners && storedTypeListeners.length && storedTypeListeners.indexOf(listener) ! -1) {
storedTypeListeners.splice(storedTypeListeners.indexOf(listener), 1);
}
return rawRemoveEventListener.call(window, type, listener, options);
};

return function free() {
listenerMap.forEach((listeners, type) =>
[...listeners].forEach((listener) => global.removeEventListener(type, listener)),
);
global.addEventListener = rawAddEventListener;
global.removeEventListener = rawRemoveEventListener;

return noop;
};
}

Hijacking historyListener (against umi framework)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import { isFunction, noop } from 'lodash';

export default function patch() {
// FIXME umi unmount feature request
// eslint-disable-next-line @typescript-eslint/no-unused-vars
let rawHistoryListen = (_: any) => noop;
const historyListeners: Array<typeof noop> = [];
const historyUnListens: Array<typeof noop> = [];

if ((window as any).g_history && isFunction((window as any).g_history.listen)) {
rawHistoryListen = (window as any).g_history.listen.bind((window as any).g_history);

(window as any).g_history.listen = (listener: typeof noop) => {
historyListeners.push(listener);

const unListen = rawHistoryListen(listener);
historyUnListens.push(unListen);

return () => {
unListen();
historyUnListens.splice(historyUnListens.indexOf(unListen), 1);
historyListeners.splice(historyListeners.indexOf(listener), 1);
};
};
}

return function free() {
let rebuild = noop;

/*
There is also a margin for the listener to indicate that it has not been unloaded, there are two cases
1. App does not properly uninstall listener when unmout
2. listener is bound before applying mount.
In the second case, the application needs to rebind the listener before the next mount.
*/
if (historyListeners.length) {
rebuild = () => {
//The listener must be rebound using window.g_history listen to ensure that the rebuild part can also be captured, otherwise this part of the side effect cannot be properly removed after the application is uninstalled
historyListeners.forEach((listener) => (window as any).g_history.listen(listener));
};
}

//uninstall the remaining listeners
historyUnListens.forEach((unListen) => unListen());

// restore
if ((window as any).g_history && isFunction((window as any).g_history.listen)) {
(window as any).g_history.listen = rawHistoryListen;
}

return rebuild;
};
}

patch

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
export function patchLooseSandbox(
appName: string,
appWrapperGetter: () => HTMLElement | ShadowRoot,
proxy: Window,
mounting = true,
scopedCSS = false,
excludeAssetFilter?: CallableFunction,
): Freer {
let dynamicStyleSheetElements: Array<HTMLLinkElement | HTMLStyleElement> = [];

const unpatchDynamicAppendPrototypeFunctions = patchHTMLDynamicAppendPrototypeFunctions(
/*
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),
() => ({
appName,
appWrapperGetter,
proxy,
strictGlobal: false,
speedySandbox: false,
scopedCSS,
dynamicStyleSheetElements,
excludeAssetFilter,
}),
);

if (!mounting) calcAppCount(appName, 'increase', 'bootstrapping');
if (mounting) calcAppCount(appName, 'increase', 'mounting');

return function free() {
if (!mounting) calcAppCount(appName, 'decrease', 'bootstrapping');
if (mounting) calcAppCount(appName, 'decrease', 'mounting');

// release the overwrite prototype after all the micro apps unmounted
if (isAllAppsUnmounted()) unpatchDynamicAppendPrototypeFunctions();

recordStyledComponentsCSSRules(dynamicStyleSheetElements);

// As now the sub app content all wrapped with a special id container,
// the dynamic style sheet would be removed automatically while unmoutting

return function rebuild() {
rebuildCSSRules(dynamicStyleSheetElements, (stylesheetElement) => {
const appWrapper = appWrapperGetter();
if (!appWrapper.contains(stylesheetElement)) {
// Using document.head.appendChild ensures that appendChild invocation can also directly use the HTMLHeadElement.prototype.appendChild method which is overwritten at mounting phase
document.head.appendChild.call(appWrapper, stylesheetElement);
return true;
}

return false;
});

// As the patcher will be invoked every mounting phase, we could release the cache for gc after rebuilding
if (mounting) {
dynamicStyleSheetElements = [];
}
};
};
}


// Get native global window with a sandbox disgusted way, thus we could share it between qiankun instances🤪
Object.defineProperty(nativeGlobal, '__proxyAttachContainerConfigMap__', { enumerable: false, writable: true });

// Share proxyAttachContainerConfigMap between multiple qiankun instance, thus they could access the same record
nativeGlobal.__proxyAttachContainerConfigMap__ =
nativeGlobal.__proxyAttachContainerConfigMap__ || new WeakMap<WindowProxy, ContainerConfig>();
const proxyAttachContainerConfigMap: WeakMap<WindowProxy, ContainerConfig> =
nativeGlobal.__proxyAttachContainerConfigMap__;

const elementAttachContainerConfigMap = new WeakMap<HTMLElement, ContainerConfig>();

const docCreatePatchedMap = new WeakMap<typeof document.createElement, typeof document.createElement>();
function patchDocumentCreateElement() {
const docCreateElementFnBeforeOverwrite = docCreatePatchedMap.get(document.createElement);

if (!docCreateElementFnBeforeOverwrite) {
const rawDocumentCreateElement = document.createElement;
Document.prototype.createElement = function createElement<K extends keyof HTMLElementTagNameMap>(
this: Document,
tagName: K,
options?: ElementCreationOptions,
): HTMLElement {
const element = rawDocumentCreateElement.call(this, tagName, options);
if (isHijackingTag(tagName)) {
const { window: currentRunningSandboxProxy } = getCurrentRunningApp() || {};
if (currentRunningSandboxProxy) {
const proxyContainerConfig = proxyAttachContainerConfigMap.get(currentRunningSandboxProxy);
if (proxyContainerConfig) {
elementAttachContainerConfigMap.set(element, proxyContainerConfig);
}
}
}

return element;
};

// It means it have been overwritten while createElement is an own property of document
if (document.hasOwnProperty('createElement')) {
document.createElement = Document.prototype.createElement;
}

docCreatePatchedMap.set(Document.prototype.createElement, rawDocumentCreateElement);
}

return function unpatch() {
if (docCreateElementFnBeforeOverwrite) {
Document.prototype.createElement = docCreateElementFnBeforeOverwrite;
document.createElement = docCreateElementFnBeforeOverwrite;
}
};
}

export function patchStrictSandbox(
appName: string,
appWrapperGetter: () => HTMLElement | ShadowRoot,
proxy: Window,
mounting = true,
scopedCSS = false,
excludeAssetFilter?: CallableFunction,
speedySandbox = false,
): Freer {
let containerConfig = proxyAttachContainerConfigMap.get(proxy);
if (!containerConfig) {
containerConfig = {
appName,
proxy,
appWrapperGetter,
dynamicStyleSheetElements: [],
strictGlobal: true,
speedySandbox,
excludeAssetFilter,
scopedCSS,
};
proxyAttachContainerConfigMap.set(proxy, containerConfig);
}
// all dynamic style sheets are stored in proxy container
const { dynamicStyleSheetElements } = containerConfig;

const unpatchDocumentCreate = patchDocumentCreateElement();

const unpatchDynamicAppendPrototypeFunctions = patchHTMLDynamicAppendPrototypeFunctions(
(element) => elementAttachContainerConfigMap.has(element),
(element) => elementAttachContainerConfigMap.get(element)!,
);

if (!mounting) calcAppCount(appName, 'increase', 'bootstrapping');
if (mounting) calcAppCount(appName, 'increase', 'mounting');

return function free() {
if (!mounting) calcAppCount(appName, 'decrease', 'bootstrapping');
if (mounting) calcAppCount(appName, 'decrease', 'mounting');

// release the overwritten prototype after all the micro apps unmounted
if (isAllAppsUnmounted()) {
unpatchDynamicAppendPrototypeFunctions();
unpatchDocumentCreate();
}

recordStyledComponentsCSSRules(dynamicStyleSheetElements);

// As now the sub app content all wrapped with a special id container,
// the dynamic style sheet would be removed automatically while unmoutting

return function rebuild() {
rebuildCSSRules(dynamicStyleSheetElements, (stylesheetElement) => {
const appWrapper = appWrapperGetter();
if (!appWrapper.contains(stylesheetElement)) {
const mountDom =
stylesheetElement[styleElementTargetSymbol] = 'head' ? getAppWrapperHeadElement(appWrapper) : appWrapper;
rawHeadAppendChild.call(mountDom, stylesheetElement);
return true;
}

return false;
});
};
};
}

patchers

Briefly summarize what Patcher does:

  • During execution, some global timers or listeners or some global variables will be intercepted and counted, which we collectively refer to as side effects, and then return the free function to be called when the sub-application is unmounted (ie unmount)
  • The free function will clear the side effects counted in the previous part internally. At the same time, if some side effects need to be restarted when the next sub-application is reactivated, put them into the rebuild returned by the free function, and finally call rebuild when mount.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import type { Freer, SandBox } from '../../interfaces';
import { SandBoxType } from '../../interfaces';
import * as css from './css';
import { patchLooseSandbox, patchStrictSandbox } from './dynamicAppend';
import patchHistoryListener from './historyListener';
import patchInterval from './interval';
import patchWindowListener from './windowListener';

export function patchAtMounting(
appName: string,
elementGetter: () => HTMLElement | ShadowRoot,
sandbox: SandBox,
scopedCSS: boolean,
excludeAssetFilter?: CallableFunction,
speedySandBox?: boolean,
): Freer[] {
const basePatchers = [
() => patchInterval(sandbox.proxy),
() => patchWindowListener(sandbox.proxy),
() => patchHistoryListener(),
];

const patchersInSandbox = {
[SandBoxType.LegacyProxy]: [
...basePatchers,
() => patchLooseSandbox(appName, elementGetter, sandbox.proxy, true, scopedCSS, excludeAssetFilter),
],
[SandBoxType.Proxy]: [
...basePatchers,
() =>
patchStrictSandbox(appName, elementGetter, sandbox.proxy, true, scopedCSS, excludeAssetFilter, speedySandBox),
],
[SandBoxType.Snapshot]: [
...basePatchers,
() => patchLooseSandbox(appName, elementGetter, sandbox.proxy, true, scopedCSS, excludeAssetFilter),
],
};

return patchersInSandbox[sandbox.type]?.map((patch) => patch());
}

export function patchAtBootstrapping(
appName: string,
elementGetter: () => HTMLElement | ShadowRoot,
sandbox: SandBox,
scopedCSS: boolean,
excludeAssetFilter?: CallableFunction,
speedySandBox?: boolean,
): Freer[] {
const patchersInSandbox = {
[SandBoxType.LegacyProxy]: [
() => patchLooseSandbox(appName, elementGetter, sandbox.proxy, false, scopedCSS, excludeAssetFilter),
],
[SandBoxType.Proxy]: [
() =>
patchStrictSandbox(appName, elementGetter, sandbox.proxy, false, scopedCSS, excludeAssetFilter, speedySandBox),
],
[SandBoxType.Snapshot]: [
() => patchLooseSandbox(appName, elementGetter, sandbox.proxy, false, scopedCSS, excludeAssetFilter),
],
};

return patchersInSandbox[sandbox.type]?.map((patch) => patch());
}

export { css };

Sandbox collection

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
import type { Freer, Rebuilder, SandBox } from '../interfaces';
import LegacySandbox from './legacy/sandbox';
import { patchAtBootstrapping, patchAtMounting } from './patchers';
import ProxySandbox from './proxySandbox';
import SnapshotSandbox from './snapshotSandbox';

export { getCurrentRunningApp } from './common';
export { css } from './patchers';

/**
* Generate application runtime sandbox
*
* There are two types of sandboxes:
* 1. app environment sandbox
* The app environment sandbox refers to the context in which the application will run after initialization. The environment sandbox of each application will only be initialized once, because the child application will only trigger bootstrap once.
When switching sub-applications, what is actually switched is the app environment sandbox.
* 2. render sandbox
* The sub-application generates a sandbox before the app mount starts. After each sub-application switch, the render sandbox will reproduce and initialize.
*
The purpose of this design is to ensure that each sub-application can run in the environment after applying bootstrap after switching back.
*
* @param appName
* @param elementGetter
* @param scopedCSS
* @param useLooseSandbox
* @param excludeAssetFilter
* @param globalContext
* @param speedySandBox
*/
export function createSandboxContainer(
appName: string,
elementGetter: () => HTMLElement | ShadowRoot,
scopedCSS: boolean,
useLooseSandbox?: boolean,
excludeAssetFilter?: (url: string) => boolean,
globalContext?: typeof window,
speedySandBox?: boolean,
) {
let sandbox: SandBox;
if (window.Proxy) {
sandbox = useLooseSandbox ? new LegacySandbox(appName, globalContext) : new ProxySandbox(appName, globalContext);
} else {
sandbox = new SnapshotSandbox(appName);
}

// some side effect could be be invoked while bootstrapping, such as dynamic stylesheet injection with style-loader, especially during the development phase
const bootstrappingFreers = patchAtBootstrapping(
appName,
elementGetter,
sandbox,
scopedCSS,
excludeAssetFilter,
speedySandBox,
);
// mounting freers are one-off and should be re-init at every mounting time
let mountingFreers: Freer[] = [];

let sideEffectsRebuilders: Rebuilder[] = [];

return {
instance: sandbox,

/**
* Sandbox is mounted
* Possibly mount entered from bootstrap state
It may also wake up from unmount and enter mount again.
*/
async mount() {
/* ------------------------------------------ because of 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) {
sideEffectsRebuildersAtBootstrapping.forEach((rebuild) => rebuild());
}

/* ------------------------------------------ 2. Enable global variable patch ------------------------------------------*/
//When the render sandbox starts, it starts hijacking various global listeners. Try not to have side effects such as event listeners/timers during the application initialization phase
mountingFreers = patchAtMounting(appName, elementGetter, sandbox, scopedCSS, excludeAssetFilter, speedySandBox);

/* ------------------------------------------ 3. Reset some initialization side effects ------------------------------------------*/
//The presence of a rebuilder indicates that some side effects need to be rebuilt
if (sideEffectsRebuildersAtMounting.length) {
sideEffectsRebuildersAtMounting.forEach((rebuild) => rebuild());
}

// clean up rebuilders
sideEffectsRebuilders = [];
},

/**
Restore the global state so that it can return to the state before the application loaded
*/
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
sideEffectsRebuilders = [...bootstrappingFreers, ...mountingFreers].map((free) => free());

sandbox.inactive();
},
};
}