qiankun-fit/src/apis.ts

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