docs(router): Added example of feature module pre-loading (#2595)

* added example of feature module pre-loading
* added transition aliases for route animations
This commit is contained in:
Brandon 2016-10-20 00:02:47 -05:00 committed by Ward Bell
parent 3eb7a41602
commit 2f5306f1b6
16 changed files with 379 additions and 50 deletions

View File

@ -27,7 +27,10 @@ describe('Router', function () {
heroDetailTitle: element(by.css('my-app > ng-component > div > h3')),
adminHref: hrefEles.get(2),
loginHref: hrefEles.get(3)
adminPreloadList: element.all(by.css('my-app > ng-component > ng-component > ul > li')),
loginHref: hrefEles.get(3),
loginButton: element.all(by.css('my-app > ng-component > p > button')),
};
}
@ -105,6 +108,16 @@ describe('Router', function () {
});
});
it('should be able to see the preloaded modules', function () {
let page = getPageStruct();
page.loginHref.click().then(function() {
return page.loginButton.click();
}).then(function() {
expect(page.adminPreloadList.count()).toBe(1, 'should be 1 preloaded module');
expect(page.adminPreloadList.first().getText()).toBe('crisis-center', 'first preload should be crisis center');
});
});
function crisisCenterEdit(index: number, shouldSave: boolean) {
let page = getPageStruct();
let crisisEle: ElementFinder;

View File

@ -0,0 +1,33 @@
// #docregion
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
@Component({
template: `
<p>Dashboard</p>
<p>Session ID: {{ sessionId | async }}</p>
<a id="anchor"></a>
<p>Token: {{ token | async }}</p>
`
})
export class AdminDashboardComponent implements OnInit {
sessionId: Observable<string>;
token: Observable<string>;
constructor(private route: ActivatedRoute) {}
ngOnInit() {
// Capture the session ID if available
this.sessionId = this.route
.queryParams
.map(params => params['session_id'] || 'None');
// Capture the fragment if available
this.token = this.route
.fragment
.map(fragment => fragment || 'None');
}
}

View File

@ -1,7 +1,9 @@
// #docregion
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { PreloadSelectedModules } from '../selective-preload-strategy';
import 'rxjs/add/operator/map';
@Component({
@ -11,13 +13,24 @@ import 'rxjs/add/operator/map';
<p>Session ID: {{ sessionId | async }}</p>
<a id="anchor"></a>
<p>Token: {{ token | async }}</p>
Preloaded Modules
<ul>
<li *ngFor="let module of modules">{{ module }}</li>
</ul>
`
})
export class AdminDashboardComponent implements OnInit {
sessionId: Observable<string>;
token: Observable<string>;
modules: string[];
constructor(private route: ActivatedRoute) {}
constructor(
private route: ActivatedRoute,
private preloadStrategy: PreloadSelectedModules
) {
this.modules = preloadStrategy.preloadedModules;
}
ngOnInit() {
// Capture the session ID if available

View File

@ -0,0 +1,33 @@
// #docplaster
// #docregion
import { NgModule } from '@angular/core';
// #docregion import-router
import { RouterModule } from '@angular/router';
// #enddocregion import-router
import { CanDeactivateGuard } from './can-deactivate-guard.service';
// #docregion can-load-guard
import { AuthGuard } from './auth-guard.service';
// #enddocregion can-load-guard
// #docregion lazy-load-admin, can-load-guard
@NgModule({
imports: [
RouterModule.forRoot([
{
path: 'admin',
loadChildren: 'app/admin/admin.module#AdminModule',
// #enddocregion lazy-load-admin
canLoad: [AuthGuard]
// #docregion lazy-load-admin
}
])
],
exports: [
RouterModule
],
providers: [
CanDeactivateGuard
]
})
export class AppRoutingModule {}

View File

@ -0,0 +1,44 @@
// #docplaster
// #docregion, preload-v1
import { NgModule } from '@angular/core';
import {
RouterModule,
// #enddocregion preload-v1
PreloadAllModules
// #docregion preload-v1
} from '@angular/router';
import { CanDeactivateGuard } from './can-deactivate-guard.service';
import { AuthGuard } from './auth-guard.service';
@NgModule({
imports: [
RouterModule.forRoot([
{
path: 'admin',
loadChildren: 'app/admin/admin.module#AdminModule',
canLoad: [AuthGuard]
},
{
path: '',
redirectTo: '/heroes',
pathMatch: 'full'
},
{
path: 'crisis-center',
loadChildren: 'app/crisis-center/crisis-center.module#CrisisCenterModule'
},
],
// #enddocregion preload-v1
{ preloadingStrategy: PreloadAllModules }
// #docregion preload-v1
)
],
exports: [
RouterModule
],
providers: [
CanDeactivateGuard
]
})
export class AppRoutingModule {}

View File

@ -1,33 +1,43 @@
// #docplaster
// #docregion
// #docregion, preload-v1
import { NgModule } from '@angular/core';
// #docregion import-router
import { RouterModule } from '@angular/router';
// #enddocregion import-router
import { CanDeactivateGuard } from './can-deactivate-guard.service';
// #docregion can-load-guard
import { AuthGuard } from './auth-guard.service';
// #enddocregion can-load-guard
import { PreloadSelectedModules } from './selective-preload-strategy';
// #docregion lazy-load-admin, can-load-guard
@NgModule({
imports: [
RouterModule.forRoot([
{
path: 'admin',
loadChildren: 'app/admin/admin.module#AdminModule',
// #enddocregion lazy-load-admin
canLoad: [AuthGuard]
// #docregion lazy-load-admin
},
{
path: '',
redirectTo: '/heroes',
pathMatch: 'full'
},
// #docregion preload-v2
{
path: 'crisis-center',
loadChildren: 'app/crisis-center/crisis-center.module#CrisisCenterModule',
data: {
preload: true
}
}
])
// #enddocregion preload-v2
],
{ preloadingStrategy: PreloadSelectedModules })
],
exports: [
RouterModule
],
providers: [
CanDeactivateGuard
CanDeactivateGuard,
PreloadSelectedModules
]
})
export class AppRoutingModule {}

View File

@ -9,6 +9,8 @@ import { AppRoutingModule } from './app-routing.module';
import { HeroesModule } from './heroes/heroes.module';
import { CrisisCenterModule } from './crisis-center/crisis-center.module';
import { LoginRoutingModule } from './login-routing.module';
import { LoginComponent } from './login.component';
import { DialogService } from './dialog.service';
@NgModule({
@ -21,7 +23,8 @@ import { DialogService } from './dialog.service';
AppRoutingModule
],
declarations: [
AppComponent
AppComponent,
LoginComponent
],
providers: [
DialogService

View File

@ -5,11 +5,9 @@ import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { LoginRoutingModule } from './login-routing.module';
import { HeroesModule } from './heroes/heroes.module';
import { CrisisCenterModule } from './crisis-center/crisis-center.module';
import { LoginRoutingModule } from './login-routing.module';
import { LoginComponent } from './login.component';
import { DialogService } from './dialog.service';
@ -19,7 +17,6 @@ import { DialogService } from './dialog.service';
BrowserModule,
FormsModule,
HeroesModule,
CrisisCenterModule,
LoginRoutingModule,
AppRoutingModule
],

View File

@ -0,0 +1,60 @@
// #docplaster
// #docregion
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { CrisisCenterHomeComponent } from './crisis-center-home.component';
import { CrisisListComponent } from './crisis-list.component';
import { CrisisCenterComponent } from './crisis-center.component';
import { CrisisDetailComponent } from './crisis-detail.component';
import { CanDeactivateGuard } from '../can-deactivate-guard.service';
// #docregion crisis-detail-resolve
import { CrisisDetailResolve } from './crisis-detail-resolve.service';
@NgModule({
imports: [
RouterModule.forChild([
// #docregion redirect
{
path: '',
redirectTo: '/crisis-center',
pathMatch: 'full'
},
// #enddocregion redirect
{
path: 'crisis-center',
component: CrisisCenterComponent,
children: [
{
path: '',
component: CrisisListComponent,
children: [
{
path: ':id',
component: CrisisDetailComponent,
canDeactivate: [CanDeactivateGuard],
resolve: {
crisis: CrisisDetailResolve
}
},
{
path: '',
component: CrisisCenterHomeComponent
}
]
}
]
}
])
],
exports: [
RouterModule
],
providers: [
CrisisDetailResolve
]
})
export class CrisisCenterRoutingModule { }
// #enddocregion

View File

@ -16,15 +16,8 @@ import { CrisisDetailResolve } from './crisis-detail-resolve.service';
@NgModule({
imports: [
RouterModule.forChild([
// #docregion redirect
{
path: '',
redirectTo: '/crisis-center',
pathMatch: 'full'
},
// #enddocregion redirect
{
path: 'crisis-center',
component: CrisisCenterComponent,
children: [
{

View File

@ -35,14 +35,14 @@ import { DialogService } from '../dialog.service';
transform: 'translateX(0)'
})
),
transition('void => *', [
transition(':enter', [
style({
opacity: 0,
transform: 'translateX(-100%)'
}),
animate('0.2s ease-in')
]),
transition('* => void', [
transition(':leave', [
animate('0.5s ease-out', style({
opacity: 0,
transform: 'translateY(100%)'

View File

@ -34,14 +34,14 @@ import { DialogService } from '../dialog.service';
transform: 'translateX(0)'
})
),
transition('void => *', [
transition(':enter', [
style({
opacity: 0,
transform: 'translateX(-100%)'
}),
animate('0.2s ease-in')
]),
transition('* => void', [
transition(':leave', [
animate('0.5s ease-out', style({
opacity: 0,
transform: 'translateY(100%)'

View File

@ -34,14 +34,14 @@ import { Hero, HeroService } from './hero.service';
transform: 'translateX(0)'
})
),
transition('void => *', [
transition(':enter', [
style({
opacity: 0,
transform: 'translateX(-100%)'
}),
animate('0.2s ease-in')
]),
transition('* => void', [
transition(':leave', [
animate('0.5s ease-out', style({
opacity: 0,
transform: 'translateY(100%)'

View File

@ -0,0 +1,24 @@
// #docregion
import 'rxjs/add/observable/of';
import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable } from 'rxjs/Observable';
@Injectable()
export class PreloadSelectedModules implements PreloadingStrategy {
preloadedModules: string[] = [];
preload(route: Route, load: Function): Observable<any> {
if (route.data && route.data['preload']) {
// add the route path to our preloaded module array
this.preloadedModules.push(route.path);
// log the route path to the console
console.log('Preloaded: ' + route.path);
return load();
} else {
return Observable.of(null);
}
}
}

View File

@ -3,7 +3,7 @@
"files":[
"!**/*.d.ts",
"!**/*.js",
"!**/*.[1,2,3,4,5,6].*",
"!**/*.[0-9].*",
"!app/crisis-list.component.ts",
"!app/hero-list.component.ts",
"!app/crisis-center/add-crisis.component.ts",

View File

@ -59,6 +59,8 @@ include ../_util-fns
* providing optional information across routes with [query parameters](#query-parameters)
* jumping to anchor elements using a [fragment](#fragment)
* loading feature areas [asynchronously](#asynchronous-routing)
* pre-loading feature areas [during navigation](#preloading)
* using a [custom strategy](#custom-preloading) to only pre-load certain features
* choosing the "HTML5" or "hash" [URL style](#browser-url-styles)
We proceed in phases marked by milestones building from a simple two-pager with placeholder views
@ -1140,8 +1142,8 @@ h3#route-animation Adding animations to the route component
:marked
Next, we'll use a **host binding** for route animations named *@routeAnimation*. There is nothing special
about the choice of the binding name, but since we are controlling route animation, we'll go with `routeAnimation`.
The binding value is set to `true` because we only care about the `*` and `void` states which are
[entering and leaving](../guide/animations.html#example-entering-and-leaving) animation states.
The binding value is set to `true` because we only care about the `:enter` and `:leave` states which are
[entering and leaving](../api/core/index/transition-function.html#transition-aliases-enter-and-leave-) transition aliases.
We'll also add some display and positioning bindings for styling.
@ -1150,8 +1152,8 @@ h3#route-animation Adding animations to the route component
:marked
Now we can build our animation trigger, which we'll call *routeAnimation* to match the binding we previously
setup. We'll use the **wildcard state** that matches any animation state our route component is in, along with
two *transitions*. One transition animates the component as it enters the application view (`void => *`), while the other
animates the component as it leaves the application view (`* => void`).
two *transitions*. One transition animates the component as it enters the application view (`:enter`), while the other
animates the component as it leaves the application view (`:leave`).
We could add different transitions to different route components depending on our needs. We'll just animate our `HeroDetailComponent` for this milestone.
@ -1903,7 +1905,7 @@ h3#resolve-guard <i>Resolve</i>: pre-fetching component data
We need the `Resolve` guard.
### Preload route information
### Fetch data before navigating
We'll update our `Crisis Detail` route to resolve our Crisis before loading the route, or if the user happens to
navigate to an invalid crisis center `:id`, we'll navigate back to our list of existing crises.
@ -1935,7 +1937,7 @@ h3#resolve-guard <i>Resolve</i>: pre-fetching component data
We'll add the `CrisisDetailResolve` service to our `CrisisCenterRoutingModule`'s `providers`, so its available to the `Router` during the navigation process.
+makeExcerpt('app/crisis-center/crisis-center-routing.module.ts (resolve)', 'crisis-detail-resolve')
+makeExcerpt('app/crisis-center/crisis-center-routing.module.4.ts (resolve)', 'crisis-detail-resolve')
:marked
Now that we've added our `Resolve` guard to fetch data before the route loads, we no longer need to do this once we get into our `CrisisDetailComponent`.
@ -1959,7 +1961,7 @@ h3#resolve-guard <i>Resolve</i>: pre-fetching component data
`router/ts/app/app.component.ts,
router/ts/app/crisis-center/crisis-center-home.component.ts,
router/ts/app/crisis-center/crisis-center.component.ts,
router/ts/app/crisis-center/crisis-center-routing.module.ts,
router/ts/app/crisis-center/crisis-center-routing.module.4.ts,
router/ts/app/crisis-center/crisis-list.component.ts,
router/ts/app/crisis-center/crisis-detail.component.ts,
router/ts/app/crisis-center/crisis-detail-resolve.service.ts,
@ -2017,7 +2019,7 @@ a#fragment
Since we'll be navigating to our *Admin Dashboard* route after logging in, we'll update it to handle our
query parameters and fragment.
+makeExcerpt('app/admin/admin-dashboard.component.ts (v2)', '')
+makeExcerpt('app/admin/admin-dashboard.component.2.ts (v2)', '')
:marked
*Query Parameters* and *Fragments* are also available through the `ActivatedRoute` service available to route components.
@ -2075,7 +2077,7 @@ a#fragment
our child routes.
+makeTabs(
`router/ts/app/app-routing.module.ts,
`router/ts/app/app-routing.module.5.ts,
router/ts/app/admin/admin-routing.module.ts`,
'lazy-load-admin,',
`app-routing.module.ts (load children),
@ -2107,7 +2109,7 @@ a#fragment
to break our `AdminModule` into a completely separate module. In our `app.module.ts`, we'll remove our `AdminModule` from the
`imports` array since we'll be loading it on-demand an we'll remove the imported `AdminModule`.
+makeExcerpt('app/app.module.ts (async admin module)', '')
+makeExcerpt('app/app.module.7.ts (async admin module)', '')
h3#can-load-guard <i>CanLoad Guard</i>: guarding against loading of feature modules
:marked
@ -2133,7 +2135,111 @@ h3#can-load-guard <i>CanLoad Guard</i>: guarding against loading of feature modu
Next, we'll import the `AuthGuard` into our `app-routing.module.ts` and add the `AuthGuard` to the `canLoad` array for
our `admin` route. Now our `admin` feature area is only loaded when the proper access has been granted.
+makeExcerpt('app/app-routing.module.ts (can load guard)', 'can-load-guard')
+makeExcerpt('app/app-routing.module.5.ts (can load guard)', 'can-load-guard')
h3#preloading <i>Pre-Loading</i>: background loading of feature areas
:marked
We've learned how to load modules on-demand, but we can also take advantage of loading feature areas modules in *advance*. The *Router*
supports **pre-loading** of asynchronous feature areas prior to navigation to their respective URL. Pre-loading allows us to to load our initial route
quickly, while other feature modules are loaded in the background. Once we navigate to those areas, they will have already been loaded
as if our they were included in our initial bundle.
Each time a **successful** navigation happens, the *Router* will look through our configuration for lazy loaded feature areas
and react based on the provided strategy.
The *Router* supports two pre-loading strategies by default:
* No pre-loading at all which is the default. Lazy loaded feature areas are still loaded on demand.
* Pre-loading of all lazy loaded feature areas.
The *Router* also supports [custom preloading strategies](#custom-preloading) to give us control of what we want to pre-load.
We'll update our *CrisisCenterModule* to be loaded lazily by default and use the `PreloadAllModules` strategy to eagerly
it them up initial navigation.
<a id="preload-canload"></a>
.l-sub-section
:marked
The **PreloadAllModules** strategy does not eagerly load feature areas protected by the [Can Load](#can-load-guard) and this is by design.
The *CanLoad* guard protects against loading feature area assets until authorized to do so. If you want to eagerly load all modules and guard
them against unauthorized access, use the [CanActivate](#can-activate-guard) guard instead.
:marked
We'll update our route configuration to eagerly load the *CrisisCenterModule*. We follow the same process as we did when we loaded the *AdminModule* asynchronously.
In the *crisis-center-routing.module.ts*, we'll change the *crisis-center* path to an *empty path* route.
We'll move our redirect and *crisis-center* route to our `AppRoutingModule` routes and use the `loadChildren` string to load the *CrisisCenterModule*.
The redirect is also changed to load the `/heroes` route on initial load.
Once we're finished, we'll remove the `CrisisCenterModule` from our `AppModule`'s imports.
Here are our updated modules:
+makeTabs(
`router/ts/app/app.module.ts,
router/ts/app/app-routing.module.6.ts,
router/ts/app/crisis-center/crisis-center-routing.module.ts
`,
',preload-v1,',
`app.module.ts,
app-routing.module.ts,
crisis-center-routing.module.ts
`)
:marked
In order to enable pre-loading of all modules, we'll import the `PreloadAllModules` token from the router package. The second argument in the
`RouterModule.forRoot` method takes an object where we can provide additional configuration options. We'll use the `preloadingStrategy` property
with the `PreloadAllModules` token. This enables the built-in *Router* pre-loader to eagerly load **all** [unguarded](#preload-canload) feature areas that use `loadChildren`.
+makeExcerpt('app/app-routing.module.6.ts (preload all)', '')
:marked
Now when we visit `http://localhost:3000`, the `/heroes` route will load in the foreground, while the *CrisisCenterModule* and any other asynchronous feature
modules we could have are _eagerly_ loaded in the background, waiting for us to navigate to them.
<a id="custom-preloading"></a>
:marked
### Custom Pre-Loading Strategy
Pre-loading all modules works well in some situations, but in some cases we need more control over what gets loaded eagerly. This becomes more clear
as we load our application on a mobile device, or a low bandwidth connection. We may only want to preload certain feature modules based on user metrics
or other data points we gather over time. The *Router* lets us have more control with a **custom** preloading strategy.
We can define our own strategy the same way the **PreloadAllModules** modules strategy was provided to our *RouterModule.forRoot* configuration object.
Since we want to take advantage of this, we'll add a custom strategy that _only_ preloads the modules we select. We'll enable the preloading by using the *Route Data*,
which we learned is an object to store arbitrary route data and and [resolve data](#resolve-guard).
We'll add a custom `preload` boolean to our `crisis-center` route data that we'll use with our custom strategy. To see it in action, we'll add to
the `route.path` to the `preloadedModules` array in our custom strategy service. We'll also log a message
to the console for the preloaded module.
+makeExcerpt('app/app-routing.module.ts (route data preload)', 'preload-v2')
:marked
To create our custom strategy we'll need to implement the abstract `PreloadingStrategy` class and the `preload` method. The `preload` method is called for each route
that loads its feature module asynchronously and determines whether to preload it. The `preload` method takes two arguments, the first being the `Route` that provides
the route configuration and a function that preloads the feature module.
We'll name our strategy **PreloadSelectedModules** since we _only_ want to preload based on certain criteria. Our custom strategy looks for the **`preload`** boolean
value in our `Route Data` and if its true, it calls the `load` function provided by the built-in `Router` pre-loader that eagerly loads feature modules.
+makeExcerpt('app/selective-preload-strategy.ts (preload selected modules)', '')
:marked
In order to use our custom preloading strategy, we import it into our `app-routing.module.ts` and replace the `PreloadAllModules` strategy. We also add
the `PreloadSelectedModules` strategy to the `AppRoutingModule` providers array. This allows the *Router* pre-loader to inject our custom strategy.
To confirm our *CrisisCenterModule* is being pre-loaded, we'll display our `preloadedModules` in the `Admin` dashboard. We already know how to use
an *ngFor* loop, so we'll skip over the details here. Since the `PreloadSelectedModules` is just a service, we can inject it into the `AdminDashboardComponent`
and wire it up to our list.
+makeExcerpt('app/admin/admin-dashboard.component.ts (preloaded modules)', '')
:marked
Once our application is loaded to our initial route, the *CrisisCenterModule* is loaded eagerly. We can verify this by logging in to the `Admin` feature area and
noting that the `crisis-center` is listed in the `Preloaded Modules` and logged to the console. We can continue to add feature modules to be selectively loaded eagerly.
<a id="final-app"></a>
.l-main-section