完成上面的里程碑后,应用程序很自然地长大了。在某一个时间点,你将达到一个顶点,应用将会需要过多的时间来加载。
为了解决这个问题,请使用异步路由,它会根据请求来惰性加载某些特性模块。惰性加载有很多好处。
你已经完成了一部分。通过把应用组织成一些模块:AppModule
、HeroesModule
、AdminModule
和 CrisisCenterModule
,你已经有了可用于实现惰性加载的候选者。
有些模块(比如 AppModule
)必须在启动时加载,但其它的都可以而且应该惰性加载。比如 AdminModule
就只有少数已认证的用户才需要它,所以你应该只有在正确的人请求它时才加载。
把 admin-routing.module.ts
中的 admin
路径从 'admin'
改为空路径 ''
。
可以用空路径路由来对路由进行分组,而不用往 URL 中添加额外的路径片段。用户仍旧访问 /admin
,并且 AdminComponent
仍然作为用来包含子路由的路由组件。
打开 AppRoutingModule
,并把一个新的 admin
路由添加到它的 appRoutes
数组中。
给它一个 loadChildren
属性(替换掉 children
属性)。loadChildren
属性接收一个函数,该函数使用浏览器内置的动态导入语法 import('...')
来惰性加载代码,并返回一个承诺(Promise)。其路径是 AdminModule
的位置(相对于应用的根目录)。当代码请求并加载完毕后,这个 Promise
就会解析成一个包含 NgModule
的对象,也就是 AdminModule
。
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
},
注意:
当使用绝对路径时,NgModule
的文件位置必须以 src/app
开头,以便正确解析。对于自定义的 使用绝对路径的路径映射表,你必须在项目的 tsconfig.json
中必须配置好 baseUrl
和 paths
属性。
当路由器导航到这个路由时,它会用 loadChildren
字符串来动态加载 AdminModule
,然后把 AdminModule
添加到当前的路由配置中,最后,它把所请求的路由加载到目标 admin
组件中。
惰性加载和重新配置工作只会发生一次,也就是在该路由首次被请求时。在后续的请求中,该模块和路由都是立即可用的。
最后一步是把管理特性区从主应用中完全分离开。根模块 AppModule
既不能加载也不能引用 AdminModule
及其文件。
在 app.module.ts
中,从顶部移除 AdminModule
的导入语句,并且从 NgModule 的 imports
数组中移除 AdminModule
。
你已经使用 CanActivate
保护 AdminModule
了,它会阻止未授权用户访问管理特性区。如果用户未登录,它就会跳转到登录页。
但是路由器仍然会加载 AdminModule
—— 即使用户无法访问它的任何一个组件。理想的方式是,只有在用户已登录的情况下你才加载 AdminModule
。
添加一个 CanLoad
守卫,它只在用户已登录并且尝试访问管理特性区的时候,才加载 AdminModule
一次。
现有的 AuthGuard
的 checkLogin()
方法中已经有了支持 CanLoad
守卫的基础逻辑。
auth.guard.ts
。@angular/router
导入 CanLoad
接口。AuthGuard
类的 implements
列表中。canLoad()
:canLoad(route: Route): boolean {
const url = `/${route.path}`;
return this.checkLogin(url);
}
路由器会把 canLoad()
方法的 route
参数设置为准备访问的目标 URL。如果用户已经登录了,checkLogin()
方法就会重定向到那个 URL。
现在,把 AuthGuard
导入到 AppRoutingModule
中,并把 AuthGuard
添加到 admin
路由的 canLoad
数组中。完整的 admin
路由是这样的:
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
canLoad: [AuthGuard]
},
除了按需加载模块外,还可以通过预加载方式异步加载模块。
当应用启动时,AppModule
被急性加载,这意味着它会立即加载。而 AdminModule
只在用户点击链接时加载,这叫做惰性加载。
预加载允许你在后台加载模块,以便当用户激活某个特定的路由时,就可以渲染这些数据了。考虑一下危机中心。它不是用户看到的第一个视图。默认情况下,英雄列表才是第一个视图。为了获得最小的初始有效负载和最快的启动时间,你应该急性加载 AppModule
和 HeroesModule
。
你可以惰性加载危机中心。但是,你几乎可以肯定用户会在启动应用之后的几分钟内访问危机中心。理想情况下,应用启动时应该只加载 AppModule
和 HeroesModule
,然后几乎立即开始后台加载 CrisisCenterModule
。在用户浏览到危机中心之前,该模块应该已经加载完毕,可供访问了。
在每次成功的导航后,路由器会在自己的配置中查找尚未加载并且可以预加载的模块。是否加载某个模块,以及要加载哪些模块,取决于预加载策略。
Router
提供了两种预加载策略:
策略 | 详情 |
---|---|
不预加载 | 这是默认值。惰性加载的特性区仍然会按需加载。 |
预加载 | 预加载所有惰性加载的特性区。 |
路由器或者完全不预加载或者预加载每个惰性加载模块。 路由器还支持自定义预加载策略,以便完全控制要预加载哪些模块以及何时加载。
本节将指导你把 CrisisCenterModule
改成惰性加载的,并使用 PreloadAllModules
策略来预加载所有惰性加载模块。
修改路由配置,来惰性加载 CrisisCenterModule
。修改的步骤和配置惰性加载 AdminModule
时一样。
CrisisCenterRoutingModule
中的路径从 crisis-center
改为空字符串。AppRoutingModule
中添加一个 crisis-center
路由。loadChildren
字符串来加载 CrisisCenterModule
。app.module.ts
中移除所有对 CrisisCenterModule
的引用。下面是打开预加载之前的模块修改版:
import { NgModule } from '@angular/core';app-routing.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { Router } from '@angular/router';
import { AppComponent } from './app.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
import { ComposeMessageComponent } from './compose-message/compose-message.component';
import { AppRoutingModule } from './app-routing.module';
import { HeroesModule } from './heroes/heroes.module';
import { AuthModule } from './auth/auth.module';
@NgModule({
imports: [
BrowserModule,
BrowserAnimationsModule,
FormsModule,
HeroesModule,
AuthModule,
AppRoutingModule,
],
declarations: [
AppComponent,
ComposeMessageComponent,
PageNotFoundComponent
],
bootstrap: [ AppComponent ]
})
export class AppModule {
}
import { NgModule } from '@angular/core';crisis-center-routing.module.ts
import {
RouterModule, Routes,
} from '@angular/router';
import { ComposeMessageComponent } from './compose-message/compose-message.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
import { AuthGuard } from './auth/auth.guard';
const appRoutes: Routes = [
{
path: 'compose',
component: ComposeMessageComponent,
outlet: 'popup'
},
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
canLoad: [AuthGuard]
},
{
path: 'crisis-center',
loadChildren: () => import('./crisis-center/crisis-center.module').then(m => m.CrisisCenterModule)
},
{ path: '', redirectTo: '/heroes', pathMatch: 'full' },
{ path: '**', component: PageNotFoundComponent }
];
@NgModule({
imports: [
RouterModule.forRoot(
appRoutes,
)
],
exports: [
RouterModule
]
})
export class AppRoutingModule {}
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CrisisCenterHomeComponent } from './crisis-center-home/crisis-center-home.component';
import { CrisisListComponent } from './crisis-list/crisis-list.component';
import { CrisisCenterComponent } from './crisis-center/crisis-center.component';
import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component';
import { CanDeactivateGuard } from '../can-deactivate.guard';
import { CrisisDetailResolverService } from './crisis-detail-resolver.service';
const crisisCenterRoutes: Routes = [
{
path: '',
component: CrisisCenterComponent,
children: [
{
path: '',
component: CrisisListComponent,
children: [
{
path: ':id',
component: CrisisDetailComponent,
canDeactivate: [CanDeactivateGuard],
resolve: {
crisis: CrisisDetailResolverService
}
},
{
path: '',
component: CrisisCenterHomeComponent
}
]
}
]
}
];
@NgModule({
imports: [
RouterModule.forChild(crisisCenterRoutes)
],
exports: [
RouterModule
]
})
export class CrisisCenterRoutingModule { }
你可以现在尝试它,并确认在点击了“Crisis Center”按钮之后加载了 CrisisCenterModule
。
要为所有惰性加载模块启用预加载功能,请从 Angular 的路由模块中导入 PreloadAllModules
。
RouterModule.forRoot()
方法的第二个参数接受一个附加配置选项对象。preloadingStrategy
就是其中之一。把 PreloadAllModules
添加到 forRoot()
调用中:
RouterModule.forRoot(
appRoutes,
{
enableTracing: true, // <-- debugging purposes only
preloadingStrategy: PreloadAllModules
}
)
这项配置会让 Router
预加载器立即加载所有惰性加载路由(带 loadChildren
属性的路由)。
当访问 http://localhost:4200
时,/heroes
路由立即随之启动,并且路由器在加载了 HeroesModule
之后立即开始加载 CrisisCenterModule
。
目前,AdminModule
并没有预加载,因为 CanLoad
阻塞了它。
PreloadAllModules
策略不会加载被CanLoad
守卫所保护的特性区。
几步之前,你刚刚给 AdminModule
中的路由添加了 CanLoad
守卫,以阻塞加载那个模块,直到用户认证结束。CanLoad
守卫的优先级高于预加载策略。
如果你要加载一个模块并且保护它防止未授权访问,请移除 canLoad
守卫,只单独依赖CanActivate
守卫。
在很多场景下,预加载的每个惰性加载模块都能正常工作。但是,考虑到低带宽和用户指标等因素,可以为特定的特性模块使用自定义预加载策略。
本节将指导你添加一个自定义策略,它只预加载 data.preload
标志为 true
路由。回想一下,你可以在路由的 data
属性中添加任何东西。
在 AppRoutingModule
的 crisis-center
路由中设置 data.preload
标志。
{
path: 'crisis-center',
loadChildren: () => import('./crisis-center/crisis-center.module').then(m => m.CrisisCenterModule),
data: { preload: true }
},
生成一个新的 SelectivePreloadingStrategy
服务。
ng generate service selective-preloading-strategy
使用下列内容替换 selective-preloading-strategy.service.ts
:
import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class SelectivePreloadingStrategyService implements PreloadingStrategy {
preloadedModules: string[] = [];
preload(route: Route, load: () => Observable<any>): Observable<any> {
if (route.data?.['preload'] && route.path != null) {
// add the route path to the preloaded module array
this.preloadedModules.push(route.path);
// log the route path to the console
console.log('Preloaded: ' + route.path);
return load();
} else {
return of(null);
}
}
}
SelectivePreloadingStrategyService
实现了 PreloadingStrategy
,它有一个方法 preload()
。
路由器会用两个参数来调用 preload()
方法:
preload
的实现要返回一个 Observable
。如果该路由应该预加载,它就会返回调用加载器函数所返回的 Observable
。如果该路由不应该预加载,它就返回一个 null
值的 Observable
对象。
在这个例子中,如果路由的 data.preload
标志是真值,则 preload()
方法会加载该路由。
它的副作用是 SelectivePreloadingStrategyService
会把所选路由的 path
记录在它的公共数组 preloadedModules
中。
很快,你就会扩展 AdminDashboardComponent
来注入该服务,并且显示它的 preloadedModules
数组。
但是首先,要对 AppRoutingModule
做少量修改。
SelectivePreloadingStrategyService
导入到 AppRoutingModule
中。 PreloadAllModules
策略替换成对 forRoot()
的调用,并且传入这个 SelectivePreloadingStrategyService
。现在,编辑 AdminDashboardComponent
以显示这些预加载路由的日志。
SelectivePreloadingStrategyService
(它是一个服务)。 preloadedModules
数组。现在文件如下:
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { SelectivePreloadingStrategyService } from '../../selective-preloading-strategy.service';
@Component({
selector: 'app-admin-dashboard',
templateUrl: './admin-dashboard.component.html',
styleUrls: ['./admin-dashboard.component.css']
})
export class AdminDashboardComponent implements OnInit {
sessionId!: Observable<string>;
token!: Observable<string>;
modules: string[] = [];
constructor(
private route: ActivatedRoute,
preloadStrategy: SelectivePreloadingStrategyService
) {
this.modules = preloadStrategy.preloadedModules;
}
ngOnInit() {
// Capture the session ID if available
this.sessionId = this.route
.queryParamMap
.pipe(map(params => params.get('session_id') || 'None'));
// Capture the fragment if available
this.token = this.route
.fragment
.pipe(map(fragment => fragment || 'None'));
}
}
一旦应用加载完了初始路由,CrisisCenterModule
也被预加载了。通过 Admin
特性区中的记录就可以验证它,“Preloaded Modules”中列出了 crisis-center
。它也被记录到了浏览器的控制台。
你已经设置好了路由,并且用命令式和声明式的方式导航到了很多不同的路由。但是,任何应用的需求都会随着时间而改变。你把链接 /heroes
和 hero/:id
指向了 HeroListComponent
和 HeroDetailComponent
组件。如果有这样一个需求,要把链接 heroes
变成 superheroes
,你可能仍然希望以前的 URL 能正常导航。但你也不想在应用中找到并修改每一个链接,这时候,重定向就可以省去这些琐碎的重构工作。
本节将指导你将 Hero
路由迁移到新的 URL。在导航之前,Router
会检查路由配置中的重定向语句,以便将来按需触发重定向。要支持这种修改,你就要在 heroes-routing.module
文件中把老的路由重定向到新的路由。
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HeroListComponent } from './hero-list/hero-list.component';
import { HeroDetailComponent } from './hero-detail/hero-detail.component';
const heroesRoutes: Routes = [
{ path: 'heroes', redirectTo: '/superheroes' },
{ path: 'hero/:id', redirectTo: '/superhero/:id' },
{ path: 'superheroes', component: HeroListComponent, data: { animation: 'heroes' } },
{ path: 'superhero/:id', component: HeroDetailComponent, data: { animation: 'hero' } }
];
@NgModule({
imports: [
RouterModule.forChild(heroesRoutes)
],
exports: [
RouterModule
]
})
export class HeroesRoutingModule { }
注意,这里有两种类型的重定向。第一种是不带参数的从 /heroes
重定向到 /superheroes
。这是一种非常直观的重定向。第二种是从 /hero/:id
重定向到 /superhero/:id
,它还要包含一个 :id
路由参数。路由器重定向时使用强大的模式匹配功能,这样,路由器就会检查 URL,并且把 path
中带的路由参数替换成相应的目标形式。以前,你导航到形如 /hero/15
的 URL 时,带了一个路由参数 id
,它的值是 15
。
在重定向的时候,路由器还支持查询参数和片段(fragment)。
redirectTo
属性中规定的查询参数和片段。目前,空路径被重定向到了 /heroes
,它又被重定向到了 /superheroes
。这样不行,因为 Router
在每一层的路由配置中只会处理一次重定向。这样可以防止出现无限循环的重定向。
所以,你要在 app-routing.module.ts
中修改空路径路由,让它重定向到 /superheroes
。
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { ComposeMessageComponent } from './compose-message/compose-message.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
import { AuthGuard } from './auth/auth.guard';
import { SelectivePreloadingStrategyService } from './selective-preloading-strategy.service';
const appRoutes: Routes = [
{
path: 'compose',
component: ComposeMessageComponent,
outlet: 'popup'
},
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
canLoad: [AuthGuard]
},
{
path: 'crisis-center',
loadChildren: () => import('./crisis-center/crisis-center.module').then(m => m.CrisisCenterModule),
data: { preload: true }
},
{ path: '', redirectTo: '/superheroes', pathMatch: 'full' },
{ path: '**', component: PageNotFoundComponent }
];
@NgModule({
imports: [
RouterModule.forRoot(
appRoutes,
{
enableTracing: false, // <-- debugging purposes only
preloadingStrategy: SelectivePreloadingStrategyService,
}
)
],
exports: [
RouterModule
]
})
export class AppRoutingModule { }
由于 routerLink
与路由配置无关,所以你要修改相关的路由链接,以便在新的路由激活时,它们也能保持激活状态。还要修改 app.component.ts
模板中的 /heroes
这个 routerLink
。
<div class="wrapper">
<h1 class="title">Angular Router</h1>
<nav>
<a routerLink="/crisis-center" routerLinkActive="active" ariaCurrentWhenActive="page">Crisis Center</a>
<a routerLink="/superheroes" routerLinkActive="active" ariaCurrentWhenActive="page">Heroes</a>
<a routerLink="/admin" routerLinkActive="active" ariaCurrentWhenActive="page">Admin</a>
<a routerLink="/login" routerLinkActive="active" ariaCurrentWhenActive="page">Login</a>
<a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a>
</nav>
<div [@routeAnimation]="getRouteAnimationData()">
<router-outlet></router-outlet>
</div>
<router-outlet name="popup"></router-outlet>
</div>
修改 hero-detail.component.ts
中的 goToHeroes()
方法,使用可选的路由参数导航回 /superheroes
。
gotoHeroes(hero: Hero) {
const heroId = hero ? hero.id : null;
// Pass along the hero id if available
// so that the HeroList component can select that hero.
// Include a junk 'foo' property for fun.
this.router.navigate(['/superheroes', { id: heroId, foo: 'foo' }]);
}
当这些重定向设置好之后,所有以前的路由都指向了它们的新目标,并且每个 URL 也仍然能正常工作。
要确定你的路由是否真的按照正确的顺序执行的,你可以审查路由器的配置。
可以通过注入路由器并在控制台中记录其 config
属性来实现。比如,把 AppModule
修改为这样,并在浏览器的控制台窗口中查看最终的路由配置。
export class AppModule {
// Diagnostic only: inspect router configuration
constructor(router: Router) {
// Use a custom replacer to display function names in the route configs
const replacer = (key, value) => (typeof value === 'function') ? value.name : value;
console.log('Routes: ', JSON.stringify(router.config, replacer, 2));
}
}