From 8a997a93216227a7f89bf27921db5d87acae2914 Mon Sep 17 00:00:00 2001 From: why520crazy Date: Mon, 16 Sep 2019 10:34:06 +0800 Subject: [PATCH] fix(lapplication): cancel bootstrapped app when next change #264544 --- .../planet-application-loader.spec.ts | 128 ++++++++++++++++-- .../application/planet-application-loader.ts | 35 +++-- 2 files changed, 142 insertions(+), 21 deletions(-) diff --git a/packages/planet/src/application/planet-application-loader.spec.ts b/packages/planet/src/application/planet-application-loader.spec.ts index 4f61233..4fee103 100644 --- a/packages/planet/src/application/planet-application-loader.spec.ts +++ b/packages/planet/src/application/planet-application-loader.spec.ts @@ -18,7 +18,7 @@ const app1 = { routerPathPrefix: '/app1', hostClass: 'app1-host', preload: false, - switchMode: SwitchModes.coexist, + switchMode: SwitchModes.default, resourcePathPrefix: '/static/app1', styles: ['styles/main.css'], scripts: ['vendor.js', 'main.js'], @@ -35,7 +35,7 @@ const app2 = { selector: 'app2-root-container', routerPathPrefix: '/app2', hostClass: 'app2-host', - preload: false, + preload: true, switchMode: SwitchModes.coexist, resourcePathPrefix: '/static/app2', styles: ['styles/main.css'], @@ -69,6 +69,11 @@ describe('PlanetApplicationLoader', () => { planetApplicationService.register(app1); planetApplicationService.register(app2); + + // 创建宿主容器 + const hostContainer = document.createElement('DIV'); + hostContainer.classList.add('host-selector'); + document.body.appendChild(hostContainer); }); afterEach(() => { @@ -80,11 +85,6 @@ describe('PlanetApplicationLoader', () => { const assetsLoaderSpy = spyOn(assetsLoader, 'loadAppAssets'); assetsLoaderSpy.and.returnValue(loadAppAssets$); - // 创建宿主容器 - const hostContainer = document.createElement('DIV'); - hostContainer.classList.add('host-selector'); - document.body.appendChild(hostContainer); - const planetAppRef = mockApplicationRef(app1.name); const bootstrapSpy = spyOn(planetAppRef, 'bootstrap'); @@ -108,6 +108,7 @@ describe('PlanetApplicationLoader', () => { expect(bootstrapSpy).toHaveBeenCalled(); expect(appStatusChangeSpy).toHaveBeenCalledTimes(4); + expect(appStatusChangeSpy).toHaveBeenCalledWith({ app: app1, status: ApplicationStatus.bootstrapping }); expect(appStatusChangeSpy).toHaveBeenCalledWith({ app: app1, status: ApplicationStatus.bootstrapped }); // 判断是否在宿主元素中创建了应用根节点 @@ -117,7 +118,7 @@ describe('PlanetApplicationLoader', () => { tick(); })); - it(`should cancel load app1 which has not loaded when next route (app2) change`, fakeAsync(() => { + it(`should cancel load app1 which assets has not loaded when next route (app2) change`, fakeAsync(() => { const loadApp1Assets$ = new Subject(); const loadApp2Assets$ = new Subject(); @@ -153,6 +154,7 @@ describe('PlanetApplicationLoader', () => { ngZone.onStable.next(); expect(appStatusChangeSpy).toHaveBeenCalledTimes(5); + expect(appStatusChangeSpy).toHaveBeenCalledWith({ app: app2, status: ApplicationStatus.bootstrapping }); expect(appStatusChangeSpy).toHaveBeenCalledWith({ app: app2, status: ApplicationStatus.bootstrapped }); expect(app1BootstrapSpy).not.toHaveBeenCalled(); @@ -161,5 +163,113 @@ describe('PlanetApplicationLoader', () => { tick(); })); - it(`should preload apps`, () => {}); + it(`should cancel load app1 which has not bootstrapped when next route (app2) change`, fakeAsync(() => { + const loadApp1Assets$ = new Subject(); + const loadApp2Assets$ = new Subject(); + + const planetApp1Ref = mockApplicationRef(app1.name); + const app1BootstrapSpy = spyOn(planetApp1Ref, 'bootstrap'); + + const planetApp2Ref = mockApplicationRef(app2.name); + const app2BootstrapSpy = spyOn(planetApp2Ref, 'bootstrap'); + + const assetsLoaderSpy = spyOn(assetsLoader, 'loadAppAssets'); + assetsLoaderSpy.and.returnValues(loadApp1Assets$, loadApp2Assets$); + + const appStatusChangeSpy = jasmine.createSpy('app status change spy'); + planetApplicationLoader.appStatusChange.subscribe(appStatusChangeSpy); + + expect(appStatusChangeSpy).not.toHaveBeenCalled(); + planetApplicationLoader.reroute({ url: '/app1' }); + expect(appStatusChangeSpy).toHaveBeenCalled(); + expect(appStatusChangeSpy).toHaveBeenCalledWith({ app: app1, status: ApplicationStatus.assetsLoading }); + + loadApp1Assets$.next(); + loadApp1Assets$.complete(); + + expect(appStatusChangeSpy).toHaveBeenCalledTimes(2); + expect(appStatusChangeSpy).toHaveBeenCalledWith({ app: app1, status: ApplicationStatus.assetsLoaded }); + + planetApplicationLoader.reroute({ url: '/app2' }); + expect(appStatusChangeSpy).toHaveBeenCalledTimes(3); + expect(appStatusChangeSpy).toHaveBeenCalledWith({ app: app2, status: ApplicationStatus.assetsLoading }); + + loadApp2Assets$.next(); + loadApp2Assets$.complete(); + + expect(appStatusChangeSpy).toHaveBeenCalledTimes(4); + expect(appStatusChangeSpy).toHaveBeenCalledWith({ app: app2, status: ApplicationStatus.assetsLoaded }); + + ngZone.onStable.next(); + + expect(appStatusChangeSpy).toHaveBeenCalledTimes(6); + expect(appStatusChangeSpy).toHaveBeenCalledWith({ app: app2, status: ApplicationStatus.bootstrapping }); + expect(appStatusChangeSpy).toHaveBeenCalledWith({ app: app2, status: ApplicationStatus.bootstrapped }); + + expect(app1BootstrapSpy).not.toHaveBeenCalled(); + expect(app2BootstrapSpy).toHaveBeenCalled(); + + tick(); + })); + + describe('preload', () => { + it(`should preload load app2 when after loaded app1`, fakeAsync(() => { + const loadApp1Assets$ = new Subject(); + const loadApp2Assets$ = new Subject(); + + const planetApp1Ref = mockApplicationRef(app1.name); + const app1BootstrapSpy = spyOn(planetApp1Ref, 'bootstrap'); + + const planetApp2Ref = mockApplicationRef(app2.name); + const app2BootstrapSpy = spyOn(planetApp2Ref, 'bootstrap'); + + const assetsLoaderSpy = spyOn(assetsLoader, 'loadAppAssets'); + assetsLoaderSpy.and.returnValues(loadApp1Assets$, loadApp2Assets$); + + const appStatusChangeSpy = jasmine.createSpy('app status change spy'); + planetApplicationLoader.appStatusChange.subscribe(appStatusChangeSpy); + + expect(appStatusChangeSpy).not.toHaveBeenCalled(); + planetApplicationLoader.reroute({ url: '/app1' }); + expect(appStatusChangeSpy).toHaveBeenCalled(); + expect(appStatusChangeSpy).toHaveBeenCalledWith({ app: app1, status: ApplicationStatus.assetsLoading }); + + loadApp1Assets$.next(); + loadApp1Assets$.complete(); + + expect(appStatusChangeSpy).toHaveBeenCalledTimes(2); + expect(appStatusChangeSpy).toHaveBeenCalledWith({ app: app1, status: ApplicationStatus.assetsLoaded }); + + ngZone.onStable.next(); + + expect(appStatusChangeSpy).toHaveBeenCalledTimes(4); + expect(appStatusChangeSpy).toHaveBeenCalledWith({ app: app1, status: ApplicationStatus.bootstrapping }); + expect(appStatusChangeSpy).toHaveBeenCalledWith({ app: app1, status: ApplicationStatus.bootstrapped }); + + expect(app1BootstrapSpy).toHaveBeenCalled(); + + tick(300); + expect(app2BootstrapSpy).not.toHaveBeenCalled(); + + // 已经开始加载 App2 静态资源 + expect(appStatusChangeSpy).toHaveBeenCalledTimes(5); + expect(appStatusChangeSpy).toHaveBeenCalledWith({ app: app2, status: ApplicationStatus.assetsLoading }); + + // App2 静态资源加载完毕 + loadApp2Assets$.next(); + loadApp2Assets$.complete(); + + expect(appStatusChangeSpy).toHaveBeenCalledTimes(6); + expect(appStatusChangeSpy).toHaveBeenCalledWith({ app: app2, status: ApplicationStatus.assetsLoaded }); + + // onStable 开始启动应用 + ngZone.onStable.next(); + expect(appStatusChangeSpy).toHaveBeenCalledTimes(8); + expect(appStatusChangeSpy).toHaveBeenCalledWith({ app: app2, status: ApplicationStatus.bootstrapping }); + expect(appStatusChangeSpy).toHaveBeenCalledWith({ app: app2, status: ApplicationStatus.bootstrapped }); + expect(app2BootstrapSpy).toHaveBeenCalled(); + })); + + it(`should preload app`, () => {}); + }); }); diff --git a/packages/planet/src/application/planet-application-loader.ts b/packages/planet/src/application/planet-application-loader.ts index d317a92..7a7df93 100644 --- a/packages/planet/src/application/planet-application-loader.ts +++ b/packages/planet/src/application/planet-application-loader.ts @@ -23,6 +23,8 @@ export enum ApplicationStatus { export class PlanetApplicationLoader { private firstLoad = true; + private startRouteChangeEvent: PlanetRouterEvent; + private options: PlanetOptions; private inProgressAppAssetsLoads = new Map>(); @@ -90,6 +92,7 @@ export class PlanetApplicationLoader { .pipe( switchMap(event => { this.loadingDone = false; + this.startRouteChangeEvent = event; const shouldLoadApps = this.planetApplicationService.getAppsByMatchedUrl(event.url); const shouldUnloadApps = this.getUnloadApps(shouldLoadApps); this.unloadApps(shouldUnloadApps, event); @@ -102,7 +105,7 @@ export class PlanetApplicationLoader { if (shouldLoadApps && shouldLoadApps.length > 0) { const loadApps$ = shouldLoadApps.map(app => { const appStatus = this.appsStatus.get(app); - if (!appStatus) { + if (!appStatus || appStatus === ApplicationStatus.assetsLoading) { return this.startLoadAppAssets(app); } else { return of(app); @@ -131,17 +134,20 @@ export class PlanetApplicationLoader { // 切换到应用后会有闪烁现象,所以使用 onStable 后启动应用 this.ngZone.onStable.pipe(take(1)).subscribe(() => { - this.ngZone.runOutsideAngular(() => { - shouldShowApps.forEach(app => { - this.showApp(app); - const appRef = getPlanetApplicationRef(app.name); - appRef.onRouteChange(eventAndApps.event); - }); - - shouldBootstrapApps.forEach(app => { - this.bootstrapApp(app); + // 此处判断是因为如果静态资源加载完毕还未启动被取消,还是会启动之前的应用,虽然可能性比较小,但是无法排除这种可能性,所以只有当 Event 是最后一个才会启动 + if (this.startRouteChangeEvent === eventAndApps.event) { + this.ngZone.runOutsideAngular(() => { + shouldShowApps.forEach(app => { + this.showApp(app); + const appRef = getPlanetApplicationRef(app.name); + appRef.onRouteChange(eventAndApps.event); + }); + + shouldBootstrapApps.forEach(app => { + this.bootstrapApp(app); + }); }); - }); + } }); return eventAndApps.apps; @@ -317,7 +323,12 @@ export class PlanetApplicationLoader { return this.startLoadAppAssets(app).pipe( map(() => { this.ngZone.runOutsideAngular(() => { - this.bootstrapApp(app, 'hidden'); + this.ngZone.onStable + .asObservable() + .pipe(take(1)) + .subscribe(() => { + this.bootstrapApp(app, 'hidden'); + }); }); return getPlanetApplicationRef(app.name); })