qiankun-fit/src/apis.ts
2021-12-02 21:28:44 +08:00

217 lines
7.8 KiB
TypeScript

import { noop } from 'lodash';
import type { ParcelConfigObject } from 'single-spa';
import { mountRootParcel, registerApplication, start as startSingleSpa } from 'single-spa';
import type {
FrameworkConfiguration,
FrameworkLifeCycles,
LoadableApp,
MicroApp,
ObjectType,
RegistrableApp,
} from './interfaces';
import type { ParcelConfigObjectGetter } from './loader';
import { loadApp } from './loader';
import { doPrefetchStrategy } from './prefetch';
import { Deferred, getContainerXPath, toArray } from './utils';
let microApps: Array<RegistrableApp<Record<string, unknown>>> = [];
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 & { autoStart?: boolean },
lifeCycles?: FrameworkLifeCycles<T>,
): MicroApp {
const { props, name } = app;
const container = 'container' in app ? app.container : undefined;
// Must compute the container xpath at beginning to keep it consist around app running
// If we compute it every time, the container dom structure most probably been changed and result in a different xpath value
const containerXPath = getContainerXPath(container);
const appContainerXPathKey = `${name}-${containerXPath}`;
let microApp: MicroApp;
const wrapParcelConfigForRemount = (config: ParcelConfigObject): ParcelConfigObject => {
let microAppConfig = config;
if (container) {
if (containerXPath) {
const containerMicroApps = containerMicroAppsMap.get(appContainerXPathKey);
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;
if (container) {
// using appName as cache for internal experimental scenario
if ($$cacheLifecycleByAppName) {
const parcelConfigGetterPromise = appConfigPromiseGetterMap.get(name);
if (parcelConfigGetterPromise) return wrapParcelConfigForRemount((await parcelConfigGetterPromise)(container));
}
if (containerXPath) {
const parcelConfigGetterPromise = appConfigPromiseGetterMap.get(appContainerXPathKey);
if (parcelConfigGetterPromise) return wrapParcelConfigForRemount((await parcelConfigGetterPromise)(container));
}
}
const parcelConfigObjectGetterPromise = loadApp(app, userConfiguration, lifeCycles);
if (container) {
if ($$cacheLifecycleByAppName) {
appConfigPromiseGetterMap.set(name, parcelConfigObjectGetterPromise);
} else if (containerXPath) appConfigPromiseGetterMap.set(appContainerXPathKey, parcelConfigObjectGetterPromise);
}
return (await parcelConfigObjectGetterPromise)(container);
};
if (!started && configuration?.autoStart !== false) {
// 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 });
if (container) {
if (containerXPath) {
// Store the microApps which they mounted on the same container
const microAppsRef = containerMicroAppsMap.get(appContainerXPathKey) || [];
microAppsRef.push(microApp);
containerMicroAppsMap.set(appContainerXPathKey, microAppsRef);
const cleanup = () => {
const index = microAppsRef.indexOf(microApp);
microAppsRef.splice(index, 1);
// @ts-ignore
microApp = null;
};
// gc after unmount
microApp.unmountPromise.then(cleanup).catch(cleanup);
}
}
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();
}