227 lines
8.0 KiB
TypeScript
227 lines
8.0 KiB
TypeScript
import { noop } from 'lodash';
|
|
import type { ParcelConfigObject } from 'single-spa';
|
|
import { mountRootParcel, registerApplication, start as startSingleSpa } from 'single-spa';
|
|
import type { ObjectType } from './interfaces';
|
|
import type { FrameworkConfiguration, FrameworkLifeCycles, LoadableApp, MicroApp, RegistrableApp } from './interfaces';
|
|
import type { ParcelConfigObjectGetter } from './loader';
|
|
import { loadApp } from './loader';
|
|
import { doPrefetchStrategy } from './prefetch';
|
|
import { Deferred, getContainer, getXPathForElement, toArray } from './utils';
|
|
|
|
let microApps: Array<RegistrableApp<Record<string, unknown>>> = [];
|
|
|
|
// eslint-disable-next-line import/no-mutable-exports
|
|
export let frameworkConfiguration: FrameworkConfiguration = {};
|
|
|
|
let started = false;
|
|
const defaultUrlRerouteOnly = true;
|
|
|
|
const frameworkStartedDefer = new Deferred<void>();
|
|
|
|
const autoDowngradeForLowVersionBrowser = (configuration: FrameworkConfiguration): FrameworkConfiguration => {
|
|
const { sandbox, singular } = configuration;
|
|
if (sandbox) {
|
|
if (!window.Proxy) {
|
|
console.warn('[qiankun] Miss window.Proxy, proxySandbox will degenerate into snapshotSandbox');
|
|
|
|
if (singular === false) {
|
|
console.warn(
|
|
'[qiankun] Setting singular as false may cause unexpected behavior while your browser not support window.Proxy',
|
|
);
|
|
}
|
|
|
|
return { ...configuration, sandbox: typeof sandbox === 'object' ? { ...sandbox, loose: true } : { loose: true } };
|
|
}
|
|
}
|
|
|
|
return configuration;
|
|
};
|
|
|
|
export function registerMicroApps<T extends ObjectType>(
|
|
apps: Array<RegistrableApp<T>>,
|
|
lifeCycles?: FrameworkLifeCycles<T>,
|
|
) {
|
|
// Each app only needs to be registered once
|
|
const unregisteredApps = apps.filter((app) => !microApps.some((registeredApp) => registeredApp.name === app.name));
|
|
|
|
microApps = [...microApps, ...unregisteredApps];
|
|
|
|
unregisteredApps.forEach((app) => {
|
|
const { name, activeRule, loader = noop, props, ...appConfig } = app;
|
|
|
|
registerApplication({
|
|
name,
|
|
app: async () => {
|
|
loader(true);
|
|
await frameworkStartedDefer.promise;
|
|
|
|
const { mount, ...otherMicroAppConfigs } = (
|
|
await loadApp({ name, props, ...appConfig }, frameworkConfiguration, lifeCycles)
|
|
)();
|
|
|
|
return {
|
|
mount: [async () => loader(true), ...toArray(mount), async () => loader(false)],
|
|
...otherMicroAppConfigs,
|
|
};
|
|
},
|
|
activeWhen: activeRule,
|
|
customProps: props,
|
|
});
|
|
});
|
|
}
|
|
|
|
const appConfigPromiseGetterMap = new Map<string, Promise<ParcelConfigObjectGetter>>();
|
|
const containerMicroAppsMap = new Map<string, MicroApp[]>();
|
|
|
|
export function loadMicroApp<T extends ObjectType>(
|
|
app: LoadableApp<T>,
|
|
configuration?: FrameworkConfiguration,
|
|
lifeCycles?: FrameworkLifeCycles<T>,
|
|
): MicroApp {
|
|
const { props, name } = app;
|
|
|
|
const getContainerXpath = (container: string | HTMLElement): string | void => {
|
|
const containerElement = getContainer(container);
|
|
if (containerElement) {
|
|
return getXPathForElement(containerElement, document);
|
|
}
|
|
|
|
return undefined;
|
|
};
|
|
|
|
let microApp: MicroApp;
|
|
const wrapParcelConfigForRemount = (config: ParcelConfigObject): ParcelConfigObject => {
|
|
const container = 'container' in app ? app.container : undefined;
|
|
|
|
let microAppConfig = config;
|
|
if (container) {
|
|
const xpath = getContainerXpath(container);
|
|
if (xpath) {
|
|
const containerMicroApps = containerMicroAppsMap.get(`${name}-${xpath}`);
|
|
if (containerMicroApps?.length) {
|
|
const mount = [
|
|
async () => {
|
|
// While there are multiple micro apps mounted on the same container, we must wait until the prev instances all had unmounted
|
|
// Otherwise it will lead some concurrent issues
|
|
const prevLoadMicroApps = containerMicroApps.slice(0, containerMicroApps.indexOf(microApp));
|
|
const prevLoadMicroAppsWhichNotBroken = prevLoadMicroApps.filter(
|
|
(v) => v.getStatus() !== 'LOAD_ERROR' && v.getStatus() !== 'SKIP_BECAUSE_BROKEN',
|
|
);
|
|
await Promise.all(prevLoadMicroAppsWhichNotBroken.map((v) => v.unmountPromise));
|
|
},
|
|
...toArray(microAppConfig.mount),
|
|
];
|
|
|
|
microAppConfig = {
|
|
...config,
|
|
mount,
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
return {
|
|
...microAppConfig,
|
|
// empty bootstrap hook which should not run twice while it calling from cached micro app
|
|
bootstrap: () => Promise.resolve(),
|
|
};
|
|
};
|
|
|
|
/**
|
|
* using name + container xpath as the micro app instance id,
|
|
* it means if you rendering a micro app to a dom which have been rendered before,
|
|
* the micro app would not load and evaluate its lifecycles again
|
|
*/
|
|
const memorizedLoadingFn = async (): Promise<ParcelConfigObject> => {
|
|
const userConfiguration = autoDowngradeForLowVersionBrowser(
|
|
configuration ?? { ...frameworkConfiguration, singular: false },
|
|
);
|
|
const { $$cacheLifecycleByAppName } = userConfiguration;
|
|
const container = 'container' in app ? app.container : undefined;
|
|
|
|
if (container) {
|
|
// using appName as cache for internal experimental scenario
|
|
if ($$cacheLifecycleByAppName) {
|
|
const parcelConfigGetterPromise = appConfigPromiseGetterMap.get(name);
|
|
if (parcelConfigGetterPromise) return wrapParcelConfigForRemount((await parcelConfigGetterPromise)(container));
|
|
}
|
|
|
|
const xpath = getContainerXpath(container);
|
|
if (xpath) {
|
|
const parcelConfigGetterPromise = appConfigPromiseGetterMap.get(`${name}-${xpath}`);
|
|
if (parcelConfigGetterPromise) return wrapParcelConfigForRemount((await parcelConfigGetterPromise)(container));
|
|
}
|
|
}
|
|
|
|
const parcelConfigObjectGetterPromise = loadApp(app, userConfiguration, lifeCycles);
|
|
|
|
if (container) {
|
|
if ($$cacheLifecycleByAppName) {
|
|
appConfigPromiseGetterMap.set(name, parcelConfigObjectGetterPromise);
|
|
} else {
|
|
const xpath = getContainerXpath(container);
|
|
if (xpath) appConfigPromiseGetterMap.set(`${name}-${xpath}`, parcelConfigObjectGetterPromise);
|
|
}
|
|
}
|
|
|
|
return (await parcelConfigObjectGetterPromise)(container);
|
|
};
|
|
|
|
if (!started) {
|
|
// We need to invoke start method of single-spa as the popstate event should be dispatched while the main app calling pushState/replaceState automatically,
|
|
// but in single-spa it will check the start status before it dispatch popstate
|
|
// see https://github.com/single-spa/single-spa/blob/f28b5963be1484583a072c8145ac0b5a28d91235/src/navigation/navigation-events.js#L101
|
|
// ref https://github.com/umijs/qiankun/pull/1071
|
|
startSingleSpa({ urlRerouteOnly: frameworkConfiguration.urlRerouteOnly ?? defaultUrlRerouteOnly });
|
|
}
|
|
|
|
microApp = mountRootParcel(memorizedLoadingFn, { domElement: document.createElement('div'), ...props });
|
|
|
|
// Store the microApps which they mounted on the same container
|
|
const container = 'container' in app ? app.container : undefined;
|
|
if (container) {
|
|
const xpath = getContainerXpath(container);
|
|
if (xpath) {
|
|
const key = `${name}-${xpath}`;
|
|
|
|
const microAppsRef = containerMicroAppsMap.get(key) || [];
|
|
microAppsRef.push(microApp);
|
|
containerMicroAppsMap.set(key, microAppsRef);
|
|
|
|
const cleanApp = () => {
|
|
const index = microAppsRef.indexOf(microApp);
|
|
microAppsRef.splice(index, 1);
|
|
// @ts-ignore
|
|
microApp = null;
|
|
};
|
|
|
|
// gc after unmount
|
|
microApp.unmountPromise.then(cleanApp).catch(cleanApp);
|
|
}
|
|
}
|
|
|
|
return microApp;
|
|
}
|
|
|
|
export function start(opts: FrameworkConfiguration = {}) {
|
|
frameworkConfiguration = { prefetch: true, singular: true, sandbox: true, ...opts };
|
|
const {
|
|
prefetch,
|
|
sandbox,
|
|
singular,
|
|
urlRerouteOnly = defaultUrlRerouteOnly,
|
|
...importEntryOpts
|
|
} = frameworkConfiguration;
|
|
|
|
if (prefetch) {
|
|
doPrefetchStrategy(microApps, prefetch, importEntryOpts);
|
|
}
|
|
|
|
frameworkConfiguration = autoDowngradeForLowVersionBrowser(frameworkConfiguration);
|
|
|
|
startSingleSpa({ urlRerouteOnly });
|
|
started = true;
|
|
|
|
frameworkStartedDefer.resolve();
|
|
}
|