support to pass a new container reference for remounting (#992)

This commit is contained in:
Kuitos 2020-10-12 22:41:50 +08:00 committed by GitHub
parent d2e2e760d6
commit d97bf55d20
3 changed files with 99 additions and 89 deletions

View File

@ -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 });

View File

@ -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,6 +310,7 @@ export async function loadApp<T extends object>(
offGlobalStateChange, offGlobalStateChange,
}: Record<string, Function> = getMicroAppStateActions(appInstanceId); }: Record<string, Function> = getMicroAppStateActions(appInstanceId);
const parcelConfigGetter: ParcelConfigObjectGetter = remountContainer => {
const parcelConfig: ParcelConfigObject = { const parcelConfig: ParcelConfigObject = {
name: appInstanceId, name: appInstanceId,
bootstrap, bootstrap,
@ -332,14 +335,14 @@ export async function loadApp<T extends object>(
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 () => {
@ -360,7 +363,7 @@ export async function loadApp<T extends object>(
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;
@ -378,4 +381,7 @@ export async function loadApp<T extends object>(
} }
return parcelConfig; return parcelConfig;
};
return parcelConfigGetter;
} }

View File

@ -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;
}