Skip to content

Commit

Permalink
feat: set proxy appendChild/insertBefore method for every sandbox rat…
Browse files Browse the repository at this point in the history
…her than modify prototype on HTMLElement
  • Loading branch information
kuitos committed Oct 19, 2023
1 parent 76b6bff commit d13d99a
Show file tree
Hide file tree
Showing 4 changed files with 153 additions and 120 deletions.
4 changes: 2 additions & 2 deletions packages/sandbox/src/core/membrane/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 8 additions & 2 deletions packages/sandbox/src/core/membrane/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { isBoundedFunction, isCallable, isConstructable } from '../../utils';

const functionBoundedValueMap = new WeakMap<CallableFunction, CallableFunction>();

export function getTargetValue<T>(target: unknown, value: T): T {
export function getTargetValue<T>(target: unknown, value: T, receiver: unknown): T {
/*
仅绑定 isCallable && !isBoundedFunction && !isConstructable 的函数对象,如 window.console、window.atob 这类,不然微应用中调用时会抛出 Illegal invocation 异常
目前没有完美的检测方式,这里通过 prototype 中是否还有可枚举的拓展方法的方式来判断
Expand All @@ -16,7 +16,13 @@ export function getTargetValue<T>(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) => {
Expand Down
132 changes: 41 additions & 91 deletions packages/sandbox/src/patchers/dynamicAppend/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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)
);
}

Expand Down Expand Up @@ -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<T extends Node>(
export function getOverwrittenAppendChildOrInsertBefore(
appendChild: typeof HTMLElement.prototype.insertBefore,
getSandboxConfig: (element: HTMLElement) => SandboxConfig | undefined,
target: DynamicDomMutationTarget = 'body',
isInvokedByMicroApp: (element: HTMLElement) => boolean,
) {
function appendChildInSandbox<T extends Node>(
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) {
Expand All @@ -159,15 +164,15 @@ 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) {
defineNonEnumerableProperty(stylesheetElement, styleElementRefNodeNo, refNo);
}
// record dynamic style elements after insert succeed
dynamicStyleSheetElements.push(stylesheetElement);
return result;
return result as T;
}

case SCRIPT_TAG_NAME: {
Expand All @@ -178,36 +183,44 @@ 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:
break;
}
}

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<T extends Node>(this: HTMLHeadElement | HTMLBodyElement, child: T) {
function removeChildInSandbox<T extends Node>(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:
Expand Down Expand Up @@ -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(
Expand Down
Loading

0 comments on commit d13d99a

Please sign in to comment.