✨patch sandbox document to aviod chaos of dynamic element appending when speedy mode enabled (#2404)
This commit is contained in:
parent
fb7f70221f
commit
dd6aa4a042
|
|
@ -130,6 +130,7 @@ export type SandBox = {
|
||||||
sandboxRunning: boolean;
|
sandboxRunning: boolean;
|
||||||
/** latest set property */
|
/** latest set property */
|
||||||
latestSetProp?: PropertyKey | null;
|
latestSetProp?: PropertyKey | null;
|
||||||
|
patchDocument: (doc: Document) => void;
|
||||||
/** 启动沙箱 */
|
/** 启动沙箱 */
|
||||||
active: () => void;
|
active: () => void;
|
||||||
/** 关闭沙箱 */
|
/** 关闭沙箱 */
|
||||||
|
|
|
||||||
|
|
@ -15,11 +15,15 @@ export function getCurrentRunningApp() {
|
||||||
return currentRunningApp;
|
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
|
// 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;
|
currentRunningApp = appInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function clearCurrentRunningApp() {
|
||||||
|
currentRunningApp = null;
|
||||||
|
}
|
||||||
|
|
||||||
const functionBoundedValueMap = new WeakMap<CallableFunction, CallableFunction>();
|
const functionBoundedValueMap = new WeakMap<CallableFunction, CallableFunction>();
|
||||||
|
|
||||||
export function getTargetValue(target: any, value: any): any {
|
export function getTargetValue(target: any, value: any): any {
|
||||||
|
|
|
||||||
|
|
@ -155,4 +155,6 @@ export default class LegacySandbox implements SandBox {
|
||||||
|
|
||||||
this.proxy = proxy;
|
this.proxy = proxy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
patchDocument(): void {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { checkActivityFunctions } from 'single-spa';
|
import { checkActivityFunctions } from 'single-spa';
|
||||||
import type { Freer } from '../../../interfaces';
|
import type { Freer, SandBox } from '../../../interfaces';
|
||||||
import {
|
import {
|
||||||
calcAppCount,
|
calcAppCount,
|
||||||
isAllAppsUnmounted,
|
isAllAppsUnmounted,
|
||||||
|
|
@ -16,10 +16,10 @@ import {
|
||||||
/**
|
/**
|
||||||
* Just hijack dynamic head append, that could avoid accidentally hijacking the insertion of elements except in head.
|
* Just hijack dynamic head append, that could avoid accidentally hijacking the insertion of elements except in head.
|
||||||
* Such a case: ReactDOM.createPortal(<style>.test{color:blue}</style>, container),
|
* Such a case: ReactDOM.createPortal(<style>.test{color:blue}</style>, 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 appName
|
||||||
* @param appWrapperGetter
|
* @param appWrapperGetter
|
||||||
* @param proxy
|
* @param sandbox
|
||||||
* @param mounting
|
* @param mounting
|
||||||
* @param scopedCSS
|
* @param scopedCSS
|
||||||
* @param excludeAssetFilter
|
* @param excludeAssetFilter
|
||||||
|
|
@ -27,20 +27,22 @@ import {
|
||||||
export function patchLooseSandbox(
|
export function patchLooseSandbox(
|
||||||
appName: string,
|
appName: string,
|
||||||
appWrapperGetter: () => HTMLElement | ShadowRoot,
|
appWrapperGetter: () => HTMLElement | ShadowRoot,
|
||||||
proxy: Window,
|
sandbox: SandBox,
|
||||||
mounting = true,
|
mounting = true,
|
||||||
scopedCSS = false,
|
scopedCSS = false,
|
||||||
excludeAssetFilter?: CallableFunction,
|
excludeAssetFilter?: CallableFunction,
|
||||||
): Freer {
|
): Freer {
|
||||||
|
const { proxy } = sandbox;
|
||||||
|
|
||||||
let dynamicStyleSheetElements: Array<HTMLLinkElement | HTMLStyleElement> = [];
|
let dynamicStyleSheetElements: Array<HTMLLinkElement | HTMLStyleElement> = [];
|
||||||
|
|
||||||
const unpatchDynamicAppendPrototypeFunctions = patchHTMLDynamicAppendPrototypeFunctions(
|
const unpatchDynamicAppendPrototypeFunctions = patchHTMLDynamicAppendPrototypeFunctions(
|
||||||
/*
|
/*
|
||||||
check if the currently specified application is active
|
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,
|
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,
|
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
|
see https://github.com/ReactTraining/history/blob/master/modules/createHashHistory.js#L222-L230
|
||||||
*/
|
*/
|
||||||
() => checkActivityFunctions(window.location).some((name) => name === appName),
|
() => checkActivityFunctions(window.location).some((name) => name === appName),
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,8 @@
|
||||||
* @since 2020-10-13
|
* @since 2020-10-13
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Freer } from '../../../interfaces';
|
import { noop } from 'lodash';
|
||||||
|
import type { Freer, SandBox } from '../../../interfaces';
|
||||||
import { nativeGlobal } from '../../../utils';
|
import { nativeGlobal } from '../../../utils';
|
||||||
import { getCurrentRunningApp } from '../../common';
|
import { getCurrentRunningApp } from '../../common';
|
||||||
import type { ContainerConfig } from './common';
|
import type { ContainerConfig } from './common';
|
||||||
|
|
@ -31,9 +32,48 @@ const proxyAttachContainerConfigMap: WeakMap<WindowProxy, ContainerConfig> =
|
||||||
const elementAttachContainerConfigMap = new WeakMap<HTMLElement, ContainerConfig>();
|
const elementAttachContainerConfigMap = new WeakMap<HTMLElement, ContainerConfig>();
|
||||||
|
|
||||||
const docCreatePatchedMap = new WeakMap<typeof document.createElement, typeof document.createElement>();
|
const docCreatePatchedMap = new WeakMap<typeof document.createElement, typeof document.createElement>();
|
||||||
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) => {
|
||||||
|
(<any>target)[p] = value;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
get: (target, p) => {
|
||||||
|
if (p === 'createElement') {
|
||||||
|
return (...args: Parameters<typeof document.createElement>) => {
|
||||||
|
const element = document.createElement(...args);
|
||||||
|
attachElementToProxy(element, sandbox.proxy);
|
||||||
|
return element;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = (<any>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) {
|
if (!docCreateElementFnBeforeOverwrite) {
|
||||||
const rawDocumentCreateElement = document.createElement;
|
const rawDocumentCreateElement = document.createElement;
|
||||||
Document.prototype.createElement = function createElement<K extends keyof HTMLElementTagNameMap>(
|
Document.prototype.createElement = function createElement<K extends keyof HTMLElementTagNameMap>(
|
||||||
|
|
@ -45,10 +85,7 @@ function patchDocumentCreateElement() {
|
||||||
if (isHijackingTag(tagName)) {
|
if (isHijackingTag(tagName)) {
|
||||||
const { window: currentRunningSandboxProxy } = getCurrentRunningApp() || {};
|
const { window: currentRunningSandboxProxy } = getCurrentRunningApp() || {};
|
||||||
if (currentRunningSandboxProxy) {
|
if (currentRunningSandboxProxy) {
|
||||||
const proxyContainerConfig = proxyAttachContainerConfigMap.get(currentRunningSandboxProxy);
|
attachElementToProxy(element, currentRunningSandboxProxy);
|
||||||
if (proxyContainerConfig) {
|
|
||||||
elementAttachContainerConfigMap.set(element, proxyContainerConfig);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,12 +111,13 @@ function patchDocumentCreateElement() {
|
||||||
export function patchStrictSandbox(
|
export function patchStrictSandbox(
|
||||||
appName: string,
|
appName: string,
|
||||||
appWrapperGetter: () => HTMLElement | ShadowRoot,
|
appWrapperGetter: () => HTMLElement | ShadowRoot,
|
||||||
proxy: Window,
|
sandbox: SandBox,
|
||||||
mounting = true,
|
mounting = true,
|
||||||
scopedCSS = false,
|
scopedCSS = false,
|
||||||
excludeAssetFilter?: CallableFunction,
|
excludeAssetFilter?: CallableFunction,
|
||||||
speedySandbox = false,
|
speedySandbox = false,
|
||||||
): Freer {
|
): Freer {
|
||||||
|
const { proxy } = sandbox;
|
||||||
let containerConfig = proxyAttachContainerConfigMap.get(proxy);
|
let containerConfig = proxyAttachContainerConfigMap.get(proxy);
|
||||||
if (!containerConfig) {
|
if (!containerConfig) {
|
||||||
containerConfig = {
|
containerConfig = {
|
||||||
|
|
@ -97,7 +135,7 @@ export function patchStrictSandbox(
|
||||||
// all dynamic style sheets are stored in proxy container
|
// all dynamic style sheets are stored in proxy container
|
||||||
const { dynamicStyleSheetElements } = containerConfig;
|
const { dynamicStyleSheetElements } = containerConfig;
|
||||||
|
|
||||||
const unpatchDocumentCreate = patchDocumentCreateElement();
|
const unpatchDocumentCreate = patchDocument({ sandbox, speedy: speedySandbox });
|
||||||
|
|
||||||
const unpatchDynamicAppendPrototypeFunctions = patchHTMLDynamicAppendPrototypeFunctions(
|
const unpatchDynamicAppendPrototypeFunctions = patchHTMLDynamicAppendPrototypeFunctions(
|
||||||
(element) => elementAttachContainerConfigMap.has(element),
|
(element) => elementAttachContainerConfigMap.has(element),
|
||||||
|
|
|
||||||
|
|
@ -28,16 +28,15 @@ export function patchAtMounting(
|
||||||
const patchersInSandbox = {
|
const patchersInSandbox = {
|
||||||
[SandBoxType.LegacyProxy]: [
|
[SandBoxType.LegacyProxy]: [
|
||||||
...basePatchers,
|
...basePatchers,
|
||||||
() => patchLooseSandbox(appName, elementGetter, sandbox.proxy, true, scopedCSS, excludeAssetFilter),
|
() => patchLooseSandbox(appName, elementGetter, sandbox, true, scopedCSS, excludeAssetFilter),
|
||||||
],
|
],
|
||||||
[SandBoxType.Proxy]: [
|
[SandBoxType.Proxy]: [
|
||||||
...basePatchers,
|
...basePatchers,
|
||||||
() =>
|
() => patchStrictSandbox(appName, elementGetter, sandbox, true, scopedCSS, excludeAssetFilter, speedySandBox),
|
||||||
patchStrictSandbox(appName, elementGetter, sandbox.proxy, true, scopedCSS, excludeAssetFilter, speedySandBox),
|
|
||||||
],
|
],
|
||||||
[SandBoxType.Snapshot]: [
|
[SandBoxType.Snapshot]: [
|
||||||
...basePatchers,
|
...basePatchers,
|
||||||
() => patchLooseSandbox(appName, elementGetter, sandbox.proxy, true, scopedCSS, excludeAssetFilter),
|
() => patchLooseSandbox(appName, elementGetter, sandbox, true, scopedCSS, excludeAssetFilter),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -54,14 +53,13 @@ export function patchAtBootstrapping(
|
||||||
): Freer[] {
|
): Freer[] {
|
||||||
const patchersInSandbox = {
|
const patchersInSandbox = {
|
||||||
[SandBoxType.LegacyProxy]: [
|
[SandBoxType.LegacyProxy]: [
|
||||||
() => patchLooseSandbox(appName, elementGetter, sandbox.proxy, false, scopedCSS, excludeAssetFilter),
|
() => patchLooseSandbox(appName, elementGetter, sandbox, false, scopedCSS, excludeAssetFilter),
|
||||||
],
|
],
|
||||||
[SandBoxType.Proxy]: [
|
[SandBoxType.Proxy]: [
|
||||||
() =>
|
() => patchStrictSandbox(appName, elementGetter, sandbox, false, scopedCSS, excludeAssetFilter, speedySandBox),
|
||||||
patchStrictSandbox(appName, elementGetter, sandbox.proxy, false, scopedCSS, excludeAssetFilter, speedySandBox),
|
|
||||||
],
|
],
|
||||||
[SandBoxType.Snapshot]: [
|
[SandBoxType.Snapshot]: [
|
||||||
() => patchLooseSandbox(appName, elementGetter, sandbox.proxy, false, scopedCSS, excludeAssetFilter),
|
() => patchLooseSandbox(appName, elementGetter, sandbox, false, scopedCSS, excludeAssetFilter),
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import { without } from 'lodash';
|
||||||
import type { SandBox } from '../interfaces';
|
import type { SandBox } from '../interfaces';
|
||||||
import { SandBoxType } from '../interfaces';
|
import { SandBoxType } from '../interfaces';
|
||||||
import { isPropertyFrozen, nativeGlobal, nextTask } from '../utils';
|
import { isPropertyFrozen, nativeGlobal, nextTask } from '../utils';
|
||||||
import { getCurrentRunningApp, getTargetValue, setCurrentRunningApp } from './common';
|
import { clearCurrentRunningApp, getCurrentRunningApp, getTargetValue, setCurrentRunningApp } from './common';
|
||||||
import { globals } from './globals';
|
import { globals } from './globals';
|
||||||
|
|
||||||
type SymbolTarget = 'target' | 'globalContext';
|
type SymbolTarget = 'target' | 'globalContext';
|
||||||
|
|
@ -153,7 +153,11 @@ export default class ProxySandbox implements SandBox {
|
||||||
type: SandBoxType;
|
type: SandBoxType;
|
||||||
|
|
||||||
proxy: WindowProxy;
|
proxy: WindowProxy;
|
||||||
|
|
||||||
sandboxRunning = true;
|
sandboxRunning = true;
|
||||||
|
|
||||||
|
private document = document;
|
||||||
|
|
||||||
latestSetProp: PropertyKey | null = null;
|
latestSetProp: PropertyKey | null = null;
|
||||||
|
|
||||||
active() {
|
active() {
|
||||||
|
|
@ -269,7 +273,7 @@ export default class ProxySandbox implements SandBox {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (p === 'document') {
|
if (p === 'document') {
|
||||||
return document;
|
return this.document;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (p === 'eval') {
|
if (p === 'eval') {
|
||||||
|
|
@ -316,7 +320,7 @@ export default class ProxySandbox implements SandBox {
|
||||||
if (globalContext.hasOwnProperty(p)) {
|
if (globalContext.hasOwnProperty(p)) {
|
||||||
const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
|
const descriptor = Object.getOwnPropertyDescriptor(globalContext, p);
|
||||||
descriptorTargetMap.set(p, 'globalContext');
|
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) {
|
if (descriptor && !descriptor.configurable) {
|
||||||
descriptor.configurable = true;
|
descriptor.configurable = true;
|
||||||
}
|
}
|
||||||
|
|
@ -331,7 +335,7 @@ export default class ProxySandbox implements SandBox {
|
||||||
return uniq(Reflect.ownKeys(globalContext).concat(Reflect.ownKeys(target)));
|
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);
|
const from = descriptorTargetMap.get(p);
|
||||||
/*
|
/*
|
||||||
Descriptor must be defined to native window while it comes from native window via Object.getOwnPropertyDescriptor(window, 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++;
|
activeSandboxCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public patchDocument(doc: Document) {
|
||||||
|
this.document = doc;
|
||||||
|
}
|
||||||
|
|
||||||
private registerRunningApp(name: string, proxy: Window) {
|
private registerRunningApp(name: string, proxy: Window) {
|
||||||
if (this.sandboxRunning) {
|
if (this.sandboxRunning) {
|
||||||
const currentRunningApp = getCurrentRunningApp();
|
const currentRunningApp = getCurrentRunningApp();
|
||||||
|
|
@ -378,9 +386,7 @@ export default class ProxySandbox implements SandBox {
|
||||||
// FIXME if you have any other good ideas
|
// 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
|
// 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
|
// 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(() => {
|
nextTask(clearCurrentRunningApp);
|
||||||
setCurrentRunningApp(null);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,4 +69,6 @@ export default class SnapshotSandbox implements SandBox {
|
||||||
|
|
||||||
this.sandboxRunning = false;
|
this.sandboxRunning = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
patchDocument(): void {}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user