Permalink
Browse files

feat(router): add router preloader to optimistically preload routes

  • Loading branch information...
vsavkin authored and alexeagle committed Sep 16, 2016
1 parent 671f734 commit 5a849829c42330d7e88e83e916e6e36380c97a97
@@ -15,7 +15,9 @@ export {CanActivate, CanActivateChild, CanDeactivate, CanLoad, Resolve} from './
export {Event, NavigationCancel, NavigationEnd, NavigationError, NavigationExtras, NavigationStart, Router, RoutesRecognized} from './router';
export {ExtraOptions, RouterModule, provideRoutes} from './router_module';
export {RouterOutletMap} from './router_outlet_map';
+export {NoPreloading, PreloadAllModules, PreloadingStrategy} from './router_preloader';
export {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot} from './router_state';
export {PRIMARY_OUTLET, Params} from './shared';
export {DefaultUrlSerializer, UrlSegment, UrlSerializer, UrlTree} from './url_tree';
+
export * from './private_export'
@@ -317,6 +317,15 @@ export class Router {
this.currentRouterState = createEmptyState(this.currentUrlTree, this.rootComponentType);
}
+ /**
+ * @internal
+ * TODO: this should be removed once the constructor of the router made internal
+ */
+ resetRootComponentType(rootComponentType: Type<any>): void {
+ this.rootComponentType = rootComponentType;
+ this.currentRouterState = createEmptyState(this.currentUrlTree, this.rootComponentType);
+ }
+
/**
* Sets up the location change listener and performs the initial navigation.
*/
@@ -16,6 +16,7 @@ import {RouterOutlet} from './directives/router_outlet';
import {ErrorHandler, Router} from './router';
import {ROUTES} from './router_config_loader';
import {RouterOutletMap} from './router_outlet_map';
+import {NoPreloading, PreloadAllModules, PreloadingStrategy, RouterPreloader} from './router_preloader';
import {ActivatedRoute} from './router_state';
import {DefaultUrlSerializer, UrlSerializer} from './url_tree';
import {flatten} from './utils/collection';
@@ -58,8 +59,8 @@ export const ROUTER_PROVIDERS: Provider[] = [
]
},
RouterOutletMap, {provide: ActivatedRoute, useFactory: rootRoute, deps: [Router]},
- {provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader},
- {provide: ROUTER_CONFIGURATION, useValue: {enableTracing: false}}
+ {provide: NgModuleFactoryLoader, useClass: SystemJsNgModuleLoader}, RouterPreloader, NoPreloading,
+ PreloadAllModules, {provide: ROUTER_CONFIGURATION, useValue: {enableTracing: false}}
];
/**
@@ -145,6 +146,11 @@ export class RouterModule {
PlatformLocation, [new Inject(APP_BASE_HREF), new Optional()], ROUTER_CONFIGURATION
]
},
+ {
+ provide: PreloadingStrategy,
+ useExisting: config && config.preloadingStrategy ? config.preloadingStrategy :
+ NoPreloading
+ },
provideRouterInitializer()
]
};
@@ -220,19 +226,19 @@ export interface ExtraOptions {
* A custom error handler.
*/
errorHandler?: ErrorHandler;
+
+ /**
+ * Configures a preloading strategy. See {@link PreloadAllModules}.
+ */
+ preloadingStrategy?: any;
}
export function setupRouter(
ref: ApplicationRef, urlSerializer: UrlSerializer, outletMap: RouterOutletMap,
location: Location, injector: Injector, loader: NgModuleFactoryLoader, compiler: Compiler,
config: Route[][], opts: ExtraOptions = {}) {
- if (ref.componentTypes.length == 0) {
- throw new Error('Bootstrap at least one component before injecting Router.');
- }
- const componentType = ref.componentTypes[0];
const r = new Router(
- componentType, urlSerializer, outletMap, location, injector, loader, compiler,
- flatten(config));
+ null, urlSerializer, outletMap, location, injector, loader, compiler, flatten(config));
if (opts.errorHandler) {
r.errorHandler = opts.errorHandler;
@@ -254,8 +260,11 @@ export function rootRoute(router: Router): ActivatedRoute {
return router.routerState.root;
}
-export function initialRouterNavigation(router: Router, opts: ExtraOptions) {
+export function initialRouterNavigation(
+ router: Router, ref: ApplicationRef, preloader: RouterPreloader, opts: ExtraOptions) {
return () => {
+ router.resetRootComponentType(ref.componentTypes[0]);
+ preloader.setUpPreloading();
if (opts.initialNavigation === false) {
router.setUpLocationChangeListener();
} else {
@@ -269,6 +278,6 @@ export function provideRouterInitializer() {
provide: APP_BOOTSTRAP_LISTENER,
multi: true,
useFactory: initialRouterNavigation,
- deps: [Router, ROUTER_CONFIGURATION]
+ deps: [Router, ApplicationRef, RouterPreloader, ROUTER_CONFIGURATION]
};
}
@@ -0,0 +1,124 @@
+/**
+*@license
+*Copyright Google Inc. All Rights Reserved.
+*
+*Use of this source code is governed by an MIT-style license that can be
+*found in the LICENSE file at https://angular.io/license
+*/
+
+import {Compiler, ComponentFactoryResolver, Injectable, Injector, NgModuleFactory, NgModuleFactoryLoader} from '@angular/core';
+import {Observable} from 'rxjs/Observable';
+import {Subscription} from 'rxjs/Subscription';
+import {from} from 'rxjs/observable/from';
+import {of } from 'rxjs/observable/of';
+import {_catch} from 'rxjs/operator/catch';
+import {concatMap} from 'rxjs/operator/concatMap';
+import {filter} from 'rxjs/operator/filter';
+import {map} from 'rxjs/operator/map';
+import {mergeAll} from 'rxjs/operator/mergeAll';
+import {mergeMap} from 'rxjs/operator/mergeMap';
+
+import {Route, Routes} from './config';
+import {NavigationEnd, Router} from './router';
+import {RouterConfigLoader} from './router_config_loader';
+
+/**
+ * @whatItDoes Provides a preloading strategy.
+ *
+ * @experimental
+ */
+export abstract class PreloadingStrategy {
+ abstract preload(route: Route, fn: () => Observable<any>): Observable<any>;
+}
+
+/**
+ * @whatItDoes Provides a preloading strategy that preloads all modules as quicky as possible.
+ *
+ * @howToUse
+ *
+ * ```
+ * RouteModule.forRoot(ROUTES, {preloadingStrategy: PreloadAllModules})
+ * ```
+ *
+ * @experimental
+ */
+export class PreloadAllModules implements PreloadingStrategy {
+ preload(route: Route, fn: () => Observable<any>): Observable<any> {
+ return _catch.call(fn(), () => of (null));
+ }
+}
+
+/**
+ * @whatItDoes Provides a preloading strategy that does not preload any modules.
+ *
+ * @description
+ *
+ * This strategy is enabled by default.
+ *
+ * @experimental
+ */
+export class NoPreloading implements PreloadingStrategy {
+ preload(route: Route, fn: () => Observable<any>): Observable<any> { return of (null); }
+}
+
+/**
+ * The preloader optimistically loads all router configurations to
+ * make navigations into lazily-loaded sections of the application faster.
+ *
+ * The preloader runs in the background. When the router bootstraps, the preloader
+ * starts listening to all navigation events. After every such event, the preloader
+ * will check if any configurations can be loaded lazily.
+ *
+ * If a route is protected by `canLoad` guards, the preloaded will not load it.
+ */
+@Injectable()
+export class RouterPreloader {
+ private loader: RouterConfigLoader;
+ private subscription: Subscription;
+
+ constructor(
+ private router: Router, moduleLoader: NgModuleFactoryLoader, compiler: Compiler,
+ private injector: Injector, private preloadingStrategy: PreloadingStrategy) {
+ this.loader = new RouterConfigLoader(moduleLoader, compiler);
+ };
+
+ setUpPreloading(): void {
+ const navigations = filter.call(this.router.events, (e: any) => e instanceof NavigationEnd);
+ this.subscription = concatMap.call(navigations, () => this.preload()).subscribe((v: any) => {});
+ }
+
+ preload(): Observable<any> { return this.processRoutes(this.injector, this.router.config); }
+
+ ngOnDestroy() { this.subscription.unsubscribe(); }
+
+ private processRoutes(injector: Injector, routes: Routes): Observable<void> {
+ const res: Observable<any>[] = [];
+ for (let c of routes) {
+ // we already have the config loaded, just recurce
+ if (c.loadChildren && !c.canLoad && (<any>c)._loadedConfig) {
+ const childConfig = (<any>c)._loadedConfig;
+ res.push(this.processRoutes(childConfig.injector, childConfig.routes));
+
+ // no config loaded, fetch the config
+ } else if (c.loadChildren && !c.canLoad) {
+ res.push(this.preloadConfig(injector, c));
+
+ // recurse into children
+ } else if (c.children) {
+ res.push(this.processRoutes(injector, c.children));
+ }
+ }
+ return mergeAll.call(from(res));
+ }
+
+ private preloadConfig(injector: Injector, route: Route): Observable<void> {
+ return this.preloadingStrategy.preload(route, () => {
+ const loaded = this.loader.load(injector, route.loadChildren);
+ return mergeMap.call(loaded, (config: any): any => {
+ const c: any = route;
+ c._loadedConfig = config;
+ return this.processRoutes(config.injector, config.routes);
+ });
+ });
+ }
+}
@@ -14,7 +14,8 @@ import {Observable} from 'rxjs/Observable';
import {of } from 'rxjs/observable/of';
import {map} from 'rxjs/operator/map';
-import {ActivatedRoute, ActivatedRouteSnapshot, CanActivate, CanDeactivate, Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Params, Resolve, Router, RouterModule, RouterStateSnapshot, RoutesRecognized} from '../index';
+import {ActivatedRoute, ActivatedRouteSnapshot, CanActivate, CanDeactivate, Event, NavigationCancel, NavigationEnd, NavigationError, NavigationStart, Params, PreloadAllModules, PreloadingStrategy, Resolve, Router, RouterModule, RouterStateSnapshot, RoutesRecognized} from '../index';
+import {RouterPreloader} from '../src/router_preloader';
import {RouterTestingModule, SpyNgModuleFactoryLoader} from '../testing';
@@ -1413,7 +1414,7 @@ describe('Integration', () => {
it('should not set the class until the first navigation succeeds', fakeAsync(() => {
@Component({
template:
- '<router-outlet></router-outlet><a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" >'
+ '<router-outlet></router-outlet><a routerLink="/" routerLinkActive="active" [routerLinkActiveOptions]="{exact: true}" ></a>'
})
class RootCmpWithLink {
}
@@ -1532,6 +1533,7 @@ describe('Integration', () => {
expect(location.path()).toEqual('/lazy/loaded/child');
expect(fixture.nativeElement).toHaveText('lazy-loaded-parent [lazy-loaded-child]');
})));
+
it('throws an error when forRoot() is used in a lazy context',
fakeAsync(inject(
[Router, Location, NgModuleFactoryLoader],
@@ -1702,6 +1704,60 @@ describe('Integration', () => {
recordedEvents,
[[NavigationStart, '/lazy/loaded'], [NavigationError, '/lazy/loaded']]);
})));
+
+ describe('preloading', () => {
+ beforeEach(() => {
+ TestBed.configureTestingModule(
+ {providers: [{provide: PreloadingStrategy, useExisting: PreloadAllModules}]});
+ const preloader = TestBed.get(RouterPreloader);
+ preloader.setUpPreloading();
+ });
+
+ it('should work',
+ fakeAsync(inject(
+ [Router, Location, NgModuleFactoryLoader],
+ (router: Router, location: Location, loader: SpyNgModuleFactoryLoader) => {
+ @Component({selector: 'lazy', template: 'should not show'})
+ class LazyLoadedComponent {
+ }
+
+ @NgModule({
+ declarations: [LazyLoadedComponent],
+ imports: [RouterModule.forChild(
+ [{path: 'LoadedModule2', component: LazyLoadedComponent}])]
+ })
+ class LoadedModule2 {
+ }
+
+ @NgModule({
+ imports:
+ [RouterModule.forChild([{path: 'LoadedModule1', loadChildren: 'expected2'}])]
+ })
+ class LoadedModule1 {
+ }
+
+ loader.stubbedModules = {expected: LoadedModule1, expected2: LoadedModule2};
+
+ const fixture = createRoot(router, RootCmp);
+
+ router.resetConfig([
+ {path: 'blank', component: BlankCmp}, {path: 'lazy', loadChildren: 'expected'}
+ ]);
+
+ router.navigateByUrl('/blank');
+ advance(fixture);
+
+ const config: any = router.config;
+ const firstConfig = config[1]._loadedConfig;
+ expect(firstConfig).toBeDefined();
+ expect(firstConfig.routes[0].path).toEqual('LoadedModule1');
+
+ const secondConfig = firstConfig.routes[0]._loadedConfig;
+ expect(secondConfig).toBeDefined();
+ expect(secondConfig.routes[0].path).toEqual('LoadedModule2');
+ })));
+
+ });
});
});
Oops, something went wrong.

0 comments on commit 5a84982

Please sign in to comment.