二.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
}