From 3d1d336719f1b1089247c1ade0cc8f38ba2826bb Mon Sep 17 00:00:00 2001 From: Kuitos Date: Mon, 23 Oct 2023 10:07:50 -0500 Subject: [PATCH] fix: should patch the container head/body element immediately rather than patch its functions with proxy (#2752) --- .changeset/twelve-donkeys-help.md | 5 + .../src/patchers/dynamicAppend/common.ts | 84 +++---- .../dynamicAppend/forStandardSandbox.ts | 214 ++++++------------ .../src/patchers/dynamicAppend/types.ts | 1 - 4 files changed, 103 insertions(+), 201 deletions(-) create mode 100644 .changeset/twelve-donkeys-help.md diff --git a/.changeset/twelve-donkeys-help.md b/.changeset/twelve-donkeys-help.md new file mode 100644 index 000000000..e3bab9a0d --- /dev/null +++ b/.changeset/twelve-donkeys-help.md @@ -0,0 +1,5 @@ +--- +"@qiankunjs/sandbox": patch +--- + +fix: should patch the container head/body element immediately rather than patch its functions with proxy diff --git a/packages/sandbox/src/patchers/dynamicAppend/common.ts b/packages/sandbox/src/patchers/dynamicAppend/common.ts index b559dfaab..671a43e5b 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 { QiankunError, transpileAssets } from '@qiankunjs/shared'; +import { transpileAssets } from '@qiankunjs/shared'; import { qiankunHeadTagName } from '../../consts'; import type { SandboxConfig } from './types'; @@ -52,7 +52,7 @@ export function isHijackingTag(tagName?: string) { * @param element */ export function isStyledComponentsLike(element: HTMLStyleElement): boolean { - return !element.textContent && (element.sheet?.cssRules.length || getStyledElementCSSRules(element)?.length); + return Boolean(!element.textContent && (element.sheet?.cssRules.length || getStyledElementCSSRules(element)?.length)); } const appsCounterMap = new Map(); @@ -93,8 +93,6 @@ const defineNonEnumerableProperty = (target: unknown, key: string | symbol, valu }; const styledComponentCSSRulesMap = new WeakMap(); -const dynamicScriptAttachedCommentMap = new WeakMap(); -const dynamicLinkAttachedInlineStyleMap = new WeakMap(); export function recordStyledComponentsCSSRules(styleElements: HTMLStyleElement[]): void { styleElements.forEach((styleElement) => { @@ -117,32 +115,26 @@ export function getStyledElementCSSRules(styledElement: HTMLStyleElement): CSSRu } export function getOverwrittenAppendChildOrInsertBefore( - opType: 'appendChild' | 'insertBefore', + nativeFn: typeof HTMLElement.prototype.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 appendChild = this[opType]; + const appendChild = nativeFn; const element = newChild as unknown as HTMLElement; - if (!isHijackingTag(element.tagName) || !isInvokedByMicroApp(element)) { + const containerConfig = getSandboxConfig(element); + + if (!isHijackingTag(element.tagName) || !containerConfig) { 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; + const { dynamicStyleSheetElements, sandbox } = containerConfig; switch (element.tagName) { case LINK_TAG_NAME: @@ -154,16 +146,13 @@ export function getOverwrittenAppendChildOrInsertBefore( configurable: true, }); - const container = getContainer(); - const mountDOM = target === 'head' ? getContainerHeadElement(container) : container; - - const referenceNode = mountDOM.contains(refChild) ? refChild : null; + const referenceNode = this.contains(refChild) ? refChild : null; let refNo: number | undefined; if (referenceNode) { - refNo = Array.from(mountDOM.childNodes).indexOf(referenceNode as ChildNode); + refNo = Array.from(this.childNodes).indexOf(referenceNode as ChildNode); } - const result = appendChild.call(mountDOM, stylesheetElement, referenceNode); + const result = appendChild.call(this, stylesheetElement, referenceNode); // record refNo thus we can keep order while remounting if (typeof refNo === 'number' && refNo !== -1) { @@ -171,18 +160,15 @@ export function getOverwrittenAppendChildOrInsertBefore( } // record dynamic style elements after insert succeed dynamicStyleSheetElements.push(stylesheetElement); + return result as T; } case SCRIPT_TAG_NAME: { - const container = getContainer(); - const mountDOM = target === 'head' ? getContainerHeadElement(container) : container; - const referenceNode = mountDOM.contains(refChild) ? refChild : null; - // TODO paas fetch configuration and current entry url as baseURI const node = transpileAssets(element, location.href, { fetch, sandbox, rawNode: element }); - return appendChild.call(mountDOM, node, referenceNode) as T; + return appendChild.call(this, node, refChild) as T; } default: @@ -199,37 +185,30 @@ export function getOverwrittenAppendChildOrInsertBefore( } export function getNewRemoveChild( - opType: 'removeChild', + nativeFn: typeof HTMLElement.prototype.removeChild, containerConfigGetter: (element: HTMLElement) => SandboxConfig | undefined, - target: DynamicDomMutationTarget, - isInvokedByMicroApp: (element: HTMLElement) => boolean, ) { - function removeChildInSandbox(this: HTMLHeadElement | HTMLBodyElement, child: T) { - const removeChild = this[opType]; + function removeChildInSandbox(this: HTMLHeadElement | HTMLBodyElement, child: T): T { + const removeChild = nativeFn; const childElement = child as unknown as HTMLElement; const { tagName } = childElement; - if (!isHijackingTag(tagName) || !isInvokedByMicroApp(childElement)) { - return removeChild.call(this, child) as T; + const containerConfig = containerConfigGetter(childElement); + + if (!isHijackingTag(tagName) || !containerConfig) { + return removeChild.call(this, childElement) as T; } try { - let attachedElement: Node; - - 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; + const { dynamicStyleSheetElements } = containerConfig; switch (tagName) { case STYLE_TAG_NAME: case LINK_TAG_NAME: { - attachedElement = dynamicLinkAttachedInlineStyleMap.get(childElement as unknown as HTMLLinkElement) || child; - // try to remove the dynamic style sheet - const dynamicElementIndex = dynamicStyleSheetElements.indexOf(attachedElement as HTMLLinkElement); + const dynamicElementIndex = dynamicStyleSheetElements.indexOf( + childElement as HTMLLinkElement | HTMLStyleElement, + ); if (dynamicElementIndex !== -1) { dynamicStyleSheetElements.splice(dynamicElementIndex, 1); } @@ -237,27 +216,20 @@ export function getNewRemoveChild( break; } - case SCRIPT_TAG_NAME: { - attachedElement = dynamicScriptAttachedCommentMap.get(childElement as unknown as HTMLScriptElement) || child; - break; - } - default: { - attachedElement = child; + break; } } - const container = getContainer(); - 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 removeChild.call(attachedElement.parentNode, attachedElement) as T; + if (this.contains(childElement)) { + return removeChild.call(this, childElement) as T; } } catch (e) { console.warn(e); } - return removeChild.call(this, child) as T; + return removeChild.call(this, childElement) as T; } removeChildInSandbox[overwrittenSymbol] = true; diff --git a/packages/sandbox/src/patchers/dynamicAppend/forStandardSandbox.ts b/packages/sandbox/src/patchers/dynamicAppend/forStandardSandbox.ts index cf9ff89ec..1c5190967 100644 --- a/packages/sandbox/src/patchers/dynamicAppend/forStandardSandbox.ts +++ b/packages/sandbox/src/patchers/dynamicAppend/forStandardSandbox.ts @@ -1,11 +1,11 @@ -/* eslint-disable @typescript-eslint/unbound-method */ +/* eslint-disable */ /** * @author Kuitos * @since 2020-10-13 */ import type { noop } from 'lodash'; -import { nativeDocument, nativeGlobal } from '../../consts'; +import { nativeDocument, nativeGlobal, qiankunHeadTagName } from '../../consts'; import { rebindTarget2Fn } from '../../core/membrane/utils'; import type { Sandbox } from '../../core/sandbox'; import type { Free } from '../types'; @@ -55,23 +55,12 @@ const elementAttachSandboxConfigMap = new WeakMap(); const patchCacheWeakMap = new WeakMap(); const getSandboxConfig = (element: HTMLElement) => elementAttachSandboxConfigMap.get(element); -const isInvokedByMicroApp = (element: HTMLElement) => elementAttachSandboxConfigMap.has(element); -function patchDocument(sandbox: Sandbox): void { - if (patchCacheWeakMap.has(sandbox)) { - return; +function patchDocument(sandbox: Sandbox, container: HTMLElement): CallableFunction { + if (patchCacheWeakMap.has(container)) { + return () => {}; } - const proxyDocumentFnsCache = new Map< - | 'appendChildOnHead' - | 'insertBeforeOnHead' - | 'removeChildOnHead' - | 'appendChildOnBody' - | 'insertBeforeOnBody' - | 'removeChildOnBody', - CallableFunction - >(); - const attachElementToSandbox = (element: HTMLElement) => { const sandboxConfig = sandboxConfigWeakMap.get(sandbox); if (sandboxConfig) { @@ -109,122 +98,11 @@ function patchDocument(sandbox: Sandbox): void { } case 'head': { - const headElement = target.head; - return new Proxy(headElement, { - set: (headElementTarget, p, value) => { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - headElementTarget[p] = value; - return true; - }, - get(headElementTarget, p, headReceiver) { - switch (p) { - case 'appendChild': { - let cachedAppendChild = proxyDocumentFnsCache.get('appendChildOnHead'); - if (!cachedAppendChild) { - cachedAppendChild = getOverwrittenAppendChildOrInsertBefore( - 'appendChild', - getSandboxConfig, - 'head', - isInvokedByMicroApp, - ).bind(headElementTarget); - proxyDocumentFnsCache.set('appendChildOnHead', cachedAppendChild); - } - return cachedAppendChild; - } - - case 'insertBefore': { - let cachedInsertBefore = proxyDocumentFnsCache.get('insertBeforeOnHead'); - if (!cachedInsertBefore) { - cachedInsertBefore = getOverwrittenAppendChildOrInsertBefore( - 'insertBefore', - getSandboxConfig, - 'head', - isInvokedByMicroApp, - ).bind(headElementTarget); - proxyDocumentFnsCache.set('insertBeforeOnHead', cachedInsertBefore); - } - return cachedInsertBefore; - } - - case 'removeChild': { - let cachedRemoveChild = proxyDocumentFnsCache.get('removeChildOnHead'); - if (!cachedRemoveChild) { - cachedRemoveChild = getNewRemoveChild( - 'removeChild', - getSandboxConfig, - 'head', - isInvokedByMicroApp, - ).bind(headElementTarget); - proxyDocumentFnsCache.set('removeChildOnHead', cachedRemoveChild); - } - return cachedRemoveChild; - } - - default: { - const value = headElementTarget[p as keyof HTMLHeadElement]; - return rebindTarget2Fn(headElementTarget, value, headReceiver); - } - } - }, - }); + return container.querySelector(qiankunHeadTagName) as HTMLHeadElement; } case 'body': { - const bodyElement = target.body; - return new Proxy(bodyElement, { - get(bodyElementTarget, p, bodyReceiver) { - switch (p) { - case 'appendChild': { - let cachedAppendChild = proxyDocumentFnsCache.get('appendChildOnBody'); - if (!cachedAppendChild) { - cachedAppendChild = getOverwrittenAppendChildOrInsertBefore( - 'appendChild', - getSandboxConfig, - 'body', - isInvokedByMicroApp, - ).bind(bodyElementTarget); - proxyDocumentFnsCache.set('appendChildOnBody', cachedAppendChild); - } - return cachedAppendChild; - } - - case 'insertBefore': { - let cachedInsertBefore = proxyDocumentFnsCache.get('insertBeforeOnBody'); - if (!cachedInsertBefore) { - cachedInsertBefore = getOverwrittenAppendChildOrInsertBefore( - 'insertBefore', - getSandboxConfig, - 'body', - isInvokedByMicroApp, - ).bind(bodyElementTarget); - proxyDocumentFnsCache.set('insertBeforeOnBody', cachedInsertBefore); - } - return cachedInsertBefore; - } - - case 'removeChild': { - let cachedRemoveChild = proxyDocumentFnsCache.get('removeChildOnBody'); - if (!cachedRemoveChild) { - cachedRemoveChild = getNewRemoveChild( - 'removeChild', - getSandboxConfig, - 'body', - isInvokedByMicroApp, - ).bind(bodyElementTarget); - proxyDocumentFnsCache.set('removeChildOnBody', cachedRemoveChild); - } - return cachedRemoveChild; - } - - default: { - const value = bodyElementTarget[p as keyof HTMLHeadElement]; - return rebindTarget2Fn(bodyElementTarget, value, bodyReceiver); - } - } - }, - }); + return container as HTMLBodyElement; } case 'querySelector': { @@ -233,18 +111,11 @@ function patchDocument(sandbox: Sandbox): void { const selector = args[0]; switch (selector) { case 'head': { - const containerConfig = sandboxConfigWeakMap.get(sandbox); - if (containerConfig) { - const qiankunHead = getContainerHeadElement(containerConfig.getContainer()); - - // proxied head in micro app should use the proxied appendChild/removeChild/insertBefore methods - qiankunHead.appendChild = proxyDocument.head.appendChild; - qiankunHead.insertBefore = proxyDocument.head.insertBefore; - qiankunHead.removeChild = proxyDocument.head.removeChild; - - return qiankunHead; - } - break; + return getContainerHeadElement(container); + } + + case 'body': { + return container; } } @@ -261,10 +132,63 @@ function patchDocument(sandbox: Sandbox): void { }, }); + /* + * patch container head element after it is mounted + */ + const observer = new MutationObserver(() => { + const containerHeadElement = container.querySelector(qiankunHeadTagName); + if (containerHeadElement) { + containerHeadElement.appendChild = getOverwrittenAppendChildOrInsertBefore( + document.head.appendChild, + getSandboxConfig, + 'head', + ); + containerHeadElement.insertBefore = getOverwrittenAppendChildOrInsertBefore( + document.head.insertBefore, + getSandboxConfig, + 'head', + ); + containerHeadElement.removeChild = getNewRemoveChild(document.head.removeChild, getSandboxConfig); + + observer.disconnect(); + } + }); + observer.observe(container, { subtree: true, childList: true }); + + const containerBodyElement = container; + containerBodyElement.appendChild = getOverwrittenAppendChildOrInsertBefore( + document.body.appendChild, + getSandboxConfig, + 'body', + ); + containerBodyElement.insertBefore = getOverwrittenAppendChildOrInsertBefore( + document.head.insertBefore, + getSandboxConfig, + 'body', + ); + containerBodyElement.removeChild = getNewRemoveChild(document.body.removeChild, getSandboxConfig); + sandbox.addIntrinsics({ document: { value: proxyDocument, writable: false, enumerable: true, configurable: true }, }); - patchCacheWeakMap.set(sandbox, true); + + patchCacheWeakMap.set(container, true); + + return () => { + const containerHeadElement = getContainerHeadElement(container); + // @ts-ignore + delete containerHeadElement.appendChild; + // @ts-ignore + delete containerHeadElement.insertBefore; + // @ts-ignore + delete containerHeadElement.removeChild; + // @ts-ignore + delete container.appendChild; + // @ts-ignore + delete container.insertBefore; + // @ts-ignore + delete container.removeChild; + }; } function patchDOMPrototypeFns(): typeof noop { @@ -350,7 +274,6 @@ export function patchStandardSandbox( sandboxConfig = { appName, sandbox, - getContainer, dynamicStyleSheetElements: [], }; sandboxConfigWeakMap.set(sandbox, sandboxConfig); @@ -358,7 +281,7 @@ export function patchStandardSandbox( // all dynamic style sheets are stored in proxy container const { dynamicStyleSheetElements } = sandboxConfig; - patchDocument(sandbox); + const unpatchDocument = patchDocument(sandbox, getContainer()); const unpatchDOMPrototype = patchDOMPrototypeFns(); if (!mounting) calcAppCount(appName, 'increase', 'bootstrapping'); @@ -368,6 +291,9 @@ export function patchStandardSandbox( if (!mounting) calcAppCount(appName, 'decrease', 'bootstrapping'); if (mounting) calcAppCount(appName, 'decrease', 'mounting'); + // release the overwritten document + unpatchDocument(); + // release the overwritten prototype after all the micro apps unmounted if (isAllAppsUnmounted()) { unpatchDOMPrototype(); diff --git a/packages/sandbox/src/patchers/dynamicAppend/types.ts b/packages/sandbox/src/patchers/dynamicAppend/types.ts index 6ddbf7f7d..1affb4595 100644 --- a/packages/sandbox/src/patchers/dynamicAppend/types.ts +++ b/packages/sandbox/src/patchers/dynamicAppend/types.ts @@ -8,5 +8,4 @@ export type SandboxConfig = { appName: string; sandbox: Sandbox; dynamicStyleSheetElements: Array; - getContainer: () => HTMLElement; };