qiankun-fit/src/sandbox/proxySandbox.ts

343 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* eslint-disable no-param-reassign */
/**
* @author Kuitos
* @since 2020-3-31
*/
import type { SandBox } from '../interfaces';
import { SandBoxType } from '../interfaces';
import { nextTick } from '../utils';
import { getTargetValue, setCurrentRunningSandboxProxy } from './common';
/**
* fastest(at most time) unique array method
* @see https://jsperf.com/array-filter-unique/30
*/
function uniq(array: PropertyKey[]) {
return array.filter(function filter(this: PropertyKey[], element) {
return element in this ? false : ((this as any)[element] = true);
}, Object.create(null));
}
// zone.js will overwrite Object.defineProperty
const rawObjectDefineProperty = Object.defineProperty;
const variableWhiteListInDev =
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 variableWhiteList: PropertyKey[] = [
// 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,
];
/*
variables who are impossible to be overwrite need to be escaped from proxy sandbox for performance reasons
*/
const unscopables = {
undefined: true,
Array: true,
Object: true,
String: true,
Boolean: true,
Math: true,
Number: true,
Symbol: true,
parseFloat: true,
Float32Array: true,
};
type SymbolTarget = 'target' | 'rawWindow';
type FakeWindow = Window & Record<PropertyKey, any>;
function createFakeWindow(global: 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 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)
.filter((p) => {
const descriptor = Object.getOwnPropertyDescriptor(global, p);
return !descriptor?.configurable;
})
.forEach((p) => {
const descriptor = Object.getOwnPropertyDescriptor(global, 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,
};
}
let activeSandboxCount = 0;
/**
* 基于 Proxy 实现的沙箱
*/
export default class ProxySandbox implements SandBox {
/** window 值变更记录 */
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 (--activeSandboxCount === 0) {
variableWhiteList.forEach((p) => {
if (this.proxy.hasOwnProperty(p)) {
// @ts-ignore
delete window[p];
}
});
}
this.sandboxRunning = false;
}
constructor(name: string) {
this.name = name;
this.type = SandBoxType.Proxy;
const { updatedValueSet } = this;
const rawWindow = window;
const { fakeWindow, propertiesWithGetter } = createFakeWindow(rawWindow);
const descriptorTargetMap = new Map<PropertyKey, SymbolTarget>();
const hasOwnProperty = (key: PropertyKey) => fakeWindow.hasOwnProperty(key) || rawWindow.hasOwnProperty(key);
const proxy = new Proxy(fakeWindow, {
set: (target: FakeWindow, p: PropertyKey, value: any): boolean => {
if (this.sandboxRunning) {
// We must kept its description while the property existed in rawWindow before
if (!target.hasOwnProperty(p) && rawWindow.hasOwnProperty(p)) {
const descriptor = Object.getOwnPropertyDescriptor(rawWindow, p);
const { writable, configurable, enumerable } = descriptor!;
if (writable) {
Object.defineProperty(target, p, {
configurable,
enumerable,
writable,
value,
});
}
} else {
// @ts-ignore
target[p] = value;
}
if (variableWhiteList.indexOf(p) !== -1) {
// @ts-ignore
rawWindow[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}!`);
}
// 在 strict-mode 下Proxy 的 handler.set 返回 false 会抛出 TypeError在沙箱卸载的情况下应该忽略错误
return true;
},
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;
}
// hijack global 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 (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' || p === 'eval') {
setCurrentRunningSandboxProxy(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
nextTick(() => setCurrentRunningSandboxProxy(null));
switch (p) {
case 'document':
return document;
case 'eval':
// eslint-disable-next-line no-eval
return eval;
// no default
}
}
// eslint-disable-next-line no-nested-ternary
const value = propertiesWithGetter.has(p)
? (rawWindow as any)[p]
: p in target
? (target as any)[p]
: (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(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[] {
const keys = uniq(Reflect.ownKeys(rawWindow).concat(Reflect.ownKeys(target)));
return keys;
},
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;
activeSandboxCount++;
}
}