✨ support to pass a new container reference for remounting (#992)
This commit is contained in:
parent
d2e2e760d6
commit
d97bf55d20
38
src/apis.ts
38
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<T extends object = {}>(
|
|||
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<T extends object = {}>(
|
|||
});
|
||||
}
|
||||
|
||||
const appConfigMap = new Map<string, Promise<ParcelConfigObject>>();
|
||||
const appConfigPormiseGetterMap = new Map<string, Promise<ParcelConfigObjectGetter>>();
|
||||
|
||||
export function loadMicroApp<T extends object = {}>(
|
||||
app: LoadableApp<T>,
|
||||
|
|
@ -56,7 +54,7 @@ export function loadMicroApp<T extends object = {}>(
|
|||
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<T extends object = {}>(
|
|||
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 });
|
||||
|
|
|
|||
146
src/loader.ts
146
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<any>, appName: strin
|
|||
|
||||
let prevAppUnmountedDeferred: Deferred<void>;
|
||||
|
||||
export type ParcelConfigObjectGetter = (remountContainer?: string | HTMLElement) => ParcelConfigObject;
|
||||
export async function loadApp<T extends object>(
|
||||
app: LoadableApp<T>,
|
||||
configuration: FrameworkConfiguration = {},
|
||||
lifeCycles?: FrameworkLifeCycles<T>,
|
||||
): Promise<ParcelConfigObject> {
|
||||
): Promise<ParcelConfigObjectGetter> {
|
||||
const { entry, name: appName } = app;
|
||||
const appInstanceId = `${appName}_${+new Date()}_${Math.floor(Math.random() * 1000)}`;
|
||||
|
||||
|
|
@ -308,74 +310,78 @@ export async function loadApp<T extends object>(
|
|||
offGlobalStateChange,
|
||||
}: Record<string, Function> = 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<void>();
|
||||
}
|
||||
},
|
||||
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<void>();
|
||||
}
|
||||
},
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user