🐛 app instance id generator compatible with nested sandbox (#1866)

This commit is contained in:
Kuitos 2021-12-10 16:22:45 +08:00 committed by GitHub
parent df11e0c8b2
commit ff93c6fd10
3 changed files with 59 additions and 41 deletions

View File

@ -1,5 +1,6 @@
import { import {
Deferred, Deferred,
genAppInstanceIdByName,
getDefaultTplWrapper, getDefaultTplWrapper,
getWrapperId, getWrapperId,
getXPathForElement, getXPathForElement,
@ -137,3 +138,14 @@ it('should nextTick just executed once in one task context', async () => {
await sleep(0); await sleep(0);
expect(counter).toBe(3); expect(counter).toBe(3);
}); });
it('should genAppInstanceIdByName works well', () => {
const instanceId1 = genAppInstanceIdByName('hello');
expect(instanceId1).toBe('hello');
const instanceId2 = genAppInstanceIdByName('hello');
expect(instanceId2).toBe('hello_1');
const instanceId3 = genAppInstanceIdByName('hello');
expect(instanceId3).toBe('hello_2');
});

View File

@ -20,7 +20,7 @@ import type {
import { createSandboxContainer, css } from './sandbox'; import { createSandboxContainer, css } from './sandbox';
import { import {
Deferred, Deferred,
getAppInstanceName, genAppInstanceIdByName,
getContainer, getContainer,
getDefaultTplWrapper, getDefaultTplWrapper,
getWrapperId, getWrapperId,
@ -68,7 +68,7 @@ function createElement(
appContent: string, appContent: string,
strictStyleIsolation: boolean, strictStyleIsolation: boolean,
scopedCSS: boolean, scopedCSS: boolean,
appName: string, appInstanceId: string,
): HTMLElement { ): HTMLElement {
const containerElement = document.createElement('div'); const containerElement = document.createElement('div');
containerElement.innerHTML = appContent; containerElement.innerHTML = appContent;
@ -97,12 +97,12 @@ function createElement(
if (scopedCSS) { if (scopedCSS) {
const attr = appElement.getAttribute(css.QiankunCSSRewriteAttr); const attr = appElement.getAttribute(css.QiankunCSSRewriteAttr);
if (!attr) { if (!attr) {
appElement.setAttribute(css.QiankunCSSRewriteAttr, appName); appElement.setAttribute(css.QiankunCSSRewriteAttr, appInstanceId);
} }
const styleNodes = appElement.querySelectorAll('style') || []; const styleNodes = appElement.querySelectorAll('style') || [];
forEach(styleNodes, (stylesheetElement: HTMLStyleElement) => { forEach(styleNodes, (stylesheetElement: HTMLStyleElement) => {
css.process(appElement!, stylesheetElement, appName); css.process(appElement!, stylesheetElement, appInstanceId);
}); });
} }
@ -111,7 +111,7 @@ function createElement(
/** generate app wrapper dom getter */ /** generate app wrapper dom getter */
function getAppWrapperGetter( function getAppWrapperGetter(
appName: string, appInstanceId: string,
useLegacyRender: boolean, useLegacyRender: boolean,
strictStyleIsolation: boolean, strictStyleIsolation: boolean,
scopedCSS: boolean, scopedCSS: boolean,
@ -122,13 +122,13 @@ function getAppWrapperGetter(
if (strictStyleIsolation) throw new QiankunError('strictStyleIsolation can not be used with legacy render!'); if (strictStyleIsolation) throw new QiankunError('strictStyleIsolation can not be used with legacy render!');
if (scopedCSS) throw new QiankunError('experimentalStyleIsolation can not be used with legacy render!'); if (scopedCSS) throw new QiankunError('experimentalStyleIsolation can not be used with legacy render!');
const appWrapper = document.getElementById(getWrapperId(appName)); const appWrapper = document.getElementById(getWrapperId(appInstanceId));
assertElementExist(appWrapper, `Wrapper element for ${appName} is not existed!`); assertElementExist(appWrapper, `Wrapper element for ${appInstanceId} is not existed!`);
return appWrapper!; return appWrapper!;
} }
const element = elementGetter(); const element = elementGetter();
assertElementExist(element, `Wrapper element for ${appName} is not existed!`); assertElementExist(element, `Wrapper element for ${appInstanceId} is not existed!`);
if (strictStyleIsolation && supportShadowDOM) { if (strictStyleIsolation && supportShadowDOM) {
return element!.shadowRoot!; return element!.shadowRoot!;
@ -148,11 +148,11 @@ type ElementRender = (
/** /**
* Get the render function * Get the render function
* If the legacy render function is provide, used as it, otherwise we will insert the app element to target container by qiankun * If the legacy render function is provide, used as it, otherwise we will insert the app element to target container by qiankun
* @param appName * @param appInstanceId
* @param appContent * @param appContent
* @param legacyRender * @param legacyRender
*/ */
function getRender(appName: string, appContent: string, legacyRender?: HTMLContentRender) { function getRender(appInstanceId: string, appContent: string, legacyRender?: HTMLContentRender) {
const render: ElementRender = ({ element, loading, container }, phase) => { const render: ElementRender = ({ element, loading, container }, phase) => {
if (legacyRender) { if (legacyRender) {
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
@ -173,13 +173,13 @@ function getRender(appName: string, appContent: string, legacyRender?: HTMLConte
switch (phase) { switch (phase) {
case 'loading': case 'loading':
case 'mounting': case 'mounting':
return `Target container with ${container} not existed while ${appName} ${phase}!`; return `Target container with ${container} not existed while ${appInstanceId} ${phase}!`;
case 'mounted': case 'mounted':
return `Target container with ${container} not existed after ${appName} ${phase}!`; return `Target container with ${container} not existed after ${appInstanceId} ${phase}!`;
default: default:
return `Target container with ${container} not existed while ${appName} rendering!`; return `Target container with ${container} not existed while ${appInstanceId} rendering!`;
} }
})(); })();
assertElementExist(containerElement, errorMsg); assertElementExist(containerElement, errorMsg);
@ -246,10 +246,10 @@ export async function loadApp<T extends ObjectType>(
configuration: FrameworkConfiguration = {}, configuration: FrameworkConfiguration = {},
lifeCycles?: FrameworkLifeCycles<T>, lifeCycles?: FrameworkLifeCycles<T>,
): Promise<ParcelConfigObjectGetter> { ): Promise<ParcelConfigObjectGetter> {
const { entry } = app; const { entry, name: appName } = app;
const appInstanceName = getAppInstanceName(app.name); const appInstanceId = genAppInstanceIdByName(appName);
const markName = `[qiankun] App ${appInstanceName} Loading`; const markName = `[qiankun] App ${appInstanceId} Loading`;
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
performanceMark(markName); performanceMark(markName);
} }
@ -272,7 +272,7 @@ export async function loadApp<T extends ObjectType>(
await (prevAppUnmountedDeferred && prevAppUnmountedDeferred.promise); await (prevAppUnmountedDeferred && prevAppUnmountedDeferred.promise);
} }
const appContent = getDefaultTplWrapper(appInstanceName)(template); const appContent = getDefaultTplWrapper(appInstanceId)(template);
const strictStyleIsolation = typeof sandbox === 'object' && !!sandbox.strictStyleIsolation; const strictStyleIsolation = typeof sandbox === 'object' && !!sandbox.strictStyleIsolation;
@ -287,20 +287,20 @@ export async function loadApp<T extends ObjectType>(
appContent, appContent,
strictStyleIsolation, strictStyleIsolation,
scopedCSS, scopedCSS,
appInstanceName, appInstanceId,
); );
const initialContainer = 'container' in app ? app.container : undefined; const initialContainer = 'container' in app ? app.container : undefined;
const legacyRender = 'render' in app ? app.render : undefined; const legacyRender = 'render' in app ? app.render : undefined;
const render = getRender(appInstanceName, appContent, legacyRender); const render = getRender(appInstanceId, appContent, legacyRender);
// 第一次加载设置应用可见区域 dom 结构 // 第一次加载设置应用可见区域 dom 结构
// 确保每次应用加载前容器 dom 结构已经设置完毕 // 确保每次应用加载前容器 dom 结构已经设置完毕
render({ element: initialAppWrapperElement, loading: true, container: initialContainer }, 'loading'); render({ element: initialAppWrapperElement, loading: true, container: initialContainer }, 'loading');
const initialAppWrapperGetter = getAppWrapperGetter( const initialAppWrapperGetter = getAppWrapperGetter(
appInstanceName, appInstanceId,
!!legacyRender, !!legacyRender,
strictStyleIsolation, strictStyleIsolation,
scopedCSS, scopedCSS,
@ -314,7 +314,7 @@ export async function loadApp<T extends ObjectType>(
let sandboxContainer; let sandboxContainer;
if (sandbox) { if (sandbox) {
sandboxContainer = createSandboxContainer( sandboxContainer = createSandboxContainer(
appInstanceName, appInstanceId,
// FIXME should use a strict sandbox logic while remount, see https://github.com/umijs/qiankun/issues/518 // FIXME should use a strict sandbox logic while remount, see https://github.com/umijs/qiankun/issues/518
initialAppWrapperGetter, initialAppWrapperGetter,
scopedCSS, scopedCSS,
@ -342,13 +342,13 @@ export async function loadApp<T extends ObjectType>(
const scriptExports: any = await execScripts(global, sandbox && !useLooseSandbox); const scriptExports: any = await execScripts(global, sandbox && !useLooseSandbox);
const { bootstrap, mount, unmount, update } = getLifecyclesFromExports( const { bootstrap, mount, unmount, update } = getLifecyclesFromExports(
scriptExports, scriptExports,
appInstanceName, appName,
global, global,
sandboxContainer?.instance?.latestSetProp, sandboxContainer?.instance?.latestSetProp,
); );
const { onGlobalStateChange, setGlobalState, offGlobalStateChange }: Record<string, CallableFunction> = const { onGlobalStateChange, setGlobalState, offGlobalStateChange }: Record<string, CallableFunction> =
getMicroAppStateActions(appInstanceName); getMicroAppStateActions(appInstanceId);
// FIXME temporary way // FIXME temporary way
const syncAppWrapperElement2Sandbox = (element: HTMLElement | null) => (initialAppWrapperElement = element); const syncAppWrapperElement2Sandbox = (element: HTMLElement | null) => (initialAppWrapperElement = element);
@ -358,7 +358,7 @@ export async function loadApp<T extends ObjectType>(
let appWrapperGetter: ReturnType<typeof getAppWrapperGetter>; let appWrapperGetter: ReturnType<typeof getAppWrapperGetter>;
const parcelConfig: ParcelConfigObject = { const parcelConfig: ParcelConfigObject = {
name: appInstanceName, name: appInstanceId,
bootstrap, bootstrap,
mount: [ mount: [
async () => { async () => {
@ -381,7 +381,7 @@ export async function loadApp<T extends ObjectType>(
async () => { async () => {
appWrapperElement = initialAppWrapperElement; appWrapperElement = initialAppWrapperElement;
appWrapperGetter = getAppWrapperGetter( appWrapperGetter = getAppWrapperGetter(
appInstanceName, appInstanceId,
!!legacyRender, !!legacyRender,
strictStyleIsolation, strictStyleIsolation,
scopedCSS, scopedCSS,
@ -394,7 +394,7 @@ export async function loadApp<T extends ObjectType>(
if (useNewContainer || !appWrapperElement) { if (useNewContainer || !appWrapperElement) {
// element will be destroyed after unmounted, we need to recreate it if it not exist // element will be destroyed after unmounted, we need to recreate it if it not exist
// or we try to remount into a new container // or we try to remount into a new container
appWrapperElement = createElement(appContent, strictStyleIsolation, scopedCSS, appInstanceName); appWrapperElement = createElement(appContent, strictStyleIsolation, scopedCSS, appInstanceId);
syncAppWrapperElement2Sandbox(appWrapperElement); syncAppWrapperElement2Sandbox(appWrapperElement);
} }
@ -415,7 +415,7 @@ export async function loadApp<T extends ObjectType>(
}, },
async () => { async () => {
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
const measureName = `[qiankun] App ${appInstanceName} Loading Consuming`; const measureName = `[qiankun] App ${appInstanceId} Loading Consuming`;
performanceMeasure(measureName, markName); performanceMeasure(measureName, markName);
} }
}, },
@ -427,7 +427,7 @@ export async function loadApp<T extends ObjectType>(
async () => execHooksChain(toArray(afterUnmount), app, global), async () => execHooksChain(toArray(afterUnmount), app, global),
async () => { async () => {
render({ element: null, loading: false, container: remountContainer }, 'unmounted'); render({ element: null, loading: false, container: remountContainer }, 'unmounted');
offGlobalStateChange(appInstanceName); offGlobalStateChange(appInstanceId);
// for gc // for gc
appWrapperElement = null; appWrapperElement = null;
syncAppWrapperElement2Sandbox(appWrapperElement); syncAppWrapperElement2Sandbox(appWrapperElement);

View File

@ -3,7 +3,7 @@
* @since 2019-05-15 * @since 2019-05-15
*/ */
import { isFunction, snakeCase } from 'lodash'; import { isFunction, snakeCase, once } from 'lodash';
import { version } from './version'; import { version } from './version';
import type { FrameworkConfiguration } from './interfaces'; import type { FrameworkConfiguration } from './interfaces';
@ -111,23 +111,31 @@ export function getWrapperId(name: string) {
export const nativeGlobal = new Function('return this')(); export const nativeGlobal = new Function('return this')();
Object.defineProperty(nativeGlobal, '__app_instance_name_map__', { const getGlobalAppInstanceMap = once<() => Record<string, number>>(() => {
enumerable: false, if (!nativeGlobal.hasOwnProperty('__app_instance_name_map__')) {
writable: true, Object.defineProperty(nativeGlobal, '__app_instance_name_map__', {
value: {}, enumerable: false,
configurable: true,
writable: true,
value: {},
});
}
return nativeGlobal.__app_instance_name_map__;
}); });
/** /**
* get app instance name with the auto-increment approach * Get app instance name with the auto-increment approach
* @param appName * @param appName
*/ */
export const getAppInstanceName = (appName: string): string => { export const genAppInstanceIdByName = (appName: string): string => {
if (!(appName in nativeGlobal.__app_instance_name_map__)) { const globalAppInstanceMap = getGlobalAppInstanceMap();
if (!(appName in globalAppInstanceMap)) {
nativeGlobal.__app_instance_name_map__[appName] = 0; nativeGlobal.__app_instance_name_map__[appName] = 0;
return appName; return appName;
} }
nativeGlobal.__app_instance_name_map__[appName]++; globalAppInstanceMap[appName]++;
return `${appName}_${nativeGlobal.__app_instance_name_map__[appName]}`; return `${appName}_${globalAppInstanceMap[appName]}`;
}; };
/** 校验子应用导出的 生命周期 对象是否正确 */ /** 校验子应用导出的 生命周期 对象是否正确 */
@ -136,7 +144,7 @@ export function validateExportLifecycle(exports: any) {
return isFunction(bootstrap) && isFunction(mount) && isFunction(unmount); return isFunction(bootstrap) && isFunction(mount) && isFunction(unmount);
} }
class Deferred<T> { export class Deferred<T> {
promise: Promise<T>; promise: Promise<T>;
resolve!: (value: T | PromiseLike<T>) => void; resolve!: (value: T | PromiseLike<T>) => void;
@ -151,8 +159,6 @@ class Deferred<T> {
} }
} }
export { Deferred };
const supportsUserTiming = const supportsUserTiming =
typeof performance !== 'undefined' && typeof performance !== 'undefined' &&
typeof performance.mark === 'function' && typeof performance.mark === 'function' &&