From e5c9f14c29446f761b3f3095462eb82b80b29096 Mon Sep 17 00:00:00 2001 From: njikm2010 <736155049@qq.com> Date: Tue, 5 Sep 2023 22:40:24 +0800 Subject: [PATCH 1/6] =?UTF-8?q?fix(mapp):=20=E4=BF=AE=E5=A4=8D`appContext`?= =?UTF-8?q?=E5=BD=93=E5=89=8D=E5=BA=94=E7=94=A8=E7=8A=B6=E6=80=81=E4=B8=8D?= =?UTF-8?q?=E6=AD=A3=E7=A1=AE=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change-Id: Ic69da7ba17fa3e3bf41425f262046fc2d49a9f63 --- packages/mapp/src/main/Bay/AppContext.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/mapp/src/main/Bay/AppContext.ts b/packages/mapp/src/main/Bay/AppContext.ts index c406d91..8284974 100644 --- a/packages/mapp/src/main/Bay/AppContext.ts +++ b/packages/mapp/src/main/Bay/AppContext.ts @@ -125,6 +125,8 @@ export class AppContext { afterMount: LifeCycleFn = async (app, global) => { this.triggerHooks('afterAppMount', this.getApp(app)); + this.currentMountingApp.value = undefined; + this.currentLoadingApp.value = undefined; }; beforeUnmount: LifeCycleFn = async (app, global) => { @@ -133,6 +135,7 @@ export class AppContext { afterUnmount: LifeCycleFn = async (app, global) => { this.triggerHooks('afterAppUnmount', this.getApp(app)); + this.currentApp.value = undefined; }; private getApp(app: LoadableApp) { @@ -142,4 +145,6 @@ export class AppContext { private triggerHooks>(name: Name, option: Option) { (this.bay as any).triggerHooks(name, option); } + + } From 3ed156c08d6fd3a2eec8395d51767393675b46ef Mon Sep 17 00:00:00 2001 From: njikm2010 <736155049@qq.com> Date: Wed, 6 Sep 2023 20:38:02 +0800 Subject: [PATCH 2/6] =?UTF-8?q?feat(mapp):=20=E7=8E=B0=E5=9C=A8=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E5=8A=A0=E8=BD=BDvite=E5=BA=94=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change-Id: I172a02179cbb738679db8e08f3df5073c75534fa --- packages/mapp-shared/index.d.ts | 11 ++ packages/mapp/package.json | 10 +- packages/mapp/src/main/Bay/Bay.ts | 36 ++-- packages/mapp/src/main/Bay/ParcelContext.ts | 198 ++++++++++++++++++++ packages/mapp/src/main/Bay/options.ts | 36 +++- packages/mapp/src/main/Bay/shared.ts | 6 + packages/mapp/src/main/Bay/types.ts | 5 + packages/mapp/src/main/Bay/utils.ts | 30 +++ packages/mapp/src/types/bay.ts | 2 + 9 files changed, 317 insertions(+), 17 deletions(-) create mode 100644 packages/mapp/src/main/Bay/ParcelContext.ts create mode 100644 packages/mapp/src/main/Bay/shared.ts diff --git a/packages/mapp-shared/index.d.ts b/packages/mapp-shared/index.d.ts index fd22099..0beb801 100644 --- a/packages/mapp-shared/index.d.ts +++ b/packages/mapp-shared/index.d.ts @@ -50,6 +50,17 @@ export interface MicroApp { */ independent?: boolean; + /** + * 可选,默认为 false。 + * + * 开启时将认为该应用为一个支持现代浏览器的应用 + * + * 因此将允许使用 module script 这些功能 + * + * 该功能只工作在开发模式下 并且不支持沙盒隔离 + */ + modern?: boolean; + /** * 路由模式,默认为 hash */ diff --git a/packages/mapp/package.json b/packages/mapp/package.json index 1a5f1d7..616def6 100644 --- a/packages/mapp/package.json +++ b/packages/mapp/package.json @@ -21,11 +21,13 @@ "author": "ivan-lee", "license": "MIT", "dependencies": { + "@qiankunjs/loader": "0.0.1-alpha.4", "@wakeadmin/h": "workspace:^0.3.0", "@wakeadmin/mapp-shared": "workspace:^0.1.1", "@wakeadmin/utils": "workspace:^0.1.5", "path-browserify": "^1.0.1", - "qiankun": "^2.10.8" + "qiankun": "^2.10.13", + "single-spa": "^5.9.5" }, "peerDependencies": { "vue": "^3.2.37", @@ -33,5 +35,11 @@ }, "jest": { "preset": "jest-preset-universe-vue" + }, + "resolutions": { + "single-spa": "^5.9.5" + }, + "devDependencies": { + "@types/semver": "^7.5.1" } } diff --git a/packages/mapp/src/main/Bay/Bay.ts b/packages/mapp/src/main/Bay/Bay.ts index c85d560..b8eb7b3 100644 --- a/packages/mapp/src/main/Bay/Bay.ts +++ b/packages/mapp/src/main/Bay/Bay.ts @@ -1,30 +1,31 @@ -import { createApp, App } from 'vue'; -import { createRouter, createWebHistory, Router } from 'vue-router'; import { EventEmitter, trimQueryAndHash } from '@wakeadmin/utils'; -import { registerMicroApps, start, RegistrableApp, prefetchApps, addGlobalUncaughtErrorHandler } from 'qiankun'; +import { RegistrableApp, addGlobalUncaughtErrorHandler, prefetchApps, registerMicroApps, start } from 'qiankun'; +import { App, createApp } from 'vue'; +import { Router, createRouter, createWebHistory } from 'vue-router'; import { BayHooks, BayOptions, IBay, - Parameter, INetworkInterceptorRegister, MicroApp, MicroAppStatus, + Parameter, } from '../../types'; +import { AJAXInterceptor, FetchInterceptor } from '../NetworkInterceptor'; +import { UniverseHistory } from '../UniverseHistory'; import { NoopPage } from '../components'; import { BayProviderContext, DEFAULT_ROOT } from '../constants'; -import { UniverseHistory } from '../UniverseHistory'; -import { AJAXInterceptor, FetchInterceptor } from '../NetworkInterceptor'; -import { groupAppsByIndependent, normalizeOptions } from './options'; -import { createRoutes, generateLandingUrl, Navigator } from './route'; import { AppContext } from './AppContext'; -import { normalizeUrl } from './utils'; -import { MicroAppNormalized } from './types'; -import { flushMountQueue } from './mount-delay'; +import { ParcelContext } from './ParcelContext'; import { AssetFilter } from './exclude-asset'; +import { flushMountQueue } from './mount-delay'; +import { groupAppsByIndependent, normalizeModernApps, normalizeOptions } from './options'; +import { Navigator, createRoutes, generateLandingUrl } from './route'; +import { MicroAppNormalized } from './types'; +import { normalizeUrl } from './utils'; export class Bay implements IBay { app: App; @@ -55,6 +56,10 @@ export class Bay implements IBay { nonIndependentApps: MicroAppNormalized[]; + modernApps: MicroAppNormalized[] = []; + + private parcelContext: ParcelContext; + get location() { return this.history.location; } @@ -113,11 +118,15 @@ export class Bay implements IBay { this.independentApps = independentApps; this.nonIndependentApps = nonIndependentApps; + const modernApps = this.apps.filter(app => app.modern).map(app => normalizeModernApps(app)); + if (this.options.networkInterceptors?.length) { this.registerNetworkInterceptor(...this.options.networkInterceptors); } + this.navigator = new Navigator(this); this.appContext = new AppContext(this); + this.parcelContext = new ParcelContext(modernApps, this.appContext); if (options.excludeAssetFilter) { this.assetFilter.addFiler(options.excludeAssetFilter); @@ -130,6 +139,7 @@ export class Bay implements IBay { this.router = this.createRouter(); this.history = new UniverseHistory(l => { this.triggerHooks('locationChange', l); + this.parcelContext.mountOrUnmountAppIfNeed(); }); this.app.use(this.router); @@ -172,6 +182,8 @@ export class Bay implements IBay { }, }); + this.parcelContext.mountOrUnmountAppIfNeed(); + this.started = true; } @@ -288,7 +300,7 @@ export class Bay implements IBay { } private registerApps() { - const apps = this.apps; + const apps = this.apps.filter(app => !app.modern); this.triggerHooks('beforeAppsRegister', apps); diff --git a/packages/mapp/src/main/Bay/ParcelContext.ts b/packages/mapp/src/main/Bay/ParcelContext.ts new file mode 100644 index 0000000..28be533 --- /dev/null +++ b/packages/mapp/src/main/Bay/ParcelContext.ts @@ -0,0 +1,198 @@ +import { mountRootParcel, type Parcel } from 'single-spa'; +import { AppContext } from './AppContext'; +import { MicroAppNormalized, ModernMicroAppNormalized } from './types'; + +import { loadEntry } from '@qiankunjs/loader'; +import { Noop } from '@wakeadmin/utils'; +import isFunction from 'lodash/isFunction'; +import { parcelUnmountDeferred, qiankunUnmountDeferred } from './shared'; +import { Deferred } from './utils'; + +const isDev = process.env.NODE_ENV === 'development'; + +const LOG_PREFIX = '[Single SPA Parcel] '; + +export class ParcelContext { + private parcelInstance?: Parcel; + + private currentApp?: MicroAppNormalized; + + private isLoading: boolean = false; + + private mountDeferredWeakMap: WeakMap> = new WeakMap(); + + constructor(private apps: ModernMicroAppNormalized[], private appContext: AppContext) {} + + mountOrUnmountAppIfNeed(container?: HTMLElement) { + const app = this.getMatchedApp(); + if (!app) { + return this.unmountAppIfNeed(); + } + return this.mountApp(app, container); + } + + private async mountApp(app: ModernMicroAppNormalized, container?: HTMLElement) { + isDev && console.log(`${LOG_PREFIX}匹配应用 `, app.name); + + if (this.currentApp?.name === app.name) { + return; + } + + if (this.parcelInstance) { + await this.unmountApp(); + this.parcelInstance = undefined; + } + + if ( + this.appContext.currentApp.value || + this.appContext.currentLoadingApp.value || + this.appContext.currentMountingApp.value + ) { + isDev && console.log(`${LOG_PREFIX}等待qiankun卸载子应用 -> `, this.appContext.currentApp.value?.name); + + await qiankunUnmountDeferred.promise; + isDev && console.log(`${LOG_PREFIX}qiankun卸载子应用完成 -> `, this.appContext.currentApp.value?.name); + } + + this.loadApp(app, container); + } + + private unmountAppIfNeed() { + if (this.parcelInstance) { + this.isLoading = false; + return this.unmountApp(); + } + + return Promise.resolve(); + } + + private getMatchedApp(location: Location = window.location): ModernMicroAppNormalized | undefined { + return this.apps.find(item => item.activeRuleWhen.some(fn => fn(location))); + } + + private unmountApp() { + if (this.parcelInstance) { + isDev && console.debug(`${LOG_PREFIX}卸载子应用 -> ${this.currentApp?.name}`); + + return this.parcelInstance.unmount().then(() => { + const deferred = this.mountDeferredWeakMap.get(this.parcelInstance!); + if (deferred) { + deferred.reject('unmount'); + } + this.parcelInstance = undefined; + }); + } + + return Promise.resolve(); + } + + // 并发可能还是有点问题 + private async loadApp(app: ModernMicroAppNormalized, target?: HTMLElement): Promise { + if (this.isLoading && this.currentApp?.name === app.name) { + return await this.mountDeferredWeakMap.get(this.parcelInstance!)!.promise; + } + + this.currentApp = app; + this.isLoading = true; + + isDev && console.debug(`${LOG_PREFIX}开始加载子应用 -> ${app.name}`); + + await app.loader!(true); + + const container = + target ?? + ((typeof app.container === 'string' ? document.querySelector(app.container) : app.container!) as HTMLElement); + + const loadedApp = Object.create(app, { + container: { + value: container, + }, + }); + + this.appContext.beforeLoad(loadedApp, window); + + await loadEntry(app.entry, container, { + fetch: window.fetch, + }); + + isDev && console.debug(`${LOG_PREFIX}加载子应用完成 -> ${app.name}`); + + // 这里重新判断下当前正在加载的子应用是否是最新的 + // --mountA-----------loadedA---- + // ----mountB----loadedB--------- + // 假设有两个并发如上 可以发现A会在B后面运行下面的代码 从而导致子应用不正确 + if (this.currentApp.name !== app.name) { + throw new Error('unmont'); + } + + const { bootstrap, mount, unmount, update } = this.getAppLifeCycles(app.name); + + const parcelConfig = { + name: app.name, + + bootstrap, + + mount: [ + () => { + isDev && console.debug(`${LOG_PREFIX}开始挂载子应用 -> ${app.name}; 挂载点: `, container); + parcelUnmountDeferred.resolve(); + parcelUnmountDeferred.reset(); + return this.appContext.beforeMount(loadedApp, window); + }, + (props: Record) => + mount({ ...props, container }).catch((err: unknown) => + console.debug(`${LOG_PREFIX}子应用 (${app.name}) 挂载失败`, err) + ), + () => { + isDev && console.debug(`${LOG_PREFIX}挂载子应用完成 -> ${app.name}`); + this.isLoading = false; + return this.appContext.afterMount(loadedApp, window); + }, + ], + + unmount: [ + () => { + isDev && console.debug(`${LOG_PREFIX}开始卸载子应用 -> ${app.name}`); + return this.appContext.beforeUnmount(loadedApp, window); + }, + (props: Record) => + unmount({ ...props, container }).catch((err: unknown) => + console.debug(`${LOG_PREFIX}子应用 (${app.name}) 卸载失败`, err) + ), + () => { + isDev && console.debug(`${LOG_PREFIX}卸载子应用完成 -> ${app.name}`); + parcelUnmountDeferred.resolve(); + this.currentApp = undefined; + return this.appContext.afterUnmount(loadedApp, window); + }, + ], + + update: Noop, + }; + + if (update) { + parcelConfig.update = update; + } + + this.parcelInstance = mountRootParcel(parcelConfig, { domElement: container }); + } + + private getAppLifeCycles(appName: string) { + // @ts-expect-error + const obj = window[appName]; + + if (!obj) { + throw new Error(`${LOG_PREFIX}无法获取到子应用 (${appName}) 的生命周期函数`); + } + + const { bootstrap, mount, unmount, update } = obj; + + if (isFunction(bootstrap) && isFunction(mount) && isFunction(unmount)) { + return { bootstrap, mount, unmount, update }; + } + + throw new Error( + `${LOG_PREFIX}子应用 (${appName}) 的生命周期函数异常,请确保 bootstrap, mount, unmount 三个字段为函数` + ); + } +} diff --git a/packages/mapp/src/main/Bay/options.ts b/packages/mapp/src/main/Bay/options.ts index 4d548e8..7f679c0 100644 --- a/packages/mapp/src/main/Bay/options.ts +++ b/packages/mapp/src/main/Bay/options.ts @@ -1,13 +1,15 @@ /* eslint-disable no-lone-blocks */ import { removeTrailingSlash } from '@wakeadmin/utils'; import path from 'path-browserify'; +import { pathToActiveWhen } from 'single-spa'; import { BayOptions, MicroApp } from '../../types'; import { DEFAULT_ROOT_FOR_CHILD, MAX_WAIT_TIMES } from '../constants'; import { pushMountQueue } from './mount-delay'; -import { MicroAppNormalized } from './types'; -import { normalizeUrl, trimBaseUrl } from './utils'; +import { parcelUnmountDeferred, qiankunUnmountDeferred } from './shared'; +import { MicroAppNormalized, ModernMicroAppNormalized } from './types'; +import { normalizeUrl, runPromiseChain, toArray, trimBaseUrl } from './utils'; function hasContainer(container: string | HTMLElement) { if (typeof container === 'string') { @@ -181,13 +183,39 @@ export function groupAppsByIndependent(apps: MicroAppNormalized[]) { * @returns */ export function normalizeOptions(options: BayOptions): BayOptions { - let { baseUrl = process.env.MAPP_BASE_URL ?? '/', apps, bayReady, ...others } = options; + let { baseUrl = process.env.MAPP_BASE_URL ?? '/', hooks = {}, apps, bayReady, ...others } = options; baseUrl = normalizeUrl(baseUrl); return { baseUrl, - apps: normalizeApps(baseUrl, apps, bayReady), + apps: normalizeApps( + baseUrl, + apps, + runPromiseChain([bayReady || (() => Promise.resolve()), () => parcelUnmountDeferred.promise]) + ), + hooks: { + ...hooks, + beforeAppMount: app => { + if (hooks.beforeAppMount) { + hooks.beforeAppMount(app); + } + // 不做任何操作 暂时先直接重置掉 + qiankunUnmountDeferred.reset(); + }, + afterAppUnmount(app) { + if (hooks.afterAppUnmount) { + hooks.afterAppUnmount(app); + } + qiankunUnmountDeferred.resolve(); + }, + }, ...others, }; } + +export function normalizeModernApps(app: MicroAppNormalized): ModernMicroAppNormalized { + return Object.assign(app, { + activeRuleWhen: toArray(app.activeRule).map(rule => pathToActiveWhen(rule, true)), + }); +} diff --git a/packages/mapp/src/main/Bay/shared.ts b/packages/mapp/src/main/Bay/shared.ts new file mode 100644 index 0000000..24cf6de --- /dev/null +++ b/packages/mapp/src/main/Bay/shared.ts @@ -0,0 +1,6 @@ +import { Deferred } from './utils'; + +export const parcelUnmountDeferred = new Deferred(); +parcelUnmountDeferred.resolve(); +export const qiankunUnmountDeferred = new Deferred(); +qiankunUnmountDeferred.resolve(); diff --git a/packages/mapp/src/main/Bay/types.ts b/packages/mapp/src/main/Bay/types.ts index 2ebe272..04ae3f0 100644 --- a/packages/mapp/src/main/Bay/types.ts +++ b/packages/mapp/src/main/Bay/types.ts @@ -1,4 +1,5 @@ import { MicroApp } from '../../types'; +import { type ActivityFn } from 'single-spa'; export interface MicroAppNormalized extends MicroApp { /** @@ -8,3 +9,7 @@ export interface MicroAppNormalized extends MicroApp { loader?: (loading: boolean) => Promise; } + +export interface ModernMicroAppNormalized extends MicroAppNormalized { + activeRuleWhen: ActivityFn[]; +} diff --git a/packages/mapp/src/main/Bay/utils.ts b/packages/mapp/src/main/Bay/utils.ts index 363bde2..fa1271c 100644 --- a/packages/mapp/src/main/Bay/utils.ts +++ b/packages/mapp/src/main/Bay/utils.ts @@ -17,3 +17,33 @@ export function normalizeUrl(url: string) { export function trimBaseUrl(baseUrl: string, path: string) { return normalizeUrl(normalizeUrl(path).replace(normalizeUrl(baseUrl), '')); } + +export function toArray(input: T): T extends any[] ? T : T[] { + // @ts-expect-error + return Array.isArray(input) ? input : [input]; +} + +export function runPromiseChain(this: any, chain: any[]): () => Promise { + // eslint-disable-next-line @typescript-eslint/no-this-alias + const that = this; + return (...args) => chain.reduce((pre, cur) => pre.then(() => cur.apply(that, args)), Promise.resolve()); +} + +export class Deferred { + promise!: Promise; + + resolve!: (value: T | PromiseLike) => void; + + reject!: (reason?: unknown) => void; + + constructor() { + this.reset(); + } + + reset() { + this.promise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); + } +} diff --git a/packages/mapp/src/types/bay.ts b/packages/mapp/src/types/bay.ts index b95f999..ecf25ef 100644 --- a/packages/mapp/src/types/bay.ts +++ b/packages/mapp/src/types/bay.ts @@ -217,6 +217,8 @@ export interface IBay extends IBayBase { nonIndependentApps: MicroApp[]; + modernApps: MicroApp[]; + /** * 当前激活的微应用 */ From 2c0316c13580fcb853b68416cc61f50a87eaec3c Mon Sep 17 00:00:00 2001 From: njikm2010 <736155049@qq.com> Date: Thu, 7 Sep 2023 13:50:19 +0800 Subject: [PATCH 3/6] =?UTF-8?q?fix(mapp):=20=E4=BC=98=E5=8C=96=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E6=8F=90=E7=A4=BA,=20=E4=BF=AE=E5=A4=8D`mountDeferred?= =?UTF-8?q?`=E6=B2=A1=E6=9C=89=E6=AD=A3=E5=B8=B8`resolve`=E7=9A=84?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change-Id: Ia997735d554c5b2984501bb758166482b6453f04 --- packages/mapp/src/main/Bay/ParcelContext.ts | 68 +++++++++++++++------ packages/mapp/src/main/Bay/deferred.ts | 24 ++++++++ packages/mapp/src/main/Bay/options.ts | 3 +- packages/mapp/src/main/Bay/shared.ts | 6 -- packages/mapp/src/main/Bay/utils.ts | 19 ------ 5 files changed, 73 insertions(+), 47 deletions(-) create mode 100644 packages/mapp/src/main/Bay/deferred.ts delete mode 100644 packages/mapp/src/main/Bay/shared.ts diff --git a/packages/mapp/src/main/Bay/ParcelContext.ts b/packages/mapp/src/main/Bay/ParcelContext.ts index 28be533..53d46b3 100644 --- a/packages/mapp/src/main/Bay/ParcelContext.ts +++ b/packages/mapp/src/main/Bay/ParcelContext.ts @@ -1,19 +1,21 @@ -import { mountRootParcel, type Parcel } from 'single-spa'; +import { mountRootParcel, type ParcelConfig, type Parcel } from 'single-spa'; import { AppContext } from './AppContext'; import { MicroAppNormalized, ModernMicroAppNormalized } from './types'; import { loadEntry } from '@qiankunjs/loader'; import { Noop } from '@wakeadmin/utils'; import isFunction from 'lodash/isFunction'; -import { parcelUnmountDeferred, qiankunUnmountDeferred } from './shared'; -import { Deferred } from './utils'; +import { parcelUnmountDeferred, qiankunUnmountDeferred, Deferred } from './deferred'; const isDev = process.env.NODE_ENV === 'development'; const LOG_PREFIX = '[Single SPA Parcel] '; +const PARCEL_INSTANCE_APP_NAME = '__$$app_name__'; export class ParcelContext { - private parcelInstance?: Parcel; + private parcelInstance?: Parcel & { + [PARCEL_INSTANCE_APP_NAME]: string; + }; private currentApp?: MicroAppNormalized; @@ -40,7 +42,6 @@ export class ParcelContext { if (this.parcelInstance) { await this.unmountApp(); - this.parcelInstance = undefined; } if ( @@ -75,9 +76,11 @@ export class ParcelContext { isDev && console.debug(`${LOG_PREFIX}卸载子应用 -> ${this.currentApp?.name}`); return this.parcelInstance.unmount().then(() => { - const deferred = this.mountDeferredWeakMap.get(this.parcelInstance!); - if (deferred) { - deferred.reject('unmount'); + if (isDev) { + const deferred = this.mountDeferredWeakMap.get(this.parcelInstance!); + deferred?.reject( + `${LOG_PREFIX}子应用(${this.parcelInstance![PARCEL_INSTANCE_APP_NAME]})挂载失败 -> 当前子应用已被卸载` + ); } this.parcelInstance = undefined; }); @@ -86,15 +89,36 @@ export class ParcelContext { return Promise.resolve(); } - // 并发可能还是有点问题 private async loadApp(app: ModernMicroAppNormalized, target?: HTMLElement): Promise { - if (this.isLoading && this.currentApp?.name === app.name) { - return await this.mountDeferredWeakMap.get(this.parcelInstance!)!.promise; + if (this.isLoading && this.parcelInstance?.[PARCEL_INSTANCE_APP_NAME] === app.name) { + // eslint-disable-next-line @typescript-eslint/return-await + return this.mountDeferredWeakMap.get(this.parcelInstance)!.promise; } - this.currentApp = app; this.isLoading = true; + // FIXME: 缓存对应的返回值 但是这样会导致vue应用没法正常显示 + // 暂时先不用缓存 没什么太大影响 + const { config, container } = await this.load(app, target); + + const instance = mountRootParcel(config, { domElement: container }); + + // @ts-expect-error + instance[PARCEL_INSTANCE_APP_NAME] = app.name; + + this.mountDeferredWeakMap.set(instance, new Deferred()); + this.parcelInstance = instance as any; + } + + private async load( + app: ModernMicroAppNormalized, + target: HTMLElement | undefined + ): Promise<{ + container: HTMLElement; + config: ParcelConfig; + }> { + this.currentApp = app; + isDev && console.debug(`${LOG_PREFIX}开始加载子应用 -> ${app.name}`); await app.loader!(true); @@ -118,16 +142,18 @@ export class ParcelContext { isDev && console.debug(`${LOG_PREFIX}加载子应用完成 -> ${app.name}`); // 这里重新判断下当前正在加载的子应用是否是最新的 - // --mountA-----------loadedA---- - // ----mountB----loadedB--------- + // --loadAppA-----------loadEntryA---- + // ----loadAppB----loadEntryB--------- // 假设有两个并发如上 可以发现A会在B后面运行下面的代码 从而导致子应用不正确 if (this.currentApp.name !== app.name) { - throw new Error('unmont'); + throw new Error( + `${LOG_PREFIX}子应用挂载失败: 当前子应用为 ${app.name}, 需要挂载的子应用为 ${this.currentApp.name}` + ); } const { bootstrap, mount, unmount, update } = this.getAppLifeCycles(app.name); - const parcelConfig = { + const config = { name: app.name, bootstrap, @@ -135,7 +161,6 @@ export class ParcelContext { mount: [ () => { isDev && console.debug(`${LOG_PREFIX}开始挂载子应用 -> ${app.name}; 挂载点: `, container); - parcelUnmountDeferred.resolve(); parcelUnmountDeferred.reset(); return this.appContext.beforeMount(loadedApp, window); }, @@ -148,6 +173,10 @@ export class ParcelContext { this.isLoading = false; return this.appContext.afterMount(loadedApp, window); }, + () => { + this.mountDeferredWeakMap.get(this.parcelInstance!)?.resolve(); + return Promise.resolve(); + }, ], unmount: [ @@ -171,10 +200,9 @@ export class ParcelContext { }; if (update) { - parcelConfig.update = update; + config.update = update; } - - this.parcelInstance = mountRootParcel(parcelConfig, { domElement: container }); + return { config, container }; } private getAppLifeCycles(appName: string) { diff --git a/packages/mapp/src/main/Bay/deferred.ts b/packages/mapp/src/main/Bay/deferred.ts new file mode 100644 index 0000000..49c2e87 --- /dev/null +++ b/packages/mapp/src/main/Bay/deferred.ts @@ -0,0 +1,24 @@ +export class Deferred { + promise!: Promise; + + resolve!: (value: T | PromiseLike) => void; + + reject!: (reason?: unknown) => void; + + constructor() { + this.reset(); + } + + reset() { + this.promise = new Promise((resolve, reject) => { + this.resolve = resolve; + this.reject = reject; + }); + } +} + +export const parcelUnmountDeferred = new Deferred(); +parcelUnmountDeferred.resolve(); + +export const qiankunUnmountDeferred = new Deferred(); +qiankunUnmountDeferred.resolve(); diff --git a/packages/mapp/src/main/Bay/options.ts b/packages/mapp/src/main/Bay/options.ts index 7f679c0..705b8e7 100644 --- a/packages/mapp/src/main/Bay/options.ts +++ b/packages/mapp/src/main/Bay/options.ts @@ -6,8 +6,8 @@ import { pathToActiveWhen } from 'single-spa'; import { BayOptions, MicroApp } from '../../types'; import { DEFAULT_ROOT_FOR_CHILD, MAX_WAIT_TIMES } from '../constants'; +import { parcelUnmountDeferred, qiankunUnmountDeferred } from './deferred'; import { pushMountQueue } from './mount-delay'; -import { parcelUnmountDeferred, qiankunUnmountDeferred } from './shared'; import { MicroAppNormalized, ModernMicroAppNormalized } from './types'; import { normalizeUrl, runPromiseChain, toArray, trimBaseUrl } from './utils'; @@ -200,7 +200,6 @@ export function normalizeOptions(options: BayOptions): BayOptions { if (hooks.beforeAppMount) { hooks.beforeAppMount(app); } - // 不做任何操作 暂时先直接重置掉 qiankunUnmountDeferred.reset(); }, afterAppUnmount(app) { diff --git a/packages/mapp/src/main/Bay/shared.ts b/packages/mapp/src/main/Bay/shared.ts deleted file mode 100644 index 24cf6de..0000000 --- a/packages/mapp/src/main/Bay/shared.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Deferred } from './utils'; - -export const parcelUnmountDeferred = new Deferred(); -parcelUnmountDeferred.resolve(); -export const qiankunUnmountDeferred = new Deferred(); -qiankunUnmountDeferred.resolve(); diff --git a/packages/mapp/src/main/Bay/utils.ts b/packages/mapp/src/main/Bay/utils.ts index fa1271c..9099678 100644 --- a/packages/mapp/src/main/Bay/utils.ts +++ b/packages/mapp/src/main/Bay/utils.ts @@ -28,22 +28,3 @@ export function runPromiseChain(this: any, chain: any[]): () => Promise { const that = this; return (...args) => chain.reduce((pre, cur) => pre.then(() => cur.apply(that, args)), Promise.resolve()); } - -export class Deferred { - promise!: Promise; - - resolve!: (value: T | PromiseLike) => void; - - reject!: (reason?: unknown) => void; - - constructor() { - this.reset(); - } - - reset() { - this.promise = new Promise((resolve, reject) => { - this.resolve = resolve; - this.reject = reject; - }); - } -} From 0e6c9be75e9f3f675d3ed6fe49d33a75040277bb Mon Sep 17 00:00:00 2001 From: njikm2010 <736155049@qq.com> Date: Thu, 7 Sep 2023 17:10:38 +0800 Subject: [PATCH 4/6] =?UTF-8?q?fix(mapp):=20=E4=BF=AE=E5=A4=8D=E5=AD=90?= =?UTF-8?q?=E5=BA=94=E7=94=A8=E5=8D=B8=E8=BD=BD=E6=97=B6=E6=A0=B7=E5=BC=8F?= =?UTF-8?q?=E6=B2=A1=E6=9C=89=E5=8D=B8=E8=BD=BD=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change-Id: I18ac5e803e6c77ccb2783f643fd0d550210c4fbc --- packages/mapp/src/main/Bay/ParcelContext.ts | 138 ++++++++++++++++++-- 1 file changed, 130 insertions(+), 8 deletions(-) diff --git a/packages/mapp/src/main/Bay/ParcelContext.ts b/packages/mapp/src/main/Bay/ParcelContext.ts index 53d46b3..1507867 100644 --- a/packages/mapp/src/main/Bay/ParcelContext.ts +++ b/packages/mapp/src/main/Bay/ParcelContext.ts @@ -12,11 +12,109 @@ const isDev = process.env.NODE_ENV === 'development'; const LOG_PREFIX = '[Single SPA Parcel] '; const PARCEL_INSTANCE_APP_NAME = '__$$app_name__'; + +const QIANKUN_CONTEXT_KEYS = ['__POWERED_BY_QIANKUN__']; + +/** + * document.head的快照 + * 用于处理添加到document.head里的元素 + * + * **只处理新增节点** + */ +class DocumentHeadSnapshot { + private snapshotRecordMap: Map = new Map(); + private currentAppName?: string; + private observer: MutationObserver; + + constructor() { + this.observer = new MutationObserver(records => { + if (!this.currentAppName) { + return; + } + const nodeRecord = this.snapshotRecordMap.get(this.currentAppName)![0]; + + if (!nodeRecord) { + return; + } + + for (const record of records) { + if (record.type === 'childList' && record.addedNodes) { + for (const node of record.addedNodes) { + if (node.nodeName.toLocaleLowerCase() === 'style') { + nodeRecord.push(node); + } + } + } + } + }); + + this.observer.observe(document.head, { childList: true }); + } + + /** + * 创建一个子应用快照列表 + * + * 如果存在 不做任何操作 + * @param name + */ + create(name: string) { + this.currentAppName = name; + if (this.snapshotRecordMap.has(name)) { + return; + } + this.snapshotRecordMap.set(name, []); + } + /** + * 创建一个快照点 + */ + store() { + if (this.currentAppName) { + this.snapshotRecordMap.get(this.currentAppName)!.unshift([]); + } + } + + /** + * 恢复到上一个快照 + * @returns + */ + restore() { + if (!this.currentAppName) { + return; + } + const snapshot = this.snapshotRecordMap.get(this.currentAppName)![0] || []; + + this.removeNodes(snapshot); + } + + /** + * 使用最新的快照进行还原 + */ + use() { + if (!this.currentAppName) { + return; + } + const list = this.snapshotRecordMap.get(this.currentAppName)![0] || []; + this.appendNodes(list); + } + + private appendNodes(nodeList: Node[]) { + const parentElement = document.head; + nodeList.filter(node => node.parentElement === parentElement).forEach(node => parentElement.appendChild(node)); + } + private removeNodes(nodeList: Node[]) { + const parentElement = document.head; + nodeList.filter(node => node.parentElement === parentElement).forEach(node => (node as any as Element).remove()); + } +} + export class ParcelContext { private parcelInstance?: Parcel & { [PARCEL_INSTANCE_APP_NAME]: string; }; + private global = window; + private documentHeadSnapshot = new DocumentHeadSnapshot(); + private currentApp?: MicroAppNormalized; private isLoading: boolean = false; @@ -97,8 +195,8 @@ export class ParcelContext { this.isLoading = true; - // FIXME: 缓存对应的返回值 但是这样会导致vue应用没法正常显示 - // 暂时先不用缓存 没什么太大影响 + this.simulateQiankunContext(); + const { config, container } = await this.load(app, target); const instance = mountRootParcel(config, { domElement: container }); @@ -121,6 +219,10 @@ export class ParcelContext { isDev && console.debug(`${LOG_PREFIX}开始加载子应用 -> ${app.name}`); + this.documentHeadSnapshot.create(app.name); + this.documentHeadSnapshot.use(); + this.documentHeadSnapshot.store(); + await app.loader!(true); const container = @@ -133,7 +235,7 @@ export class ParcelContext { }, }); - this.appContext.beforeLoad(loadedApp, window); + this.appContext.beforeLoad(loadedApp, this.global); await loadEntry(app.entry, container, { fetch: window.fetch, @@ -162,7 +264,7 @@ export class ParcelContext { () => { isDev && console.debug(`${LOG_PREFIX}开始挂载子应用 -> ${app.name}; 挂载点: `, container); parcelUnmountDeferred.reset(); - return this.appContext.beforeMount(loadedApp, window); + return this.appContext.beforeMount(loadedApp, this.global); }, (props: Record) => mount({ ...props, container }).catch((err: unknown) => @@ -171,7 +273,7 @@ export class ParcelContext { () => { isDev && console.debug(`${LOG_PREFIX}挂载子应用完成 -> ${app.name}`); this.isLoading = false; - return this.appContext.afterMount(loadedApp, window); + return this.appContext.afterMount(loadedApp, this.global); }, () => { this.mountDeferredWeakMap.get(this.parcelInstance!)?.resolve(); @@ -182,7 +284,7 @@ export class ParcelContext { unmount: [ () => { isDev && console.debug(`${LOG_PREFIX}开始卸载子应用 -> ${app.name}`); - return this.appContext.beforeUnmount(loadedApp, window); + return this.appContext.beforeUnmount(loadedApp, this.global); }, (props: Record) => unmount({ ...props, container }).catch((err: unknown) => @@ -192,7 +294,13 @@ export class ParcelContext { isDev && console.debug(`${LOG_PREFIX}卸载子应用完成 -> ${app.name}`); parcelUnmountDeferred.resolve(); this.currentApp = undefined; - return this.appContext.afterUnmount(loadedApp, window); + return this.appContext.afterUnmount(loadedApp, this.global); + }, + () => { + container.innerHTML = ''; + this.documentHeadSnapshot.restore(); + this.cancelSimulateQiankunContext(); + return Promise.resolve(); }, ], @@ -207,7 +315,7 @@ export class ParcelContext { private getAppLifeCycles(appName: string) { // @ts-expect-error - const obj = window[appName]; + const obj = this.global[appName]; if (!obj) { throw new Error(`${LOG_PREFIX}无法获取到子应用 (${appName}) 的生命周期函数`); @@ -223,4 +331,18 @@ export class ParcelContext { `${LOG_PREFIX}子应用 (${appName}) 的生命周期函数异常,请确保 bootstrap, mount, unmount 三个字段为函数` ); } + + private simulateQiankunContext() { + for (const key of QIANKUN_CONTEXT_KEYS) { + // @ts-expect-error + this.global[key] = true; + } + } + + private cancelSimulateQiankunContext() { + for (const key of QIANKUN_CONTEXT_KEYS) { + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete (this.global as any)[key]; + } + } } From f7fb8e72bed2e1f54575b80e54817f1c24d3d0c6 Mon Sep 17 00:00:00 2001 From: njikm2010 <736155049@qq.com> Date: Fri, 8 Sep 2023 16:38:46 +0800 Subject: [PATCH 5/6] =?UTF-8?q?fix(mapp):=20=E4=BF=AE=E5=A4=8D=E5=AD=90?= =?UTF-8?q?=E5=BA=94=E7=94=A8`bootstrap`=E9=92=A9=E5=AD=90=E4=BC=9A?= =?UTF-8?q?=E8=A2=AB=E8=B0=83=E7=94=A8=E5=A4=9A=E6=AC=A1=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change-Id: I5ae9900d187e5ffeef1bcbd9cab30d7b4fa454d1 --- packages/mapp/src/main/Bay/ParcelContext.ts | 37 ++++++++++++++++++--- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/packages/mapp/src/main/Bay/ParcelContext.ts b/packages/mapp/src/main/Bay/ParcelContext.ts index 1507867..59d74ca 100644 --- a/packages/mapp/src/main/Bay/ParcelContext.ts +++ b/packages/mapp/src/main/Bay/ParcelContext.ts @@ -82,7 +82,6 @@ class DocumentHeadSnapshot { return; } const snapshot = this.snapshotRecordMap.get(this.currentAppName)![0] || []; - this.removeNodes(snapshot); } @@ -99,8 +98,9 @@ class DocumentHeadSnapshot { private appendNodes(nodeList: Node[]) { const parentElement = document.head; - nodeList.filter(node => node.parentElement === parentElement).forEach(node => parentElement.appendChild(node)); + nodeList.filter(node => node.parentElement !== parentElement).forEach(node => parentElement.appendChild(node)); } + private removeNodes(nodeList: Node[]) { const parentElement = document.head; nodeList.filter(node => node.parentElement === parentElement).forEach(node => (node as any as Element).remove()); @@ -121,6 +121,14 @@ export class ParcelContext { private mountDeferredWeakMap: WeakMap> = new WeakMap(); + private parcelConfigCache: Map< + string, + { + config: ParcelConfig; + container: HTMLElement; + } + > = new Map(); + constructor(private apps: ModernMicroAppNormalized[], private appContext: AppContext) {} mountOrUnmountAppIfNeed(container?: HTMLElement) { @@ -150,6 +158,7 @@ export class ParcelContext { isDev && console.log(`${LOG_PREFIX}等待qiankun卸载子应用 -> `, this.appContext.currentApp.value?.name); await qiankunUnmountDeferred.promise; + isDev && console.log(`${LOG_PREFIX}qiankun卸载子应用完成 -> `, this.appContext.currentApp.value?.name); } @@ -217,12 +226,25 @@ export class ParcelContext { }> { this.currentApp = app; - isDev && console.debug(`${LOG_PREFIX}开始加载子应用 -> ${app.name}`); - this.documentHeadSnapshot.create(app.name); this.documentHeadSnapshot.use(); this.documentHeadSnapshot.store(); + if (this.parcelConfigCache.has(app.name)) { + const cache = this.parcelConfigCache.get(app.name)!; + + // 这里重新执行下 用于添加html entry的html内容到挂载点上 + await loadEntry(app.entry, cache.container, { + fetch: window.fetch, + }); + + // 重置下bootstrap 防止多次调用 + (cache.config as any).bootstrap = () => Promise.resolve(); + return cache; + } + + isDev && console.debug(`${LOG_PREFIX}开始加载子应用 -> ${app.name}`); + await app.loader!(true); const container = @@ -307,9 +329,14 @@ export class ParcelContext { update: Noop, }; - if (update) { + if (isFunction(update)) { config.update = update; } + + this.parcelConfigCache.set(app.name, { + config, + container, + }); return { config, container }; } From 2ffea823ae5d9f10b089ba0fde89adcc03b448af Mon Sep 17 00:00:00 2001 From: njikm2010 <736155049@qq.com> Date: Wed, 13 Sep 2023 22:28:40 +0800 Subject: [PATCH 6/6] =?UTF-8?q?refactor(mapp):=20=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E8=BE=93=E5=87=BA=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Change-Id: Ic0924bdb63806b2d98d47f5bcd65a90e3e4082fa --- packages/mapp/src/main/Bay/Bay.ts | 2 -- packages/mapp/src/main/Bay/ParcelContext.ts | 18 +++++++++++------- packages/mapp/src/types/bay.ts | 2 -- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/mapp/src/main/Bay/Bay.ts b/packages/mapp/src/main/Bay/Bay.ts index b8eb7b3..3d89689 100644 --- a/packages/mapp/src/main/Bay/Bay.ts +++ b/packages/mapp/src/main/Bay/Bay.ts @@ -56,8 +56,6 @@ export class Bay implements IBay { nonIndependentApps: MicroAppNormalized[]; - modernApps: MicroAppNormalized[] = []; - private parcelContext: ParcelContext; get location() { diff --git a/packages/mapp/src/main/Bay/ParcelContext.ts b/packages/mapp/src/main/Bay/ParcelContext.ts index 59d74ca..50c2a99 100644 --- a/packages/mapp/src/main/Bay/ParcelContext.ts +++ b/packages/mapp/src/main/Bay/ParcelContext.ts @@ -67,7 +67,7 @@ class DocumentHeadSnapshot { /** * 创建一个快照点 */ - store() { + capture() { if (this.currentAppName) { this.snapshotRecordMap.get(this.currentAppName)!.unshift([]); } @@ -92,8 +92,12 @@ class DocumentHeadSnapshot { if (!this.currentAppName) { return; } - const list = this.snapshotRecordMap.get(this.currentAppName)![0] || []; - this.appendNodes(list); + const snapshot = this.snapshotRecordMap.get(this.currentAppName)![0] || []; + this.appendNodes(snapshot); + } + + destroy() { + this.observer.disconnect(); } private appendNodes(nodeList: Node[]) { @@ -159,7 +163,7 @@ export class ParcelContext { await qiankunUnmountDeferred.promise; - isDev && console.log(`${LOG_PREFIX}qiankun卸载子应用完成 -> `, this.appContext.currentApp.value?.name); + isDev && console.log(`${LOG_PREFIX}qiankun卸载子应用完成`); } this.loadApp(app, container); @@ -228,12 +232,12 @@ export class ParcelContext { this.documentHeadSnapshot.create(app.name); this.documentHeadSnapshot.use(); - this.documentHeadSnapshot.store(); + this.documentHeadSnapshot.capture(); if (this.parcelConfigCache.has(app.name)) { const cache = this.parcelConfigCache.get(app.name)!; - // 这里重新执行下 用于添加html entry的html内容到挂载点上 + // 这里重新执行下 用于添加 entry 的 html 内容到挂载点上 await loadEntry(app.entry, cache.container, { fetch: window.fetch, }); @@ -284,7 +288,7 @@ export class ParcelContext { mount: [ () => { - isDev && console.debug(`${LOG_PREFIX}开始挂载子应用 -> ${app.name}; 挂载点: `, container); + isDev && console.debug(`${LOG_PREFIX}开始挂载子应用 -> ${app.name}`); parcelUnmountDeferred.reset(); return this.appContext.beforeMount(loadedApp, this.global); }, diff --git a/packages/mapp/src/types/bay.ts b/packages/mapp/src/types/bay.ts index ecf25ef..b95f999 100644 --- a/packages/mapp/src/types/bay.ts +++ b/packages/mapp/src/types/bay.ts @@ -217,8 +217,6 @@ export interface IBay extends IBayBase { nonIndependentApps: MicroApp[]; - modernApps: MicroApp[]; - /** * 当前激活的微应用 */