diff --git a/src/apis.ts b/src/apis.ts index 415bf03..d37c84e 100644 --- a/src/apis.ts +++ b/src/apis.ts @@ -1,9 +1,9 @@ import { noop } from 'lodash'; import { mountRootParcel, ParcelConfigObject, registerApplication, start as startSingleSpa } from 'single-spa'; import { FrameworkConfiguration, FrameworkLifeCycles, LoadableApp, MicroApp, RegistrableApp } from './interfaces'; -import { loadApp } from './loader'; +import { loadApp, ParcelConfigObjectGetter } from './loader'; import { doPrefetchStrategy } from './prefetch'; -import { Deferred, getXPathForElement, toArray } from './utils'; +import { Deferred, getContainer, getXPathForElement, toArray } from './utils'; let microApps: RegistrableApp[] = []; @@ -29,11 +29,9 @@ export function registerMicroApps( loader(true); await frameworkStartedDefer.promise; - const { mount, ...otherMicroAppConfigs } = await loadApp( - { name, props, ...appConfig }, - frameworkConfiguration, - lifeCycles, - ); + const { mount, ...otherMicroAppConfigs } = ( + await loadApp({ name, props, ...appConfig }, frameworkConfiguration, lifeCycles) + )(); return { mount: [async () => loader(true), ...toArray(mount), async () => loader(false)], @@ -46,7 +44,7 @@ export function registerMicroApps( }); } -const appConfigMap = new Map>(); +const appConfigPormiseGetterMap = new Map>(); export function loadMicroApp( app: LoadableApp, @@ -56,7 +54,7 @@ export function loadMicroApp( const { props, name } = app; const getContainerXpath = (container: string | HTMLElement): string | void => { - const containerElement = typeof container === 'string' ? document.querySelector(container) : container; + const containerElement = getContainer(container); if (containerElement) { return getXPathForElement(containerElement, document); } @@ -74,24 +72,26 @@ export function loadMicroApp( if (container) { const xpath = getContainerXpath(container); if (xpath) { - const parcelConfig = appConfigMap.get(`${name}-${xpath}`); - if (parcelConfig) return parcelConfig; + const parcelConfigGetterPromise = appConfigPormiseGetterMap.get(`${name}-${xpath}`); + if (parcelConfigGetterPromise) { + const parcelConfig = (await parcelConfigGetterPromise)(container); + return { + ...parcelConfig, + // empty bootstrap hook which should not run twice while it calling from cached micro app + bootstrap: () => Promise.resolve(), + }; + } } } - const parcelConfig = loadApp(app, configuration ?? frameworkConfiguration, lifeCycles); + const parcelConfigObjectGetterPromise = loadApp(app, configuration ?? frameworkConfiguration, lifeCycles); if (container) { const xpath = getContainerXpath(container); - if (xpath) - appConfigMap.set( - `${name}-${xpath}`, - // empty bootstrap hook which should not run twice while it calling from cached micro app - parcelConfig.then(config => ({ ...config, bootstrap: () => Promise.resolve() })), - ); + if (xpath) appConfigPormiseGetterMap.set(`${name}-${xpath}`, parcelConfigObjectGetterPromise); } - return parcelConfig; + return (await parcelConfigObjectGetterPromise)(container); }; return mountRootParcel(memorizedLoadingFn, { domElement: document.createElement('div'), ...props }); diff --git a/src/loader.ts b/src/loader.ts index e43fb20..0141657 100644 --- a/src/loader.ts +++ b/src/loader.ts @@ -12,6 +12,7 @@ import { FrameworkConfiguration, FrameworkLifeCycles, HTMLContentRender, LifeCyc import { createSandbox, css } from './sandbox'; import { Deferred, + getContainer, getDefaultTplWrapper, getWrapperId, isEnableScopedCSS, @@ -126,7 +127,7 @@ function getAppWrapperGetter( const rawAppendChild = HTMLElement.prototype.appendChild; const rawRemoveChild = HTMLElement.prototype.removeChild; type ElementRender = ( - props: { element: HTMLElement | null; loading: boolean }, + props: { element: HTMLElement | null; loading: boolean; remountContainer?: string | HTMLElement }, phase: 'loading' | 'mounting' | 'mounted' | 'unmounted', ) => any; @@ -144,7 +145,7 @@ function getRender( container?: string | HTMLElement, legacyRender?: HTMLContentRender, ) { - const render: ElementRender = ({ element, loading }, phase) => { + const render: ElementRender = ({ element, loading, remountContainer }, phase) => { if (legacyRender) { if (process.env.NODE_ENV === 'development') { console.warn( @@ -155,7 +156,7 @@ function getRender( return legacyRender({ loading, appContent: element ? appContent : '' }); } - const containerElement = typeof container === 'string' ? document.querySelector(container) : container; + const containerElement = getContainer(remountContainer || container!); // The container might have be removed after micro app unmounted. // Such as the micro app unmount lifecycle called by a react componentWillUnmount lifecycle, after micro app unmounted, the react component might also be removed @@ -217,11 +218,12 @@ function getLifecyclesFromExports(scriptExports: LifeCycles, appName: strin let prevAppUnmountedDeferred: Deferred; +export type ParcelConfigObjectGetter = (remountContainer?: string | HTMLElement) => ParcelConfigObject; export async function loadApp( app: LoadableApp, configuration: FrameworkConfiguration = {}, lifeCycles?: FrameworkLifeCycles, -): Promise { +): Promise { const { entry, name: appName } = app; const appInstanceId = `${appName}_${+new Date()}_${Math.floor(Math.random() * 1000)}`; @@ -308,74 +310,78 @@ export async function loadApp( offGlobalStateChange, }: Record = getMicroAppStateActions(appInstanceId); - const parcelConfig: ParcelConfigObject = { - name: appInstanceId, - bootstrap, - mount: [ - async () => { - if (process.env.NODE_ENV === 'development') { - const marks = performance.getEntriesByName(markName, 'mark'); - // mark length is zero means the app is remounting - if (!marks.length) { - performanceMark(markName); + const parcelConfigGetter: ParcelConfigObjectGetter = remountContainer => { + const parcelConfig: ParcelConfigObject = { + name: appInstanceId, + bootstrap, + mount: [ + async () => { + if (process.env.NODE_ENV === 'development') { + const marks = performance.getEntriesByName(markName, 'mark'); + // mark length is zero means the app is remounting + if (!marks.length) { + performanceMark(markName); + } + } + }, + async () => { + if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) { + return prevAppUnmountedDeferred.promise; } - } - }, - async () => { - if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) { - return prevAppUnmountedDeferred.promise; - } - return undefined; - }, - // 添加 mount hook, 确保每次应用加载前容器 dom 结构已经设置完毕 - async () => { - // element would be destroyed after unmounted, we need to recreate it if it not exist - appWrapperElement = appWrapperElement || createElement(appContent, strictStyleIsolation); - render({ element: appWrapperElement, loading: true }, 'mounting'); - }, - mountSandbox, - // exec the chain after rendering to keep the behavior with beforeLoad - async () => execHooksChain(toArray(beforeMount), app, global), - async props => mount({ ...props, container: appWrapperGetter(), setGlobalState, onGlobalStateChange }), - // finish loading after app mounted - async () => render({ element: appWrapperElement, loading: false }, 'mounted'), - async () => execHooksChain(toArray(afterMount), app, global), - // initialize the unmount defer after app mounted and resolve the defer after it unmounted - async () => { - if (await validateSingularMode(singular, app)) { - prevAppUnmountedDeferred = new Deferred(); - } - }, - async () => { - if (process.env.NODE_ENV === 'development') { - const measureName = `[qiankun] App ${appInstanceId} Loading Consuming`; - performanceMeasure(measureName, markName); - } - }, - ], - unmount: [ - async () => execHooksChain(toArray(beforeUnmount), app, global), - async props => unmount({ ...props, container: appWrapperGetter() }), - unmountSandbox, - async () => execHooksChain(toArray(afterUnmount), app, global), - async () => { - render({ element: null, loading: false }, 'unmounted'); - offGlobalStateChange(appInstanceId); - // for gc - appWrapperElement = null; - }, - async () => { - if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) { - prevAppUnmountedDeferred.resolve(); - } - }, - ], + return undefined; + }, + // 添加 mount hook, 确保每次应用加载前容器 dom 结构已经设置完毕 + async () => { + // element would be destroyed after unmounted, we need to recreate it if it not exist + appWrapperElement = appWrapperElement || createElement(appContent, strictStyleIsolation); + render({ element: appWrapperElement, loading: true, remountContainer }, 'mounting'); + }, + mountSandbox, + // exec the chain after rendering to keep the behavior with beforeLoad + async () => execHooksChain(toArray(beforeMount), app, global), + async props => mount({ ...props, container: appWrapperGetter(), setGlobalState, onGlobalStateChange }), + // finish loading after app mounted + async () => render({ element: appWrapperElement, loading: false, remountContainer }, 'mounted'), + async () => execHooksChain(toArray(afterMount), app, global), + // initialize the unmount defer after app mounted and resolve the defer after it unmounted + async () => { + if (await validateSingularMode(singular, app)) { + prevAppUnmountedDeferred = new Deferred(); + } + }, + async () => { + if (process.env.NODE_ENV === 'development') { + const measureName = `[qiankun] App ${appInstanceId} Loading Consuming`; + performanceMeasure(measureName, markName); + } + }, + ], + unmount: [ + async () => execHooksChain(toArray(beforeUnmount), app, global), + async props => unmount({ ...props, container: appWrapperGetter() }), + unmountSandbox, + async () => execHooksChain(toArray(afterUnmount), app, global), + async () => { + render({ element: null, loading: false, remountContainer }, 'unmounted'); + offGlobalStateChange(appInstanceId); + // for gc + appWrapperElement = null; + }, + async () => { + if ((await validateSingularMode(singular, app)) && prevAppUnmountedDeferred) { + prevAppUnmountedDeferred.resolve(); + } + }, + ], + }; + + if (typeof update === 'function') { + parcelConfig.update = update; + } + + return parcelConfig; }; - if (typeof update === 'function') { - parcelConfig.update = update; - } - - return parcelConfig; + return parcelConfigGetter; } diff --git a/src/utils.ts b/src/utils.ts index 8d3643d..5e10349 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -180,3 +180,7 @@ export function getXPathForElement(el: Node, document: Document): string | void return xpath; } + +export function getContainer(container: string | HTMLElement): HTMLElement | null { + return typeof container === 'string' ? document.querySelector(container) : container; +}