Skip to content

Commit

Permalink
fix(application): should trigger next route change when last throw error
Browse files Browse the repository at this point in the history
  • Loading branch information
why520crazy committed Sep 16, 2019
1 parent 0045a4d commit 75b3ca3
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 71 deletions.
62 changes: 62 additions & 0 deletions packages/planet/src/application/planet-application-loader.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,68 @@ describe('PlanetApplicationLoader', () => {
tick();
}));

it(`should load next app(app2) when last app(app1) load error`, fakeAsync(() => {
const loadApp1Assets$ = new Subject();
const loadApp2Assets$ = new Subject();

const errorHandlerSpy = jasmine.createSpy(`error handler spy`);
planetApplicationLoader.setOptions({
errorHandler: errorHandlerSpy
});

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 });

expect(errorHandlerSpy).not.toHaveBeenCalled();

// Load Error
loadApp1Assets$.error(new Error(`load app1 assets error`));
loadApp1Assets$.complete();

expect(errorHandlerSpy).toHaveBeenCalled();
expect(errorHandlerSpy).toHaveBeenCalledWith(new Error(`load app1 assets error`));

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();

// 判断是否在宿主元素中创建了应用根节点
const app2Host = document.querySelector(app2.selector);
expect(app2Host).toBeTruthy();
expect(app2Host.outerHTML).toEqual(`<app2-root-container class="app2-host"></app2-root-container>`);
tick();
tick();
}));

