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>> = []; // eslint-disable-next-line import/no-mutable-exports export let frameworkConfiguration: FrameworkConfiguration = {}; let started = false; const defaultUrlRerouteOnly = true; const frameworkStartedDefer = new Deferred(); 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>(); export function loadMicroApp( app: LoadableApp, configuration?: FrameworkConfiguration, lifeCycles?: FrameworkLifeCycles, ): MicroApp { const { props, name } = app; const getContainerXpath = (container: string | HTMLElement): string | void => { const containerElement = getContainer(container); if (containerElement) { return getXPathForElement(containerElement, document); } return undefined; }; const wrapParcelConfigForRemount = (config: ParcelConfigObject): ParcelConfigObject => { return { ...config, // 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 = 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 }); } return mountRootParcel(memorizedLoadingFn, { domElement: document.createElement('div'), ...props }); } 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); } if (sandbox) { if (!window.Proxy) { console.warn('[qiankun] Miss window.Proxy, proxySandbox will degenerate into snapshotSandbox'); frameworkConfiguration.sandbox = typeof sandbox === 'object' ? { ...sandbox, loose: true } : { loose: true }; if (!singular) { console.warn( '[qiankun] Setting singular as false may cause unexpected behavior while your browser not support window.Proxy', ); } } } startSingleSpa({ urlRerouteOnly }); started = true; frameworkStartedDefer.resolve(); }