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