New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Preload modules modules based on canLoad guard result #20994
Comments
Yes, I had the same issue. I needed to apply my authentication CanLoad guard and preload my module at a same time. |
Just a heads up that we kicked off a community voting process for your feature request. There are 20 days until the voting process ends. Find more details about Angular's feature request process in our documentation. |
Thank you for submitting your feature request! Looks like during the polling process it didn't collect a sufficient number of votes to move to the next stage. We want to keep Angular rich and ergonomic and at the same time be mindful about its scope and learning journey. If you think your request could live outside Angular's scope, we'd encourage you to collaborate with the community on publishing it as an open source package. You can find more details about the feature request process in our documentation. |
Old issue, but I have found myself in a situation where this is exactly what I need. I work a project where additional modules can be purchased and the app is deployed on-premise. The modules are implemented as lazy-loaded routes, we should only pre-load the module when it is needed, i.e. the customer purchased it (checked via an HTTP request). I still however want to pre-load the modules when needed, so the canload_router_preloader.ts import { Compiler, Injectable, Injector, NgModuleFactoryLoader, NgModuleRef, OnDestroy } from '@angular/core';
import {
NavigationEnd,
PreloadingStrategy,
Route,
RouteConfigLoadEnd,
RouteConfigLoadStart,
Router,
Routes,
UrlSegment,
} from '@angular/router';
import { prioritizedGuardValue } from '@angular/router/esm2015/src/operators/prioritized_guard_value';
import { wrapIntoObservable } from '@angular/router/esm2015/src/utils/collection';
import { isCanLoad, isFunction } from '@angular/router/esm2015/src/utils/type_guards';
import { RouterConfigLoader, LoadedRouterConfig } from '@angular/router/esm2015/src/router_config_loader';
import { EMPTY, from, Observable, of, Subscription } from 'rxjs';
import { concatMap, filter, map, mergeAll, mergeMap, switchMap } from 'rxjs/operators';
@Injectable()
export class CanLoadRouterPreloader implements OnDestroy {
private loader: RouterConfigLoader;
private subscription?: Subscription;
constructor(
private router: Router,
moduleLoader: NgModuleFactoryLoader,
compiler: Compiler,
private injector: Injector,
private preloadingStrategy: PreloadingStrategy,
) {
const onStartLoad = (r: Route) => (router as any).triggerEvent(new RouteConfigLoadStart(r));
const onEndLoad = (r: Route) => (router as any).triggerEvent(new RouteConfigLoadEnd(r));
this.loader = new RouterConfigLoader(moduleLoader, compiler, onStartLoad, onEndLoad);
}
setUpPreloading(): void {
this.subscription = this.router.events
.pipe(
filter((e) => e instanceof NavigationEnd),
concatMap(() => this.preload()),
)
.subscribe(() => {});
}
preload(): Observable<any> {
const ngModule = this.injector.get(NgModuleRef);
return this.processRoutes(ngModule, this.router.config);
}
/** @nodoc */
ngOnDestroy(): void {
if (this.subscription) {
this.subscription.unsubscribe();
}
}
private processRoutes(ngModule: NgModuleRef<any>, routes: Routes): Observable<void> {
const res: Observable<any>[] = [];
for (const route of routes) {
// we already have the config loaded, just recurse
if (route.loadChildren && !route.canLoad && (route as any)._loadedConfig) {
const childConfig = (route as any)._loadedConfig;
res.push(this.processRoutes(childConfig.module, childConfig.routes));
// no config loaded, fetch the config
} else if (route.loadChildren) {
if (!route.canLoad) {
res.push(this.preloadConfig(ngModule, route));
} else {
res.push(
this.runCanLoadGuards(ngModule.injector, route, []).pipe(
switchMap((guardVal) => {
return guardVal ? this.preloadConfig(ngModule, route) : EMPTY;
}),
),
);
}
// recurse into children
} else if (route.children) {
res.push(this.processRoutes(ngModule, route.children));
}
}
return from(res).pipe(
mergeAll(),
map((_) => void 0),
);
}
private preloadConfig(ngModule: NgModuleRef<any>, route: Route): Observable<void> {
return this.preloadingStrategy.preload(route, () => {
const loaded$ = (route as any)._loadedConfig
? of((route as any)._loadedConfig)
: this.loader.load(ngModule.injector, route);
return loaded$.pipe(
mergeMap((config: LoadedRouterConfig) => {
(route as any)._loadedConfig = config;
return this.processRoutes(config.module, config.routes);
}),
);
});
}
private runCanLoadGuards(moduleInjector: Injector, route: Route, segments: UrlSegment[]): Observable<boolean> {
const canLoad = route.canLoad;
if (!canLoad || canLoad.length === 0) return of(true);
const canLoadObservables = canLoad.map((injectionToken: any) => {
const guard = moduleInjector.get(injectionToken);
let guardVal;
if (isCanLoad(guard)) {
guardVal = guard.canLoad(route, segments);
} else if (isFunction(guard)) {
guardVal = guard(route, segments);
} else {
throw new Error('Invalid CanLoad guard');
}
return wrapIntoObservable(guardVal);
});
return of(canLoadObservables).pipe(
prioritizedGuardValue(),
map((result) => result === true),
);
}
} The idea is essentially to run the I would like to propose a new app.module.ts excerpt providers: [
{
provide: RouterPreloader,
useClass: CanLoadRouterPreloader,
},
], As an alternative, I can imagine a CanPreload guard, which would have the following interface and would be respected by the default can_preload.ts export interface CanPreLoad {
canPreLoad(route: Route):
Observable<boolean>|Promise<boolean>|boolean;
}
export type CanPreLoadFn = (route: Route) =>
Observable<boolean>|Promise<boolean>|boolean; What is your view on my proposals? Which approach is best here? I would probably go with the second option, but I needed a solution now, so that is why I implemented another |
I'm submitting a...
Current behavior
Currently, when using a preloadering strategy (like
PreloadAllModules
), the preloader does not process routes with acanLoad
property and therefore does not preload them. There is no indication in theCanLoad
interface documentation that this will happen.angular/packages/router/src/router_preloader.ts
Lines 112 to 114 in 0f5c70d
The router guide does note this behavior and that it is by design. However, it does not give any indication as to why.
The language in the guide also isn't very clear. When I originally read that part of the guide, I misunderstood "areas protected by a CanLoadGuard" as only referring to areas where the
canLoad
guard check returned false.Expected behavior
Preloaders should decide whether to preload based on the boolean result of the
canLoad
guard (or the boolean result of the observable/promise).If this is not possible, it should be more clear that
canLoad
negates any preloading strategy for that module. This could be through console warnings and/or better documentation.Minimal reproduction of the problem with instructions
What is the motivation / use case for changing the behavior?
canLoad
guard allows preventing navigation before a lazy loaded module is downloaded/bootstrapedcanLoad
option still makes logical sensecanActivate
overcanLoad
when preloading wastes network/memory/cpu resourcescanLoad
option happens silentlycanLoad
returns an observable or promise so it can delay loading for an async task (i.e. user login)canLoad
hookPreloadAllModules
could be used in concert withcanLoad
for a simple selective loading strategyEnvironment
The text was updated successfully, but these errors were encountered: