From dd6aa4a04272e95713ac36aec690143262692e3b Mon Sep 17 00:00:00 2001 From: Kuitos Date: Wed, 22 Feb 2023 01:12:43 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8patch=20sandbox=20document=20to=20avio?= =?UTF-8?q?d=20chaos=20of=20dynamic=20element=20appending=20when=20speedy?= =?UTF-8?q?=20mode=20enabled=20(#2404)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/interfaces.ts | 1 + src/sandbox/common.ts | 6 +- src/sandbox/legacy/sandbox.ts | 2 + .../patchers/dynamicAppend/forLooseSandbox.ts | 14 +++-- .../dynamicAppend/forStrictSandbox.ts | 56 ++++++++++++++++--- src/sandbox/patchers/index.ts | 14 ++--- src/sandbox/proxySandbox.ts | 20 ++++--- src/sandbox/snapshotSandbox.ts | 2 + 8 files changed, 84 insertions(+), 31 deletions(-) diff --git a/src/interfaces.ts b/src/interfaces.ts index a49afdf..da94291 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -130,6 +130,7 @@ export type SandBox = { sandboxRunning: boolean; /** latest set property */ latestSetProp?: PropertyKey | null; + patchDocument: (doc: Document) => void; /** 启动沙箱 */ active: () => void; /** 关闭沙箱 */ diff --git a/src/sandbox/common.ts b/src/sandbox/common.ts index 6a78a7b..ac450c9 100644 --- a/src/sandbox/common.ts +++ b/src/sandbox/common.ts @@ -15,11 +15,15 @@ export function getCurrentRunningApp() { return currentRunningApp; } -export function setCurrentRunningApp(appInstance: { name: string; window: WindowProxy } | null) { +export function setCurrentRunningApp(appInstance: { name: string; window: WindowProxy }) { // Set currentRunningApp and it's proxySandbox to global window, as its only use case is for document.createElement from now on, which hijacked by a global way currentRunningApp = appInstance; } +export function clearCurrentRunningApp() { + currentRunningApp = null; +} + const functionBoundedValueMap = new WeakMap(); export function getTargetValue(target: any, value: any): any { diff --git a/src/sandbox/legacy/sandbox.ts b/src/sandbox/legacy/sandbox.ts index c496b4c..98aea1a 100644 --- a/src/sandbox/legacy/sandbox.ts +++ b/src/sandbox/legacy/sandbox.ts @@ -155,4 +155,6 @@ export default class LegacySandbox implements SandBox { this.proxy = proxy; } + + patchDocument(): void {} } diff --git a/src/sandbox/patchers/dynamicAppend/forLooseSandbox.ts b/src/sandbox/patchers/dynamicAppend/forLooseSandbox.ts index e4fbb75..93be8e0 100644 --- a/src/sandbox/patchers/dynamicAppend/forLooseSandbox.ts +++ b/src/sandbox/patchers/dynamicAppend/forLooseSandbox.ts @@ -4,7 +4,7 @@ */ import { checkActivityFunctions } from 'single-spa'; -import type { Freer } from '../../../interfaces'; +import type { Freer, SandBox } from '../../../interfaces'; import { calcAppCount, isAllAppsUnmounted, @@ -16,10 +16,10 @@ import { /** * Just hijack dynamic head append, that could avoid accidentally hijacking the insertion of elements except in head. * Such a case: ReactDOM.createPortal(, 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. + * this could make 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 * @param appWrapperGetter - * @param proxy + * @param sandbox * @param mounting * @param scopedCSS * @param excludeAssetFilter @@ -27,20 +27,22 @@ import { export function patchLooseSandbox( appName: string, appWrapperGetter: () => HTMLElement | ShadowRoot, - proxy: Window, + sandbox: SandBox, mounting = true, scopedCSS = false, excludeAssetFilter?: CallableFunction, ): Freer { + const { proxy } = sandbox; + let dynamicStyleSheetElements: Array = []; 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. + but the url change listener must 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 + and remove them after the url change triggered and qiankun app is unmounting see https://github.com/ReactTraining/history/blob/master/modules/createHashHistory.js#L222-L230 */ () => checkActivityFunctions(window.location).some((name) => name === appName), diff --git a/src/sandbox/patchers/dynamicAppend/forStrictSandbox.ts b/src/sandbox/patchers/dynamicAppend/forStrictSandbox.ts index 0217f05..3fdbdfe 100644 --- a/src/sandbox/patchers/dynamicAppend/forStrictSandbox.ts +++ b/src/sandbox/patchers/dynamicAppend/forStrictSandbox.ts @@ -3,7 +3,8 @@ * @since 2020-10-13 */ -import type { Freer } from '../../../interfaces'; +import { noop } from 'lodash'; +import type { Freer, SandBox } from '../../../interfaces'; import { nativeGlobal } from '../../../utils'; import { getCurrentRunningApp } from '../../common'; import type { ContainerConfig } from './common'; @@ -31,9 +32,48 @@ const proxyAttachContainerConfigMap: WeakMap = const elementAttachContainerConfigMap = new WeakMap(); const docCreatePatchedMap = new WeakMap(); -function patchDocumentCreateElement() { - const docCreateElementFnBeforeOverwrite = docCreatePatchedMap.get(document.createElement); +function patchDocument(cfg: { sandbox: SandBox; speedy: boolean }) { + const { sandbox, speedy } = cfg; + + const attachElementToProxy = (element: HTMLElement, proxy: Window) => { + const proxyContainerConfig = proxyAttachContainerConfigMap.get(proxy); + if (proxyContainerConfig) { + elementAttachContainerConfigMap.set(element, proxyContainerConfig); + } + }; + + if (speedy) { + const proxyDocument = new Proxy(document, { + set: (target, p, value) => { + (target)[p] = value; + return true; + }, + get: (target, p) => { + if (p === 'createElement') { + return (...args: Parameters) => { + const element = document.createElement(...args); + attachElementToProxy(element, sandbox.proxy); + return element; + }; + } + + const value = (target)[p]; + // must rebind the function to the target otherwise it will cause illegal invocation error + if (typeof value === 'function') { + return value.bind(target); + } + + return value; + }, + }); + + sandbox.patchDocument(proxyDocument); + + return noop; + } + + const docCreateElementFnBeforeOverwrite = docCreatePatchedMap.get(document.createElement); if (!docCreateElementFnBeforeOverwrite) { const rawDocumentCreateElement = document.createElement; Document.prototype.createElement = function createElement( @@ -45,10 +85,7 @@ function patchDocumentCreateElement() { if (isHijackingTag(tagName)) { const { window: currentRunningSandboxProxy } = getCurrentRunningApp() || {}; if (currentRunningSandboxProxy) { - const proxyContainerConfig = proxyAttachContainerConfigMap.get(currentRunningSandboxProxy); - if (proxyContainerConfig) { - elementAttachContainerConfigMap.set(element, proxyContainerConfig); - } + attachElementToProxy(element, currentRunningSandboxProxy); } } @@ -74,12 +111,13 @@ function patchDocumentCreateElement() { export function patchStrictSandbox( appName: string, appWrapperGetter: () => HTMLElement | ShadowRoot, - proxy: Window, + sandbox: SandBox, mounting = true, scopedCSS = false, excludeAssetFilter?: CallableFunction, speedySandbox = false, ): Freer { + const { proxy } = sandbox; let containerConfig = proxyAttachContainerConfigMap.get(proxy); if (!containerConfig) { containerConfig = { @@ -97,7 +135,7 @@ export function patchStrictSandbox( // all dynamic style sheets are stored in proxy container const { dynamicStyleSheetElements } = containerConfig; - const unpatchDocumentCreate = patchDocumentCreateElement(); + const unpatchDocumentCreate = patchDocument({ sandbox, speedy: speedySandbox }); const unpatchDynamicAppendPrototypeFunctions = patchHTMLDynamicAppendPrototypeFunctions( (element) => elementAttachContainerConfigMap.has(element), diff --git a/src/sandbox/patchers/index.ts b/src/sandbox/patchers/index.ts index cc60661..34827a6 100644 --- a/src/sandbox/patchers/index.ts +++ b/src/sandbox/patchers/index.ts @@ -28,16 +28,15 @@ export function patchAtMounting( const patchersInSandbox = { [SandBoxType.LegacyProxy]: [ ...basePatchers, - () => patchLooseSandbox(appName, elementGetter, sandbox.proxy, true, scopedCSS, excludeAssetFilter), + () => patchLooseSandbox(appName, elementGetter, sandbox, true, scopedCSS, excludeAssetFilter), ], [SandBoxType.Proxy]: [ ...basePatchers, - () => - patchStrictSandbox(appName, elementGetter, sandbox.proxy, true, scopedCSS, excludeAssetFilter, speedySandBox), + () => patchStrictSandbox(appName, elementGetter, sandbox, true, scopedCSS, excludeAssetFilter, speedySandBox), ], [SandBoxType.Snapshot]: [ ...basePatchers, - () => patchLooseSandbox(appName, elementGetter, sandbox.proxy, true, scopedCSS, excludeAssetFilter), + () => patchLooseSandbox(appName, elementGetter, sandbox, true, scopedCSS, excludeAssetFilter), ], }; @@ -54,14 +53,13 @@ export function patchAtBootstrapping( ): Freer[] { const patchersInSandbox = { [SandBoxType.LegacyProxy]: [ - () => patchLooseSandbox(appName, elementGetter, sandbox.proxy, false, scopedCSS, excludeAssetFilter), + () => patchLooseSandbox(appName, elementGetter, sandbox, false, scopedCSS, excludeAssetFilter), ], [SandBoxType.Proxy]: [ - () => - patchStrictSandbox(appName, elementGetter, sandbox.proxy, false, scopedCSS, excludeAssetFilter, speedySandBox), + () => patchStrictSandbox(appName, elementGetter, sandbox, false, scopedCSS, excludeAssetFilter, speedySandBox), ], [SandBoxType.Snapshot]: [ - () => patchLooseSandbox(appName, elementGetter, sandbox.proxy, false, scopedCSS, excludeAssetFilter), + () => patchLooseSandbox(appName, elementGetter, sandbox, false, scopedCSS, excludeAssetFilter), ], }; diff --git a/src/sandbox/proxySandbox.ts b/src/sandbox/proxySandbox.ts index 83f4b9b..91667eb 100644 --- a/src/sandbox/proxySandbox.ts +++ b/src/sandbox/proxySandbox.ts @@ -7,7 +7,7 @@ import { without } from 'lodash'; import type { SandBox } from '../interfaces'; import { SandBoxType } from '../interfaces'; import { isPropertyFrozen, nativeGlobal, nextTask } from '../utils'; -import { getCurrentRunningApp, getTargetValue, setCurrentRunningApp } from './common'; +import { clearCurrentRunningApp, getCurrentRunningApp, getTargetValue, setCurrentRunningApp } from './common'; import { globals } from './globals'; type SymbolTarget = 'target' | 'globalContext'; @@ -153,7 +153,11 @@ export default class ProxySandbox implements SandBox { type: SandBoxType; proxy: WindowProxy; + sandboxRunning = true; + + private document = document; + latestSetProp: PropertyKey | null = null; active() { @@ -269,7 +273,7 @@ export default class ProxySandbox implements SandBox { } if (p === 'document') { - return document; + return this.document; } if (p === 'eval') { @@ -316,7 +320,7 @@ export default class ProxySandbox implements SandBox { 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 + // A property cannot be reported as non-configurable, if it does not exist as an own property of the target object if (descriptor && !descriptor.configurable) { descriptor.configurable = true; } @@ -331,7 +335,7 @@ export default class ProxySandbox implements SandBox { return uniq(Reflect.ownKeys(globalContext).concat(Reflect.ownKeys(target))); }, - defineProperty(target: Window, p: PropertyKey, attributes: PropertyDescriptor): boolean { + 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), @@ -369,6 +373,10 @@ export default class ProxySandbox implements SandBox { activeSandboxCount++; } + public patchDocument(doc: Document) { + this.document = doc; + } + private registerRunningApp(name: string, proxy: Window) { if (this.sandboxRunning) { const currentRunningApp = getCurrentRunningApp(); @@ -378,9 +386,7 @@ export default class ProxySandbox implements SandBox { // 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); - }); + nextTask(clearCurrentRunningApp); } } } diff --git a/src/sandbox/snapshotSandbox.ts b/src/sandbox/snapshotSandbox.ts index 3d22055..aba46a6 100644 --- a/src/sandbox/snapshotSandbox.ts +++ b/src/sandbox/snapshotSandbox.ts @@ -69,4 +69,6 @@ export default class SnapshotSandbox implements SandBox { this.sandboxRunning = false; } + + patchDocument(): void {} }