Skip to content

二.qiankun(源码解析)

  • qiankun@2.6.3项目目录结构

    bash
    ├── src              # 源码文件夹
       ├── assets       # 存放组件中的静态资源
       ├── apis.ts   # 存放一些公共组件
       ├── effects.ts        # 存放所有的路由组件
       ├── error.ts      # 应用根主组件
       ├── index.ts      # 应用入口
       ├── router       # 路由配置文件
       └── store        # vuex状态管理文件

1.入口

ts
export { loadMicroApp, registerMicroApps, start } from "./apis"
export { initGlobalState } from "./globalState"
export { getCurrentRunningApp as __internalGetCurrentRunningApp } from "./sandbox"
export * from "./errorHandler"
export * from "./effects"
export * from "./interfaces"
export { prefetchImmediately as prefetchApps } from "./prefetch"

2.apis

ts
let microApps: Array<RegistrableApp<Record<string, unknown>>> = []
export let frameworkConfiguration: FrameworkConfiguration = {};
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({
      //调用single-spa的方法注册子项目
      name,
      app: async () => {
        loader(true)
        await frameworkStartedDefer.promise // promise等待resolve

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

3.loader.ts

ts
export async function loadApp<T extends ObjectType>(
  app: LoadableApp<T>,
  configuration: FrameworkConfiguration = {},
  lifeCycles?: FrameworkLifeCycles<T>
): Promise<ParcelConfigObjectGetter> {
  const { entry, name: appName } = app
  const appInstanceId = genAppInstanceIdByName(appName)

  const markName = `[qiankun] App ${appInstanceId} Loading`
  if (process.env.NODE_ENV === "development") {
    performanceMark(markName)
  }

  const {
    singular = false,
    sandbox = true,
    excludeAssetFilter,
    globalContext = window,
    ...importEntryOpts
  } = configuration

  // get the entry html content and script executor
  const { template, execScripts, assetPublicPath } = await importEntry( //核心部分获取子应用的 HTML 和 JS,同时对 HTML 和 JS 进行了各自的处理,以便于子应用在父应用中加载
    entry,
    importEntryOpts
  )

  // as single-spa load and bootstrap new app parallel with other apps unmounting
  // (see https://github.com/CanopyTax/single-spa/blob/master/src/navigation/reroute.js#L74)
  // we need wait to load the app until all apps are finishing unmount in singular mode
  if (await validateSingularMode(singular, app)) {
    await (prevAppUnmountedDeferred && prevAppUnmountedDeferred.promise)
  }

  const appContent = getDefaultTplWrapper(appInstanceId)(template)

  const strictStyleIsolation =
    typeof sandbox === "object" && !!sandbox.strictStyleIsolation

  if (process.env.NODE_ENV === "development" && strictStyleIsolation) {
    console.warn(
      "[qiankun] strictStyleIsolation configuration will be removed in 3.0, pls don't depend on it or use experimentalStyleIsolation instead!"
    )
  }

  const scopedCSS = isEnableScopedCSS(sandbox)
  let initialAppWrapperElement: HTMLElement | null = createElement(
    appContent,
    strictStyleIsolation,
    scopedCSS,
    appInstanceId
  )

  const initialContainer = "container" in app ? app.container : undefined
  const legacyRender = "render" in app ? app.render : undefined

  const render = getRender(appInstanceId, appContent, legacyRender)

  // 第一次加载设置应用可见区域 dom 结构
  // 确保每次应用加载前容器 dom 结构已经设置完毕
  render(
    {
      element: initialAppWrapperElement,
      loading: true,
      container: initialContainer,
    },
    "loading"
  )

  const initialAppWrapperGetter = getAppWrapperGetter(
    appInstanceId,
    !!legacyRender,
    strictStyleIsolation,
    scopedCSS,
    () => initialAppWrapperElement
  )

  let global = globalContext
  let mountSandbox = () => Promise.resolve()
  let unmountSandbox = () => Promise.resolve()
  const useLooseSandbox = typeof sandbox === "object" && !!sandbox.loose
  let sandboxContainer
  if (sandbox) {
    sandboxContainer = createSandboxContainer(
      appInstanceId,
      // FIXME should use a strict sandbox logic while remount, see https://github.com/umijs/qiankun/issues/518
      initialAppWrapperGetter,
      scopedCSS,
      useLooseSandbox,
      excludeAssetFilter,
      global
    )
    // 用沙箱的代理对象作为接下来使用的全局对象
    global = sandboxContainer.instance.proxy as typeof window
    mountSandbox = sandboxContainer.mount
    unmountSandbox = sandboxContainer.unmount
  }

  const {
    beforeUnmount = [],
    afterUnmount = [],
    afterMount = [],
    beforeMount = [],
    beforeLoad = [],
  } = mergeWith({}, getAddOns(global, assetPublicPath), lifeCycles, (v1, v2) =>
    concat(v1 ?? [], v2 ?? [])
  )

  await execHooksChain(toArray(beforeLoad), app, global)

  // get the lifecycle hooks from module exports
  const scriptExports: any = await execScripts(
    global,
    sandbox && !useLooseSandbox
  )
  const { bootstrap, mount, unmount, update } = getLifecyclesFromExports(
    scriptExports,
    appName,
    global,
    sandboxContainer?.instance?.latestSetProp
  )

  const {
    onGlobalStateChange,
    setGlobalState,
    offGlobalStateChange,
  }: Record<string, CallableFunction> = getMicroAppStateActions(appInstanceId)

  // FIXME temporary way
  const syncAppWrapperElement2Sandbox = (element: HTMLElement | null) =>
    (initialAppWrapperElement = element)

  const parcelConfigGetter: ParcelConfigObjectGetter = (
    remountContainer = initialContainer
  ) => {
    let appWrapperElement: HTMLElement | null
    let appWrapperGetter: ReturnType<typeof getAppWrapperGetter>

    const parcelConfig: ParcelConfigObject = {
      name: appInstanceId,
      bootstrap,
      mount: [
        async () => {
          if (process.env.NODE_ENV === "development") {
            const marks = performanceGetEntriesByName(markName, "mark")
            // mark length is zero means the app is remounting
            if (marks && !marks.length) {
              performanceMark(markName)
            }
          }
        },
        async () => {
          if (
            (await validateSingularMode(singular, app)) &&
            prevAppUnmountedDeferred
          ) {
            return prevAppUnmountedDeferred.promise
          }

          return undefined
        },
        // initial wrapper element before app mount/remount
        async () => {
          appWrapperElement = initialAppWrapperElement
          appWrapperGetter = getAppWrapperGetter(
            appInstanceId,
            !!legacyRender,
            strictStyleIsolation,
            scopedCSS,
            () => appWrapperElement
          )
        },
        // 添加 mount hook, 确保每次应用加载前容器 dom 结构已经设置完毕
        async () => {
          const useNewContainer = remountContainer !== initialContainer
          if (useNewContainer || !appWrapperElement) {
            // element will be destroyed after unmounted, we need to recreate it if it not exist
            // or we try to remount into a new container
            appWrapperElement = createElement(
              appContent,
              strictStyleIsolation,
              scopedCSS,
              appInstanceId
            )
            syncAppWrapperElement2Sandbox(appWrapperElement)
          }

          render(
            {
              element: appWrapperElement,
              loading: true,
              container: remountContainer,
            },
            "mounting"
          )
        },
        mountSandbox,
        // exec the chain after rendering to keep the behavior with beforeLoad
        async () => execHooksChain(toArray(beforeMount), app, global),
        async (props) =>
          mount({
            ...props,
            container: appWrapperGetter(),
            setGlobalState,
            onGlobalStateChange,
          }),
        // finish loading after app mounted
        async () =>
          render(
            {
              element: appWrapperElement,
              loading: false,
              container: remountContainer,
            },
            "mounted"
          ),
        async () => execHooksChain(toArray(afterMount), app, global),
        // initialize the unmount defer after app mounted and resolve the defer after it unmounted
        async () => {
          if (await validateSingularMode(singular, app)) {
            prevAppUnmountedDeferred = new Deferred<void>()
          }
        },
        async () => {
          if (process.env.NODE_ENV === "development") {
            const measureName = `[qiankun] App ${appInstanceId} Loading Consuming`
            performanceMeasure(measureName, markName)
          }
        },
      ],
      unmount: [
        async () => execHooksChain(toArray(beforeUnmount), app, global),
        async (props) => unmount({ ...props, container: appWrapperGetter() }),
        unmountSandbox,
        async () => execHooksChain(toArray(afterUnmount), app, global),
        async () => {
          render(
            { element: null, loading: false, container: remountContainer },
            "unmounted"
          )
          offGlobalStateChange(appInstanceId)
          // for gc
          appWrapperElement = null
          syncAppWrapperElement2Sandbox(appWrapperElement)
        },
        async () => {
          if (
            (await validateSingularMode(singular, app)) &&
            prevAppUnmountedDeferred
          ) {
            prevAppUnmountedDeferred.resolve()
          }
        },
      ],
    }

    if (typeof update === "function") {
      parcelConfig.update = update
    }

    return parcelConfig
  }

  return parcelConfigGetter
}