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 {EmptyError, from, Observable, Observer, of} from 'rxjs';
|
||||
import {catchError, concatAll, every, first, map, mergeMap, tap} from 'rxjs/operators';
|
||||
import {EmptyError, Observable, Observer, of} from 'rxjs';
|
||||
import {catchError, concatAll, first, map, mergeMap, tap} from 'rxjs/operators';
|
||||
|
||||
import {LoadedRouterConfig, Route, Routes} from './config';
|
||||
import {CanLoadFn} from './interfaces';
|
||||
import {prioritizedGuardValue} from './operators/prioritized_guard_value';
|
||||
import {RouterConfigLoader} from './router_config_loader';
|
||||
import {defaultUrlMatcher, navigationCancelingError, Params, PRIMARY_OUTLET} from './shared';
|
||||
import {UrlSegment, UrlSegmentGroup, UrlSerializer, UrlTree} from './url_tree';
|
||||
|
@ -321,7 +322,7 @@ class ApplyRedirects {
|
|||
const canLoad = route.canLoad;
|
||||
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);
|
||||
let guardVal;
|
||||
if (isCanLoad(guard)) {
|
||||
|
@ -332,20 +333,21 @@ class ApplyRedirects {
|
|||
throw new Error('Invalid CanLoad guard');
|
||||
}
|
||||
return wrapIntoObservable(guardVal);
|
||||
}));
|
||||
});
|
||||
|
||||
return obs.pipe(
|
||||
concatAll(),
|
||||
tap((result: UrlTree|boolean) => {
|
||||
if (!isUrlTree(result)) return;
|
||||
return of(canLoadObservables)
|
||||
.pipe(
|
||||
prioritizedGuardValue(),
|
||||
tap((result: UrlTree|boolean) => {
|
||||
if (!isUrlTree(result)) return;
|
||||
|
||||
const error: Error&{url?: UrlTree} =
|
||||
navigationCancelingError(`Redirecting to "${this.urlSerializer.serialize(result)}"`);
|
||||
error.url = result;
|
||||
throw error;
|
||||
}),
|
||||
every(result => result === true),
|
||||
);
|
||||
const error: Error&{url?: UrlTree} = navigationCancelingError(
|
||||
`Redirecting to "${this.urlSerializer.serialize(result)}"`);
|
||||
error.url = result;
|
||||
throw error;
|
||||
}),
|
||||
map(result => result === true),
|
||||
);
|
||||
}
|
||||
|
||||
private lineralizeSegments(route: Route, urlTree: UrlTree): Observable<UrlSegment[]> {
|
||||
|
|
|
@ -14,7 +14,7 @@ import {By} from '@angular/platform-browser/src/dom/debug/by';
|
|||
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 {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 {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', () => {
|
||||
class Logger {
|
||||
logs: string[] = [];
|
||||
|
|
Loading…
Reference in New Issue