Skip to content

Commit

Permalink
✨ support to pass a new container reference for remounting (umijs#992)
Browse files Browse the repository at this point in the history
  • Loading branch information
kuitos committed Oct 12, 2020
1 parent 81fcc46 commit e75e680
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 89 deletions.
38 changes: 19 additions & 19 deletions src/apis.ts
Original file line number Diff line number Diff line change
@@ -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[] = [];

Expand All @@ -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)],
Expand All @@ -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>,
Expand All @@ -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);
}
Expand All @@ -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 });
Expand Down
146 changes: 76 additions & 70 deletions src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { FrameworkConfiguration, FrameworkLifeCycles, HTMLContentRender, LifeCyc
import { createSandbox, css } from './sandbox';
import {
Deferred,
getContainer,
getDefaultTplWrapper,
getWrapperId,
isEnableScopedCSS,
Expand Down Expand Up @@ -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;

Expand All @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -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)}`;

Expand Down Expand Up @@ -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;
}
if (typeof update === 'function') {
parcelConfig.update = update;
}

return parcelConfig;
};

return parcelConfig;
return parcelConfigGetter;
}
4 changes: 4 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

0 comments on commit e75e680

Please sign in to comment.