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, isConstDestructAssignmentSupported, toArray } from './utils'; let microApps: Array>> = []; export let frameworkConfiguration: FrameworkConfiguration = {}; let started = false; const defaultUrlRerouteOnly = true; const frameworkStartedDefer = new Deferred(); const autoDowngradeForLowVersionBrowser = (configuration: FrameworkConfiguration): FrameworkConfiguration => { const { sandbox = true, singular } = configuration; if (sandbox) { if (!window.Proxy) { console.warn('[qiankun] Missing 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 } }; } if ( !isConstDestructAssignmentSupported() && (sandbox === true || (typeof sandbox === 'object' && sandbox.speedy !== false)) ) { console.warn( '[qiankun] Speedy mode will turn off as const destruct assignment not supported in current browser!', ); return { ...configuration, sandbox: typeof sandbox === 'object' ? { ...sandbox, speedy: false } : { speedy: false }, }; } } return configuration; }; export function registerMicroApps( apps: Array>, lifeCycles?: FrameworkLifeCycles, ) { // 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>(); const containerMicroAppsMap = new Map(); export function loadMicroApp( app: LoadableApp, configuration?: FrameworkConfiguration & { autoStart?: boolean }, lifeCycles?: FrameworkLifeCycles, ): 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 => { 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, urlRerouteOnly = defaultUrlRerouteOnly, ...importEntryOpts } = frameworkConfiguration; if (prefetch) { doPrefetchStrategy(microApps, prefetch, importEntryOpts); } frameworkConfiguration = autoDowngradeForLowVersionBrowser(frameworkConfiguration); startSingleSpa({ urlRerouteOnly }); started = true; frameworkStartedDefer.resolve(); }