diff --git a/packages/sandbox/src/core/membrane/index.ts b/packages/sandbox/src/core/membrane/index.ts index da689894b..79fe2c619 100644 --- a/packages/sandbox/src/core/membrane/index.ts +++ b/packages/sandbox/src/core/membrane/index.ts @@ -124,7 +124,7 @@ export class Membrane { return true; }, - get: (membraneTarget, p) => { + get: (membraneTarget, p, receiver) => { if (p === Symbol.unscopables) return unscopables; // properties in endowments returns directly @@ -155,7 +155,7 @@ export class Membrane { proxyFetch('https://qiankun.com'); */ const boundTarget = useNativeWindowForBindingsProps.get(p) ? nativeGlobal : incubatorContext; - return getTargetValue(boundTarget, value); + return getTargetValue(boundTarget, value, receiver); }, // trap in operator diff --git a/packages/sandbox/src/core/membrane/utils.ts b/packages/sandbox/src/core/membrane/utils.ts index 96556dbc0..b7e1604fa 100644 --- a/packages/sandbox/src/core/membrane/utils.ts +++ b/packages/sandbox/src/core/membrane/utils.ts @@ -3,7 +3,7 @@ import { isBoundedFunction, isCallable, isConstructable } from '../../utils'; const functionBoundedValueMap = new WeakMap(); -export function getTargetValue(target: unknown, value: T): T { +export function getTargetValue(target: unknown, value: T, receiver: unknown): T { /* 仅绑定 isCallable && !isBoundedFunction && !isConstructable 的函数对象,如 window.console、window.atob 这类,不然微应用中调用时会抛出 Illegal invocation 异常 目前没有完美的检测方式,这里通过 prototype 中是否还有可枚举的拓展方法的方式来判断 @@ -16,7 +16,13 @@ export function getTargetValue(target: unknown, value: T): T { return cachedBoundFunction as T; } - const boundValue = Function.prototype.bind.call(typedValue, target) as CallableFunction; + const boundValue = function proxyFunction(...args: unknown[]): unknown { + return Function.prototype.apply.call( + typedValue, + target, + args.map((arg) => (arg === receiver ? target : arg)), + ); + }; // some callable function has custom fields, we need to copy the own props to boundValue. such as moment function. getOwnPropertyNames(typedValue).forEach((key) => { diff --git a/packages/sandbox/src/patchers/dynamicAppend/common.ts b/packages/sandbox/src/patchers/dynamicAppend/common.ts index 4aa6af549..940ea71c0 100644 --- a/packages/sandbox/src/patchers/dynamicAppend/common.ts +++ b/packages/sandbox/src/patchers/dynamicAppend/common.ts @@ -3,7 +3,7 @@ * @author Kuitos * @since 2019-10-21 */ -import { transpileAssets } from '@qiankunjs/shared'; +import { QiankunError, transpileAssets } from '@qiankunjs/shared'; import { qiankunHeadTagName } from '../../consts'; import type { SandboxConfig } from './types'; @@ -54,7 +54,7 @@ export function isHijackingTag(tagName?: string) { export function isStyledComponentsLike(element: HTMLStyleElement) { return ( !element.textContent && - ((element.sheet as CSSStyleSheet)?.cssRules.length || getStyledElementCSSRules(element)?.length) + ((element.sheet as CSSStyleSheet).cssRules.length || getStyledElementCSSRules(element)?.length) ); } @@ -119,25 +119,30 @@ export function getStyledElementCSSRules(styledElement: HTMLStyleElement): CSSRu return styledComponentCSSRulesMap.get(styledElement); } -function getOverwrittenAppendChildOrInsertBefore(opts: { - rawDOMAppendOrInsertBefore: typeof HTMLElement.prototype.insertBefore; - isInvokedByMicroApp: (element: HTMLElement) => boolean; - getSandboxConfig: (element: HTMLElement) => SandboxConfig; - target: DynamicDomMutationTarget; -}) { - function appendChildOrInsertBefore( +export function getOverwrittenAppendChildOrInsertBefore( + appendChild: typeof HTMLElement.prototype.insertBefore, + getSandboxConfig: (element: HTMLElement) => SandboxConfig | undefined, + target: DynamicDomMutationTarget = 'body', + isInvokedByMicroApp: (element: HTMLElement) => boolean, +) { + function appendChildInSandbox( this: HTMLHeadElement | HTMLBodyElement, newChild: T, refChild: Node | null = null, - ) { + ): T { const element = newChild as unknown as HTMLElement; - const { rawDOMAppendOrInsertBefore, isInvokedByMicroApp, getSandboxConfig, target = 'body' } = opts; if (!isHijackingTag(element.tagName) || !isInvokedByMicroApp(element)) { - return rawDOMAppendOrInsertBefore.call(this, element, refChild) as T; + return appendChild.call(this, element, refChild) as T; } if (element.tagName) { const containerConfig = getSandboxConfig(element); + if (!containerConfig) { + throw new QiankunError( + `You haven't set the container for ${element.tagName} element while calling appendChild/insertBefore!`, + ); + } + const { getContainer, dynamicStyleSheetElements, sandbox } = containerConfig; switch (element.tagName) { @@ -159,7 +164,7 @@ function getOverwrittenAppendChildOrInsertBefore(opts: { refNo = Array.from(mountDOM.childNodes).indexOf(referenceNode as ChildNode); } - const result = rawDOMAppendOrInsertBefore.call(mountDOM, stylesheetElement, referenceNode); + const result = appendChild.call(mountDOM, stylesheetElement, referenceNode); // record refNo thus we can keep order while remounting if (typeof refNo === 'number' && refNo !== -1) { @@ -167,7 +172,7 @@ function getOverwrittenAppendChildOrInsertBefore(opts: { } // record dynamic style elements after insert succeed dynamicStyleSheetElements.push(stylesheetElement); - return result; + return result as T; } case SCRIPT_TAG_NAME: { @@ -178,7 +183,7 @@ function getOverwrittenAppendChildOrInsertBefore(opts: { // TODO paas fetch configuration and current entry url as baseURI const node = transpileAssets(element, location.href, { fetch, sandbox, rawNode: element }); - return rawDOMAppendOrInsertBefore.call(mountDOM, node, referenceNode); + return appendChild.call(mountDOM, node, referenceNode) as T; } default: @@ -186,28 +191,36 @@ function getOverwrittenAppendChildOrInsertBefore(opts: { } } - return rawDOMAppendOrInsertBefore.call(this, element, refChild); + return appendChild.call(this, element, refChild) as T; } - appendChildOrInsertBefore[overwrittenSymbol] = true; + appendChildInSandbox[overwrittenSymbol] = true; - return appendChildOrInsertBefore; + return appendChildInSandbox; } -function getNewRemoveChild( - rawRemoveChild: typeof HTMLElement.prototype.removeChild, - containerConfigGetter: (element: HTMLElement) => SandboxConfig, +export function getNewRemoveChild( + removeChild: typeof HTMLElement.prototype.removeChild, + containerConfigGetter: (element: HTMLElement) => SandboxConfig | undefined, target: DynamicDomMutationTarget, isInvokedByMicroApp: (element: HTMLElement) => boolean, ) { - function removeChild(this: HTMLHeadElement | HTMLBodyElement, child: T) { + function removeChildInSandbox(this: HTMLHeadElement | HTMLBodyElement, child: T) { const childElement = child as unknown as HTMLElement; const { tagName } = childElement; - if (!isHijackingTag(tagName) || !isInvokedByMicroApp(childElement)) return rawRemoveChild.call(this, child) as T; + if (!isHijackingTag(tagName) || !isInvokedByMicroApp(childElement)) { + return removeChild.call(this, child) as T; + } try { let attachedElement: Node; - const { getContainer, dynamicStyleSheetElements } = containerConfigGetter(childElement); + + const containerConfig = containerConfigGetter(childElement); + if (!containerConfig) { + throw new QiankunError(`You haven't set the container for ${tagName} element while calling removeChild!`); + } + + const { getContainer, dynamicStyleSheetElements } = containerConfig; switch (tagName) { case STYLE_TAG_NAME: @@ -237,80 +250,17 @@ function getNewRemoveChild( const mountDOM = target === 'head' ? getContainerHeadElement(container) : container; // container might have been removed while app unmounting if the removeChild action was async if (mountDOM.contains(attachedElement)) { - return rawRemoveChild.call(attachedElement.parentNode, attachedElement) as T; + return removeChild.call(attachedElement.parentNode, attachedElement) as T; } } catch (e) { console.warn(e); } - return rawRemoveChild.call(this, child) as T; + return removeChild.call(this, child) as T; } - removeChild[overwrittenSymbol] = true; - return removeChild; -} - -export function patchHTMLDynamicAppendPrototypeFunctions( - isInvokedByMicroApp: (element: HTMLElement) => boolean, - getSandboxConfig: (element: HTMLElement) => SandboxConfig, -) { - const rawHeadAppendChild = HTMLHeadElement.prototype.appendChild; - const rawBodyAppendChild = HTMLBodyElement.prototype.appendChild; - const rawHeadInsertBefore = HTMLHeadElement.prototype.insertBefore; - - // Just overwrite it while it have not been overwritten - if ( - rawHeadAppendChild[overwrittenSymbol] !== true && - rawBodyAppendChild[overwrittenSymbol] !== true && - rawHeadInsertBefore[overwrittenSymbol] !== true - ) { - HTMLHeadElement.prototype.appendChild = getOverwrittenAppendChildOrInsertBefore({ - rawDOMAppendOrInsertBefore: rawHeadAppendChild, - getSandboxConfig: getSandboxConfig, - isInvokedByMicroApp, - target: 'head', - }) as typeof rawHeadAppendChild; - HTMLBodyElement.prototype.appendChild = getOverwrittenAppendChildOrInsertBefore({ - rawDOMAppendOrInsertBefore: rawBodyAppendChild, - getSandboxConfig: getSandboxConfig, - isInvokedByMicroApp, - target: 'body', - }) as typeof rawBodyAppendChild; - - HTMLHeadElement.prototype.insertBefore = getOverwrittenAppendChildOrInsertBefore({ - rawDOMAppendOrInsertBefore: rawHeadInsertBefore, - getSandboxConfig: getSandboxConfig, - isInvokedByMicroApp, - target: 'head', - }) as typeof rawHeadInsertBefore; - } - - const rawHeadRemoveChild = HTMLHeadElement.prototype.removeChild; - const rawBodyRemoveChild = HTMLBodyElement.prototype.removeChild; - // Just overwrite it while it have not been overwritten - if (rawHeadRemoveChild[overwrittenSymbol] !== true && rawBodyRemoveChild[overwrittenSymbol] !== true) { - HTMLHeadElement.prototype.removeChild = getNewRemoveChild( - rawHeadRemoveChild, - getSandboxConfig, - 'head', - isInvokedByMicroApp, - ); - HTMLBodyElement.prototype.removeChild = getNewRemoveChild( - rawBodyRemoveChild, - getSandboxConfig, - 'body', - isInvokedByMicroApp, - ); - } - - return function unpatch() { - HTMLHeadElement.prototype.appendChild = rawHeadAppendChild; - HTMLHeadElement.prototype.removeChild = rawHeadRemoveChild; - HTMLBodyElement.prototype.appendChild = rawBodyAppendChild; - HTMLBodyElement.prototype.removeChild = rawBodyRemoveChild; - - HTMLHeadElement.prototype.insertBefore = rawHeadInsertBefore; - }; + removeChildInSandbox[overwrittenSymbol] = true; + return removeChildInSandbox; } export function rebuildCSSRules( diff --git a/packages/sandbox/src/patchers/dynamicAppend/forStandardSandbox.ts b/packages/sandbox/src/patchers/dynamicAppend/forStandardSandbox.ts index 46e8c0fbf..a11cf99d6 100644 --- a/packages/sandbox/src/patchers/dynamicAppend/forStandardSandbox.ts +++ b/packages/sandbox/src/patchers/dynamicAppend/forStandardSandbox.ts @@ -4,15 +4,17 @@ * @since 2020-10-13 */ +import type { noop } from 'lodash'; import { nativeDocument, nativeGlobal } from '../../consts'; +import { getTargetValue } from '../../core/membrane/utils'; import type { Sandbox } from '../../core/sandbox'; -import { isBoundedFunction, isCallable } from '../../utils'; import type { Free } from '../types'; import { calcAppCount, getContainerHeadElement, + getNewRemoveChild, + getOverwrittenAppendChildOrInsertBefore, isAllAppsUnmounted, - patchHTMLDynamicAppendPrototypeFunctions, rebuildCSSRules, recordStyledComponentsCSSRules, styleElementRefNodeNo, @@ -55,7 +57,10 @@ const sandboxConfigWeakMap = nativeGlobal.__sandboxConfigWeakMap__; const elementAttachSandboxConfigMap = new WeakMap(); const patchMap = new WeakMap(); -function patchDocument(sandbox: Sandbox) { +const getSandboxConfig = (element: HTMLElement) => elementAttachSandboxConfigMap.get(element); +const isInvokedByMicroApp = (element: HTMLElement) => elementAttachSandboxConfigMap.has(element); + +function patchDocument(sandbox: Sandbox): void { const attachElementToSandbox = (element: HTMLElement) => { const sandboxConfig = sandboxConfigWeakMap.get(sandbox); if (sandboxConfig) { @@ -65,7 +70,7 @@ function patchDocument(sandbox: Sandbox) { const proxyDocument = new Proxy(document, { set: (target, p, value) => { - target[p as string] = value; + target[p as keyof Document] = value; return true; }, get: (target, p, receiver) => { @@ -92,6 +97,86 @@ function patchDocument(sandbox: Sandbox) { }; } + case 'head': { + const headElement = target.head; + return new Proxy(headElement, { + get(headElementTarget, p, headReceiver) { + switch (p) { + case 'appendChild': { + const appendChild = headElementTarget.appendChild; + return getOverwrittenAppendChildOrInsertBefore( + appendChild, + getSandboxConfig, + 'head', + isInvokedByMicroApp, + ).bind(headElementTarget); + } + case 'insertBefore': { + const insertBefore = headElementTarget.insertBefore; + return getOverwrittenAppendChildOrInsertBefore( + insertBefore, + getSandboxConfig, + 'head', + isInvokedByMicroApp, + ).bind(headElementTarget); + } + + case 'removeChild': { + const removeChild = headElementTarget.removeChild; + return getNewRemoveChild(removeChild, getSandboxConfig, 'head', isInvokedByMicroApp).bind( + headElementTarget, + ); + } + + default: { + const value = headElementTarget[p as keyof HTMLHeadElement]; + return getTargetValue(headElementTarget, value, headReceiver); + } + } + }, + }); + } + + case 'body': { + const bodyElement = target.body; + return new Proxy(bodyElement, { + get(bodyElementTarget, p, bodyReceiver) { + switch (p) { + case 'appendChild': { + const appendChild = bodyElementTarget.appendChild; + return getOverwrittenAppendChildOrInsertBefore( + appendChild, + getSandboxConfig, + 'body', + isInvokedByMicroApp, + ).bind(bodyElementTarget); + } + case 'insertBefore': { + const insertBefore = bodyElementTarget.insertBefore; + return getOverwrittenAppendChildOrInsertBefore( + insertBefore, + getSandboxConfig, + 'body', + isInvokedByMicroApp, + ).bind(bodyElementTarget); + } + + case 'removeChild': { + const removeChild = bodyElementTarget.removeChild; + return getNewRemoveChild(removeChild, getSandboxConfig, 'body', isInvokedByMicroApp).bind( + bodyElementTarget, + ); + } + + default: { + const value = bodyElementTarget[p as keyof HTMLHeadElement]; + return getTargetValue(bodyElementTarget, value, bodyReceiver); + } + } + }, + }); + } + case 'querySelector': { const targetQuerySelector = target.querySelector; return function querySelector(...args: Parameters) { @@ -122,22 +207,19 @@ function patchDocument(sandbox: Sandbox) { const value = target[p as string]; // must rebind the function to the target otherwise it will cause illegal invocation error - if (isCallable(value) && !isBoundedFunction(value)) { - return function proxyFunction(...args: unknown[]): unknown { - return Function.prototype.apply.call( - value, - target, - args.map((arg) => (arg === receiver ? target : arg)), - ); - }; - } - - return value; + return getTargetValue(target, value, receiver); }, }); - sandbox.addIntrinsics({ document: { value: proxyDocument, writable: false, enumerable: true, configurable: true } }); + if (!patchMap.has(sandbox)) { + sandbox.addIntrinsics({ + document: { value: proxyDocument, writable: false, enumerable: true, configurable: true }, + }); + patchMap.set(sandbox, true); + } +} +function patchDOMPrototypeFns(): typeof noop { // patch MutationObserver.prototype.observe to avoid type error // https://github.com/umijs/qiankun/issues/2406 const nativeMutationObserverObserveFn = MutationObserver.prototype.observe; @@ -216,7 +298,7 @@ export function patchStandardSandbox( sandboxConfig = { appName, sandbox, - getContainer: getContainer, + getContainer, dynamicStyleSheetElements: [], }; sandboxConfigWeakMap.set(sandbox, sandboxConfig); @@ -224,12 +306,8 @@ export function patchStandardSandbox( // all dynamic style sheets are stored in proxy container const { dynamicStyleSheetElements } = sandboxConfig; - const unpatchDynamicAppendPrototypeFunctions = patchHTMLDynamicAppendPrototypeFunctions( - (element) => elementAttachSandboxConfigMap.has(element), - (element) => elementAttachSandboxConfigMap.get(element)!, - ); - - const unpatchDocument = patchDocument(sandbox); + patchDocument(sandbox); + const unpatchDOMPrototype = patchDOMPrototypeFns(); if (!mounting) calcAppCount(appName, 'increase', 'bootstrapping'); if (mounting) calcAppCount(appName, 'increase', 'mounting'); @@ -240,8 +318,7 @@ export function patchStandardSandbox( // release the overwritten prototype after all the micro apps unmounted if (isAllAppsUnmounted()) { - unpatchDynamicAppendPrototypeFunctions(); - unpatchDocument(); + unpatchDOMPrototype(); } recordStyledComponentsCSSRules(dynamicStyleSheetElements as HTMLStyleElement[]);