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:
parent
a5ffca0576
commit
d7dd2959c8
|
@ -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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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[] = [];
|
||||||
|
|
Loading…
Reference in New Issue