diff --git a/package.json b/package.json index 82b522c..212cda1 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/src/interfaces.ts b/src/interfaces.ts index 6dd1e28..8a14e50 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -75,6 +75,7 @@ type QiankunSpecialOpts = { * @deprecated We use strict mode by default */ loose?: boolean; + speedy?: boolean; patchers?: Patcher[]; }; /* diff --git a/src/loader.ts b/src/loader.ts index eb612e4..e940d16 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -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( 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( useLooseSandbox, excludeAssetFilter, global, + speedySandbox, ); // 用沙箱的代理对象作为接下来使用的全局对象 global = sandboxContainer.instance.proxy as typeof window; @@ -338,7 +341,9 @@ export async function loadApp( 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, diff --git a/src/sandbox/common.ts b/src/sandbox/common.ts index 863727f..39552b6 100644 --- a/src/sandbox/common.ts +++ b/src/sandbox/common.ts @@ -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(); + 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(); +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']; diff --git a/src/sandbox/index.ts b/src/sandbox/index.ts index 38a71b9..0387f5e 100644 --- a/src/sandbox/index.ts +++ b/src/sandbox/index.ts @@ -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 则表明有些副作用需要重建 diff --git a/src/sandbox/patchers/dynamicAppend/common.ts b/src/sandbox/patchers/dynamicAppend/common.ts index e6c5bb4..af5085d 100644 --- a/src/sandbox/patchers/dynamicAppend/common.ts +++ b/src/sandbox/patchers/dynamicAppend/common.ts @@ -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; 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, [``], proxy, { strictGlobal }); + execScripts(null, [``], proxy, { strictGlobal, scopedGlobalVariables }); const dynamicInlineScriptCommentElement = document.createComment('dynamic inline script replaced by qiankun'); dynamicScriptAttachedCommentMap.set(element, dynamicInlineScriptCommentElement); return rawDOMAppendOrInsertBefore.call(mountDOM, dynamicInlineScriptCommentElement, referenceNode); diff --git a/src/sandbox/patchers/dynamicAppend/forLooseSandbox.ts b/src/sandbox/patchers/dynamicAppend/forLooseSandbox.ts index a1f36c8..e4fbb75 100644 --- a/src/sandbox/patchers/dynamicAppend/forLooseSandbox.ts +++ b/src/sandbox/patchers/dynamicAppend/forLooseSandbox.ts @@ -49,6 +49,7 @@ export function patchLooseSandbox( appWrapperGetter, proxy, strictGlobal: false, + speedySandbox: false, scopedCSS, dynamicStyleSheetElements, excludeAssetFilter, diff --git a/src/sandbox/patchers/dynamicAppend/forStrictSandbox.ts b/src/sandbox/patchers/dynamicAppend/forStrictSandbox.ts index 553d292..0217f05 100644 --- a/src/sandbox/patchers/dynamicAppend/forStrictSandbox.ts +++ b/src/sandbox/patchers/dynamicAppend/forStrictSandbox.ts @@ -19,19 +19,14 @@ import { styleElementTargetSymbol, } from './common'; -declare global { - interface Window { - __proxyAttachContainerConfigMap__: WeakMap; - } -} - // 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(); -const proxyAttachContainerConfigMap = nativeGlobal.__proxyAttachContainerConfigMap__; +const proxyAttachContainerConfigMap: WeakMap = + nativeGlobal.__proxyAttachContainerConfigMap__; const elementAttachContainerConfigMap = new WeakMap(); @@ -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, }; diff --git a/src/sandbox/patchers/index.ts b/src/sandbox/patchers/index.ts index 634f3e0..cc60661 100644 --- a/src/sandbox/patchers/index.ts +++ b/src/sandbox/patchers/index.ts @@ -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), diff --git a/src/sandbox/proxySandbox.ts b/src/sandbox/proxySandbox.ts index db40308..0f89fb2 100644 --- a/src/sandbox/proxySandbox.ts +++ b/src/sandbox/proxySandbox.ts @@ -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([ ['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); + }); + } + } }