perf(router): apply prioritizedGuardValue operator to optimize CanLoad guards (#37523)

CanLoad guards are processed in asynchronous manner with the following rules:
* If all guards return `true`, operator returns `true`;
* `false` and `UrlTree` values wait for higher priority guards to resolve;
* Highest priority `false` or `UrlTree` value will be returned.

`prioritizedGuardValue` uses `combineLatest` which in order subscribes to each Observable immediately (not waiting when previous one completes that `concatAll` do). So it makes some advantages in order to run them concurrently. Respectively, a time to resolve all guards will be reduced.

PR Close #37523
This commit is contained in:
Dmitrij Kuba 2020-07-01 13:56:47 +03:00 committed by Alex Rickabaugh
parent a5ffca0576
commit d7dd2959c8
2 changed files with 124 additions and 16 deletions

View File

@ -7,11 +7,12 @@
*/ */
import {Injector, NgModuleRef} from '@angular/core'; import {Injector, NgModuleRef} from '@angular/core';
import {EmptyError, from, Observable, Observer, of} from 'rxjs'; import {EmptyError, Observable, Observer, of} from 'rxjs';
import {catchError, concatAll, every, first, map, mergeMap, tap} from 'rxjs/operators'; import {catchError, concatAll, first, map, mergeMap, tap} from 'rxjs/operators';
import {LoadedRouterConfig, Route, Routes} from './config'; import {LoadedRouterConfig, Route, Routes} from './config';
import {CanLoadFn} from './interfaces'; import {CanLoadFn} from './interfaces';
import {prioritizedGuardValue} from './operators/prioritized_guard_value';
import {RouterConfigLoader} from './router_config_loader'; import {RouterConfigLoader} from './router_config_loader';
import {defaultUrlMatcher, navigationCancelingError, Params, PRIMARY_OUTLET} from './shared'; import {defaultUrlMatcher, navigationCancelingError, Params, PRIMARY_OUTLET} from './shared';
import {UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree'; import {UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree';
@ -321,7 +322,7 @@ class ApplyRedirects {
const canLoad = route.canLoad; const canLoad = route.canLoad;
if (!canLoad || canLoad.length === 0) return of(true); if (!canLoad || canLoad.length === 0) return of(true);
const obs = from(canLoad).pipe(map((injectionToken: any) => { const canLoadObservables = canLoad.map((injectionToken: any) => {
const guard = moduleInjector.get(injectionToken); const guard = moduleInjector.get(injectionToken);
let guardVal; let guardVal;
if (isCanLoad(guard)) { if (isCanLoad(guard)) {
@ -332,19 +333,20 @@ class ApplyRedirects {
throw new Error('Invalid CanLoad guard'); throw new Error('Invalid CanLoad guard');
} }
return wrapIntoObservable(guardVal); return wrapIntoObservable(guardVal);
})); });
return obs.pipe( return of(canLoadObservables)
concatAll(), .pipe(
prioritizedGuardValue(),
tap((result: UrlTree|boolean) => { tap((result: UrlTree|boolean) => {
if (!isUrlTree(result)) return; if (!isUrlTree(result)) return;
const error: Error&{url?: UrlTree} = const error: Error&{url?: UrlTree} = navigationCancelingError(
navigationCancelingError(`Redirecting to "${this.urlSerializer.serialize(result)}"`); `Redirecting to "${this.urlSerializer.serialize(result)}"`);
error.url = result; error.url = result;
throw error; throw error;
}), }),
every(result => result === true), map(result => result === true),
); );
} }

View File

@ -14,7 +14,7 @@ import {By} from '@angular/platform-browser/src/dom/debug/by';
import {expect} from '@angular/platform-browser/testing/src/matchers'; import {expect} from '@angular/platform-browser/testing/src/matchers';
import {ActivatedRoute, ActivatedRouteSnapshot, ActivationEnd, ActivationStart, CanActivate, CanDeactivate, ChildActivationEnd, ChildActivationStart, DefaultUrlSerializer, DetachedRouteHandle, Event, GuardsCheckEnd, GuardsCheckStart, Navigation, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, ParamMap, Params, PreloadAllModules, PreloadingStrategy, PRIMARY_OUTLET, Resolve, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, Router, RouteReuseStrategy, RouterEvent, RouterModule, RouterPreloader, RouterStateSnapshot, RoutesRecognized, RunGuardsAndResolvers, UrlHandlingStrategy, UrlSegmentGroup, UrlSerializer, UrlTree} from '@angular/router'; import {ActivatedRoute, ActivatedRouteSnapshot, ActivationEnd, ActivationStart, CanActivate, CanDeactivate, ChildActivationEnd, ChildActivationStart, DefaultUrlSerializer, DetachedRouteHandle, Event, GuardsCheckEnd, GuardsCheckStart, Navigation, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, ParamMap, Params, PreloadAllModules, PreloadingStrategy, PRIMARY_OUTLET, Resolve, ResolveEnd, ResolveStart, RouteConfigLoadEnd, RouteConfigLoadStart, Router, RouteReuseStrategy, RouterEvent, RouterModule, RouterPreloader, RouterStateSnapshot, RoutesRecognized, RunGuardsAndResolvers, UrlHandlingStrategy, UrlSegmentGroup, UrlSerializer, UrlTree} from '@angular/router';
import {EMPTY, Observable, Observer, of, Subscription} from 'rxjs'; import {EMPTY, Observable, Observer, of, Subscription} from 'rxjs';
import {filter, first, map, tap} from 'rxjs/operators'; import {delay, filter, first, map, mapTo, tap} from 'rxjs/operators';
import {forEach} from '../src/utils/collection'; import {forEach} from '../src/utils/collection';
import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing'; import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing';
@ -3998,6 +3998,112 @@ describe('Integration', () => {
}))); })));
}); });
describe('should run CanLoad guards concurrently', () => {
function delayObservable(delayMs: number): Observable<boolean> {
return of(delayMs).pipe(delay(delayMs), mapTo(true));
}
@NgModule()
class LoadedModule {
}
let log: string[];
beforeEach(() => {
log = [];
TestBed.configureTestingModule({
providers: [
{
provide: 'guard1',
useValue: () => {
return delayObservable(5).pipe(tap({next: () => log.push('guard1')}));
}
},
{
provide: 'guard2',
useValue: () => {
return delayObservable(0).pipe(tap({next: () => log.push('guard2')}));
}
},
{
provide: 'returnFalse',
useValue: () => {
log.push('returnFalse');
return false;
}
},
{
provide: 'returnUrlTree',
useFactory: (router: Router) => () => {
return delayObservable(15).pipe(
mapTo(router.parseUrl('/redirected')),
tap({next: () => log.push('returnUrlTree')}));
},
deps: [Router]
},
]
});
});
it('should wait for higher priority guards to be resolved',
fakeAsync(inject(
[Router, NgModuleFactoryLoader],
(router: Router, loader: SpyNgModuleFactoryLoader) => {
loader.stubbedModules = {expected: LoadedModule};
router.resetConfig(
[{path: 'lazy', canLoad: ['guard1', 'guard2'], loadChildren: 'expected'}]);
router.navigateByUrl('/lazy');
tick(5);
expect(log.length).toEqual(2);
expect(log).toEqual(['guard2', 'guard1']);
})));
it('should redirect with UrlTree if higher priority guards have resolved',
fakeAsync(inject(
[Router, NgModuleFactoryLoader, Location],
(router: Router, loader: SpyNgModuleFactoryLoader, location: Location) => {
loader.stubbedModules = {expected: LoadedModule};
router.resetConfig([
{
path: 'lazy',
canLoad: ['returnUrlTree', 'guard1', 'guard2'],
loadChildren: 'expected'
},
{path: 'redirected', component: SimpleCmp}
]);
router.navigateByUrl('/lazy');
tick(15);
expect(log.length).toEqual(3);
expect(log).toEqual(['guard2', 'guard1', 'returnUrlTree']);
expect(location.path()).toEqual('/redirected');
})));
it('should redirect with UrlTree if UrlTree is lower priority',
fakeAsync(inject(
[Router, NgModuleFactoryLoader, Location],
(router: Router, loader: SpyNgModuleFactoryLoader, location: Location) => {
loader.stubbedModules = {expected: LoadedModule};
router.resetConfig([
{path: 'lazy', canLoad: ['guard1', 'returnUrlTree'], loadChildren: 'expected'},
{path: 'redirected', component: SimpleCmp}
]);
router.navigateByUrl('/lazy');
tick(15);
expect(log.length).toEqual(2);
expect(log).toEqual(['guard1', 'returnUrlTree']);
expect(location.path()).toEqual('/redirected');
})));
});
describe('order', () => { describe('order', () => {
class Logger { class Logger {
logs: string[] = []; logs: string[] = [];