⚡️ add speedy mode for sandbox to improve performance (#2271)
This commit is contained in:
parent
a04e9ebf4d
commit
7f517264bb
|
|
@ -102,7 +102,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.10.5",
|
||||
"import-html-entry": "^1.9.0",
|
||||
"import-html-entry": "^1.14.0",
|
||||
"lodash": "^4.17.11",
|
||||
"single-spa": "^5.9.2"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -75,6 +75,7 @@ type QiankunSpecialOpts = {
|
|||
* @deprecated We use strict mode by default
|
||||
*/
|
||||
loose?: boolean;
|
||||
speedy?: boolean;
|
||||
patchers?: Patcher[];
|
||||
};
|
||||
/*
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import type {
|
|||
ObjectType,
|
||||
} from './interfaces';
|
||||
import { createSandboxContainer, css } from './sandbox';
|
||||
import { lexicalGlobals } from './sandbox/common';
|
||||
import {
|
||||
Deferred,
|
||||
genAppInstanceIdByName,
|
||||
|
|
@ -310,6 +311,7 @@ export async function loadApp<T extends ObjectType>(
|
|||
let mountSandbox = () => Promise.resolve();
|
||||
let unmountSandbox = () => Promise.resolve();
|
||||
const useLooseSandbox = typeof sandbox === 'object' && !!sandbox.loose;
|
||||
const speedySandbox = typeof sandbox === 'object' && !!sandbox.speedy;
|
||||
let sandboxContainer;
|
||||
if (sandbox) {
|
||||
sandboxContainer = createSandboxContainer(
|
||||
|
|
@ -320,6 +322,7 @@ export async function loadApp<T extends ObjectType>(
|
|||
useLooseSandbox,
|
||||
excludeAssetFilter,
|
||||
global,
|
||||
speedySandbox,
|
||||
);
|
||||
// 用沙箱的代理对象作为接下来使用的全局对象
|
||||
global = sandboxContainer.instance.proxy as typeof window;
|
||||
|
|
@ -338,7 +341,9 @@ export async function loadApp<T extends ObjectType>(
|
|||
await execHooksChain(toArray(beforeLoad), app, global);
|
||||
|
||||
// get the lifecycle hooks from module exports
|
||||
const scriptExports: any = await execScripts(global, sandbox && !useLooseSandbox);
|
||||
const scriptExports: any = await execScripts(global, sandbox && !useLooseSandbox, {
|
||||
scopedGlobalVariables: speedySandbox ? lexicalGlobals : [],
|
||||
});
|
||||
const { bootstrap, mount, unmount, update } = getLifecyclesFromExports(
|
||||
scriptExports,
|
||||
appName,
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { isBoundedFunction, isCallable, isConstructable } from '../utils';
|
|||
|
||||
type AppInstance = { name: string; window: WindowProxy };
|
||||
let currentRunningApp: AppInstance | null = null;
|
||||
|
||||
/**
|
||||
* get the app that running tasks at current tick
|
||||
*/
|
||||
|
|
@ -20,6 +21,7 @@ export function setCurrentRunningApp(appInstance: { name: string; window: Window
|
|||
}
|
||||
|
||||
const functionBoundedValueMap = new WeakMap<CallableFunction, CallableFunction>();
|
||||
|
||||
export function getTargetValue(target: any, value: any): any {
|
||||
/*
|
||||
仅绑定 isCallable && !isBoundedFunction && !isConstructable 的函数对象,如 window.console、window.atob 这类,不然微应用中调用时会抛出 Illegal invocation 异常
|
||||
|
|
@ -77,15 +79,29 @@ export function getTargetValue(target: any, value: any): any {
|
|||
return value;
|
||||
}
|
||||
|
||||
const getterInvocationResultMap = new WeakMap<CallableFunction, any>();
|
||||
export const unscopedGlobals = [
|
||||
'undefined',
|
||||
'Array',
|
||||
'Object',
|
||||
'String',
|
||||
'Boolean',
|
||||
'Math',
|
||||
'Number',
|
||||
'Symbol',
|
||||
'parseFloat',
|
||||
'Float32Array',
|
||||
'isNaN',
|
||||
'Infinity',
|
||||
'Reflect',
|
||||
'Float64Array',
|
||||
'Function',
|
||||
'Map',
|
||||
'NaN',
|
||||
'Promise',
|
||||
'Proxy',
|
||||
'Set',
|
||||
'parseInt',
|
||||
'requestAnimationFrame',
|
||||
];
|
||||
|
||||
export function getProxyPropertyValue(getter: CallableFunction) {
|
||||
const getterResult = getterInvocationResultMap.get(getter);
|
||||
if (!getterResult) {
|
||||
const result = getter();
|
||||
getterInvocationResultMap.set(getter, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
return getterResult;
|
||||
}
|
||||
export const lexicalGlobals = [...unscopedGlobals, 'globalThis', 'window', 'self'];
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export { css } from './patchers';
|
|||
* @param useLooseSandbox
|
||||
* @param excludeAssetFilter
|
||||
* @param globalContext
|
||||
* @param speedySandBox
|
||||
*/
|
||||
export function createSandboxContainer(
|
||||
appName: string,
|
||||
|
|
@ -37,6 +38,7 @@ export function createSandboxContainer(
|
|||
useLooseSandbox?: boolean,
|
||||
excludeAssetFilter?: (url: string) => boolean,
|
||||
globalContext?: typeof window,
|
||||
speedySandBox?: boolean,
|
||||
) {
|
||||
let sandbox: SandBox;
|
||||
if (window.Proxy) {
|
||||
|
|
@ -46,7 +48,14 @@ export function createSandboxContainer(
|
|||
}
|
||||
|
||||
// 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);
|
||||
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[] = [];
|
||||
|
||||
|
|
@ -76,7 +85,7 @@ export function createSandboxContainer(
|
|||
|
||||
/* ------------------------------------------ 2. 开启全局变量补丁 ------------------------------------------*/
|
||||
// render 沙箱启动时开始劫持各类全局监听,尽量不要在应用初始化阶段有 事件监听/定时器 等副作用
|
||||
mountingFreers = patchAtMounting(appName, elementGetter, sandbox, scopedCSS, excludeAssetFilter);
|
||||
mountingFreers = patchAtMounting(appName, elementGetter, sandbox, scopedCSS, excludeAssetFilter, speedySandBox);
|
||||
|
||||
/* ------------------------------------------ 3. 重置一些初始化时的副作用 ------------------------------------------*/
|
||||
// 存在 rebuilder 则表明有些副作用需要重建
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { execScripts } from 'import-html-entry';
|
|||
import { isFunction } from 'lodash';
|
||||
import { frameworkConfiguration } from '../../../apis';
|
||||
import { qiankunHeadTagName } from '../../../utils';
|
||||
import { lexicalGlobals } from '../../common';
|
||||
import * as css from '../css';
|
||||
|
||||
export const rawHeadAppendChild = HTMLHeadElement.prototype.appendChild;
|
||||
|
|
@ -183,6 +184,7 @@ export type ContainerConfig = {
|
|||
appName: string;
|
||||
proxy: WindowProxy;
|
||||
strictGlobal: boolean;
|
||||
speedySandbox: boolean;
|
||||
dynamicStyleSheetElements: Array<HTMLStyleElement | HTMLLinkElement>;
|
||||
appWrapperGetter: CallableFunction;
|
||||
scopedCSS: boolean;
|
||||
|
|
@ -213,6 +215,7 @@ function getOverwrittenAppendChildOrInsertBefore(opts: {
|
|||
appWrapperGetter,
|
||||
proxy,
|
||||
strictGlobal,
|
||||
speedySandbox,
|
||||
dynamicStyleSheetElements,
|
||||
scopedCSS,
|
||||
excludeAssetFilter,
|
||||
|
|
@ -277,10 +280,13 @@ function getOverwrittenAppendChildOrInsertBefore(opts: {
|
|||
const { fetch } = frameworkConfiguration;
|
||||
const referenceNode = mountDOM.contains(refChild) ? refChild : null;
|
||||
|
||||
const scopedGlobalVariables = speedySandbox ? lexicalGlobals : [];
|
||||
|
||||
if (src) {
|
||||
execScripts(null, [src], proxy, {
|
||||
fetch,
|
||||
strictGlobal,
|
||||
scopedGlobalVariables,
|
||||
beforeExec: () => {
|
||||
const isCurrentScriptConfigurable = () => {
|
||||
const descriptor = Object.getOwnPropertyDescriptor(document, 'currentScript');
|
||||
|
|
@ -311,7 +317,7 @@ function getOverwrittenAppendChildOrInsertBefore(opts: {
|
|||
}
|
||||
|
||||
// inline script never trigger the onload and onerror event
|
||||
execScripts(null, [`<script>${text}</script>`], proxy, { strictGlobal });
|
||||
execScripts(null, [`<script>${text}</script>`], proxy, { strictGlobal, scopedGlobalVariables });
|
||||
const dynamicInlineScriptCommentElement = document.createComment('dynamic inline script replaced by qiankun');
|
||||
dynamicScriptAttachedCommentMap.set(element, dynamicInlineScriptCommentElement);
|
||||
return rawDOMAppendOrInsertBefore.call(mountDOM, dynamicInlineScriptCommentElement, referenceNode);
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ export function patchLooseSandbox(
|
|||
appWrapperGetter,
|
||||
proxy,
|
||||
strictGlobal: false,
|
||||
speedySandbox: false,
|
||||
scopedCSS,
|
||||
dynamicStyleSheetElements,
|
||||
excludeAssetFilter,
|
||||
|
|
|
|||
|
|
@ -19,19 +19,14 @@ import {
|
|||
styleElementTargetSymbol,
|
||||
} from './common';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__proxyAttachContainerConfigMap__: WeakMap<WindowProxy, ContainerConfig>;
|
||||
}
|
||||
}
|
||||
|
||||
// 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 = nativeGlobal.__proxyAttachContainerConfigMap__;
|
||||
const proxyAttachContainerConfigMap: WeakMap<WindowProxy, ContainerConfig> =
|
||||
nativeGlobal.__proxyAttachContainerConfigMap__;
|
||||
|
||||
const elementAttachContainerConfigMap = new WeakMap<HTMLElement, ContainerConfig>();
|
||||
|
||||
|
|
@ -83,6 +78,7 @@ export function patchStrictSandbox(
|
|||
mounting = true,
|
||||
scopedCSS = false,
|
||||
excludeAssetFilter?: CallableFunction,
|
||||
speedySandbox = false,
|
||||
): Freer {
|
||||
let containerConfig = proxyAttachContainerConfigMap.get(proxy);
|
||||
if (!containerConfig) {
|
||||
|
|
@ -92,6 +88,7 @@ export function patchStrictSandbox(
|
|||
appWrapperGetter,
|
||||
dynamicStyleSheetElements: [],
|
||||
strictGlobal: true,
|
||||
speedySandbox,
|
||||
excludeAssetFilter,
|
||||
scopedCSS,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export function patchAtMounting(
|
|||
sandbox: SandBox,
|
||||
scopedCSS: boolean,
|
||||
excludeAssetFilter?: CallableFunction,
|
||||
speedySandBox?: boolean,
|
||||
): Freer[] {
|
||||
const basePatchers = [
|
||||
() => patchInterval(sandbox.proxy),
|
||||
|
|
@ -31,7 +32,8 @@ export function patchAtMounting(
|
|||
],
|
||||
[SandBoxType.Proxy]: [
|
||||
...basePatchers,
|
||||
() => patchStrictSandbox(appName, elementGetter, sandbox.proxy, true, scopedCSS, excludeAssetFilter),
|
||||
() =>
|
||||
patchStrictSandbox(appName, elementGetter, sandbox.proxy, true, scopedCSS, excludeAssetFilter, speedySandBox),
|
||||
],
|
||||
[SandBoxType.Snapshot]: [
|
||||
...basePatchers,
|
||||
|
|
@ -48,13 +50,15 @@ export function patchAtBootstrapping(
|
|||
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),
|
||||
() =>
|
||||
patchStrictSandbox(appName, elementGetter, sandbox.proxy, false, scopedCSS, excludeAssetFilter, speedySandBox),
|
||||
],
|
||||
[SandBoxType.Snapshot]: [
|
||||
() => patchLooseSandbox(appName, elementGetter, sandbox.proxy, false, scopedCSS, excludeAssetFilter),
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
import type { SandBox } from '../interfaces';
|
||||
import { SandBoxType } from '../interfaces';
|
||||
import { nativeGlobal, nextTask } from '../utils';
|
||||
import { getCurrentRunningApp, getTargetValue, setCurrentRunningApp } from './common';
|
||||
import { getCurrentRunningApp, getTargetValue, setCurrentRunningApp, unscopedGlobals } from './common';
|
||||
|
||||
type SymbolTarget = 'target' | 'globalContext';
|
||||
|
||||
|
|
@ -46,32 +46,10 @@ const variableWhiteList: PropertyKey[] = [
|
|||
];
|
||||
|
||||
/*
|
||||
variables who are impossible to be overwrite need to be escaped from proxy sandbox for performance reasons
|
||||
variables who are impossible to be overwritten need to be escaped from proxy sandbox for performance reasons
|
||||
see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/unscopables
|
||||
*/
|
||||
const unscopables = {
|
||||
undefined: true,
|
||||
Array: true,
|
||||
Object: true,
|
||||
String: true,
|
||||
Boolean: true,
|
||||
Math: true,
|
||||
Number: true,
|
||||
Symbol: true,
|
||||
parseFloat: true,
|
||||
Float32Array: true,
|
||||
isNaN: true,
|
||||
Infinity: true,
|
||||
Reflect: true,
|
||||
Float64Array: true,
|
||||
Function: true,
|
||||
Map: true,
|
||||
NaN: true,
|
||||
Promise: true,
|
||||
Proxy: true,
|
||||
Set: true,
|
||||
parseInt: true,
|
||||
requestAnimationFrame: true,
|
||||
};
|
||||
const unscopables = unscopedGlobals.reduce((acc, key) => ({ ...acc, [key]: true }), { __proto__: null });
|
||||
|
||||
const useNativeWindowForBindingsProps = new Map<PropertyKey, boolean>([
|
||||
['fetch', true],
|
||||
|
|
@ -151,28 +129,9 @@ export default class ProxySandbox implements SandBox {
|
|||
type: SandBoxType;
|
||||
|
||||
proxy: WindowProxy;
|
||||
|
||||
globalContext: typeof window;
|
||||
|
||||
sandboxRunning = true;
|
||||
|
||||
latestSetProp: PropertyKey | null = null;
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
active() {
|
||||
if (!this.sandboxRunning) activeSandboxCount++;
|
||||
this.sandboxRunning = true;
|
||||
|
|
@ -197,6 +156,8 @@ export default class ProxySandbox implements SandBox {
|
|||
this.sandboxRunning = false;
|
||||
}
|
||||
|
||||
globalContext: typeof window;
|
||||
|
||||
constructor(name: string, globalContext = window) {
|
||||
this.name = name;
|
||||
this.globalContext = globalContext;
|
||||
|
|
@ -377,4 +338,19 @@ export default class ProxySandbox implements SandBox {
|
|||
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user