describe('preload', () => {
it(`should preload load app2 when after loaded app1`, fakeAsync(() => {
const loadApp1Assets$ = new Subject();
Expand Down
161 changes: 90 additions & 71 deletions packages/planet/src/application/planet-application-loader.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Injectable, NgZone, ApplicationRef, Injector } from '@angular/core';
import { of, Observable, Subject, forkJoin } from 'rxjs';
import { of, Observable, Subject, forkJoin, throwError } from 'rxjs';
import { AssetsLoader, AssetsLoadResult } from '../assets-loader';
import { PlanetApplication, PlanetRouterEvent, SwitchModes, PlanetOptions } from '../planet.class';
import { switchMap, finalize, share, map, tap, delay, take, filter } from 'rxjs/operators';
import { switchMap, finalize, share, map, tap, delay, take, filter, catchError } from 'rxjs/operators';
import { getScriptsAndStylesFullPaths, getHTMLElement, coerceArray } from '../helpers';
import { PlanetApplicationRef, getPlanetApplicationRef, globalPlanet } from './planet-application-ref';
import { PlanetPortalApplication } from './portal-application';
Expand All @@ -14,7 +14,8 @@ export enum ApplicationStatus {
assetsLoading = 1,
assetsLoaded = 2,
bootstrapping = 3,
bootstrapped = 4
bootstrapped = 4,
loadError = 10
}

@Injectable({
Expand Down Expand Up @@ -90,82 +91,95 @@ export class PlanetApplicationLoader {
private setupRouteChange() {
this.routeChange$
.pipe(
// Using switchMap so we cancel executing loading when a new one comes in
switchMap(event => {
this.loadingDone = false;
this.startRouteChangeEvent = event;
const shouldLoadApps = this.planetApplicationService.getAppsByMatchedUrl(event.url);
const shouldUnloadApps = this.getUnloadApps(shouldLoadApps);
this.unloadApps(shouldUnloadApps, event);

const eventAndApps = {
event: event,
apps: shouldLoadApps
};

if (shouldLoadApps && shouldLoadApps.length > 0) {
const loadApps$ = shouldLoadApps.map(app => {
const appStatus = this.appsStatus.get(app);
if (!appStatus || appStatus === ApplicationStatus.assetsLoading) {
return this.startLoadAppAssets(app);
// Return new observable use of and catchError,
// in order to prevent routeChange$ completed which never trigger new route change
return of(event).pipe(
// unload apps and return should load apps
map(() => {
this.loadingDone = false;
this.startRouteChangeEvent = event;
const shouldLoadApps = this.planetApplicationService.getAppsByMatchedUrl(event.url);
const shouldUnloadApps = this.getUnloadApps(shouldLoadApps);
this.unloadApps(shouldUnloadApps, event);
return shouldLoadApps;
}),
// Load app assets (static resources)
switchMap(shouldLoadApps => {
if (shouldLoadApps && shouldLoadApps.length > 0) {
const loadApps$ = shouldLoadApps.map(app => {
const appStatus = this.appsStatus.get(app);
if (!appStatus || appStatus === ApplicationStatus.assetsLoading) {
return this.ngZone.runOutsideAngular(() => {
return this.startLoadAppAssets(app);
});
} else {
return of(app);
}
});
return forkJoin(loadApps$);
} else {
return of(app);
return of([]);
}
});
return forkJoin(loadApps$).pipe(
map(() => {
return eventAndApps;
})
);
} else {
return of(eventAndApps);
}
}),
map(eventAndApps => {
const shouldBootstrapApps = [];
const shouldShowApps = [];
eventAndApps.apps.forEach(app => {
const appStatus = this.appsStatus.get(app);
if (appStatus === ApplicationStatus.bootstrapped) {
shouldShowApps.push(app);
} else {
shouldBootstrapApps.push(app);
}
});
}),
// Bootstrap or show apps
map(apps => {
const shouldBootstrapApps = [];
const shouldShowApps = [];
apps.forEach(app => {
const appStatus = this.appsStatus.get(app);
if (appStatus === ApplicationStatus.bootstrapped) {
shouldShowApps.push(app);
} else {
shouldBootstrapApps.push(app);
}
});

// 切换到应用后会有闪烁现象,所以使用 onStable 后启动应用
this.ngZone.onStable.pipe(take(1)).subscribe(() => {
// 此处判断是因为如果静态资源加载完毕还未启动被取消,还是会启动之前的应用,虽然可能性比较小,但是无法排除这种可能性,所以只有当 Event 是最后一个才会启动
if (this.startRouteChangeEvent === eventAndApps.event) {
this.ngZone.runOutsideAngular(() => {
shouldShowApps.forEach(app => {
this.showApp(app);
const appRef = getPlanetApplicationRef(app.name);
appRef.onRouteChange(eventAndApps.event);
// 切换到应用后会有闪烁现象,所以使用 onStable 后启动应用
this.ngZone.onStable
.asObservable()
.pipe(take(1))
.subscribe(() => {
// 此处判断是因为如果静态资源加载完毕还未启动被取消,还是会启动之前的应用,虽然可能性比较小,但是无法排除这种可能性,所以只有当 Event 是最后一个才会启动
if (this.startRouteChangeEvent === event) {
this.ngZone.runOutsideAngular(() => {
shouldShowApps.forEach(app => {
this.showApp(app);
const appRef = getPlanetApplicationRef(app.name);
appRef.onRouteChange(event);
});

shouldBootstrapApps.forEach(app => {
this.bootstrapApp(app);
});
});
}
});

shouldBootstrapApps.forEach(app => {
this.bootstrapApp(app);
});
});
}
});

return eventAndApps.apps;
return apps;
}),
// Start preload apps
tap(activeApps => {
// 第一次加载,预加载
if (this.firstLoad) {
this.preloadApps(activeApps);
this.firstLoad = false;
}
}),
// Finish loadingDone true
finalize(() => {
this.loadingDone = true;
}),
// Error handler
catchError(error => {
this.errorHandler(error);
return [];
})
);
})
)
.subscribe({
next: apps => {
this.loadingDone = true;
// 第一次加载,预加载
if (this.firstLoad) {
this.preloadApps(apps);
this.firstLoad = false;
}
},
error: error => {
this.errorHandler(error);
}
});
.subscribe();
}

private startLoadAppAssets(app: PlanetApplication) {
Expand All @@ -180,6 +194,11 @@ export class PlanetApplicationLoader {
map(() => {
return app;
}),
catchError(error => {
this.inProgressAppAssetsLoads.delete(app.name);
this.setAppStatus(app, ApplicationStatus.loadError);
throw error;
}),
share()
);
this.inProgressAppAssetsLoads.set(app.name, loadApp$);
Expand Down

0 comments on commit 75b3ca3

Please sign in to comment.