完成上面的里程碑后,應用程序很自然地長大了。在某一個時間點,你將達到一個頂點,應用將會需要過多的時間來加載。
為了解決這個問題,請使用異步路由,它會根據(jù)請求來惰性加載某些特性模塊。惰性加載有很多好處。
你可以只在用戶請求時才加載某些特性區(qū)。
對于那些只訪問應用程序某些區(qū)域的用戶,這樣能加快加載速度。
你可以持續(xù)擴充惰性加載特性區(qū)的功能,而不用增加初始加載的包體積。
你已經(jīng)完成了一部分。通過把應用組織成一些模塊:AppModule
、HeroesModule
、AdminModule
和 CrisisCenterModule
, 你已經(jīng)有了可用于實現(xiàn)惰性加載的候選者。
有些模塊(比如 AppModule
)必須在啟動時加載,但其它的都可以而且應該惰性加載。 比如 AdminModule 就只有少數(shù)已認證的用戶才需要它,所以你應該只有在正確的人請求它時才加載。
把 "admin-routing.module.ts" 中的 admin
路徑從 'admin'
改為空路徑 ''
。
可以用空路徑路由來對路由進行分組,而不用往 URL
中添加額外的路徑片段。 用戶仍舊訪問 "/admin",并且 AdminComponent
仍然作為用來包含子路由的路由組件。
打開 AppRoutingModule
,并把一個新的 admin
路由添加到它的 appRoutes
數(shù)組中。
給它一個 loadChildren
屬性(替換掉 children
屬性)。 loadChildren
屬性接收一個函數(shù),該函數(shù)使用瀏覽器內(nèi)置的動態(tài)導入語法 import('...')
來惰性加載代碼,并返回一個承諾(Promise
)。 其路徑是 AdminModule
的位置(相對于應用的根目錄)。 當代碼請求并加載完畢后,這個 Promise
就會解析成一個包含 NgModule
的對象,也就是 AdminModule
。
Path:"app-routing.module.ts (load children)" 。
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
},
注:
- 當使用絕對路徑時,
NgModule
的文件位置必須以 "src/app" 開頭,以便正確解析。對于自定義的 使用絕對路徑的路徑映射表,你必須在項目的 "tsconfig.json" 中必須配置好baseUrl
和paths
屬性。
當路由器導航到這個路由時,它會用 loadChildren
字符串來動態(tài)加載 AdminModule
,然后把 AdminModule
添加到當前的路由配置中, 最后,它把所請求的路由加載到目標 admin
組件中。
惰性加載和重新配置工作只會發(fā)生一次,也就是在該路由首次被請求時。在后續(xù)的請求中,該模塊和路由都是立即可用的。
Angular 提供一個內(nèi)置模塊加載器,支持SystemJS 來異步加載模塊。如果你使用其它捆綁工具比如 Webpack,則使用 Webpack 的機制來異步加載模塊。
最后一步是把管理特性區(qū)從主應用中完全分離開。 根模塊 AppModule
既不能加載也不能引用 AdminModule
及其文件。
在 "app.module.ts" 中,從頂部移除 AdminModule
的導入語句,并且從 NgModule
的 imports
數(shù)組中移除 AdminModule
。
你已經(jīng)使用 CanActivate
保護 AdminModule
了,它會阻止未授權(quán)用戶訪問管理特性區(qū)。如果用戶未登錄,它就會跳轉(zhuǎn)到登錄頁。
但是路由器仍然會加載 AdminModule
—— 即使用戶無法訪問它的任何一個組件。 理想的方式是,只有在用戶已登錄的情況下你才加載 AdminModule
。
添加一個 CanLoad
守衛(wèi),它只在用戶已登錄并且嘗試訪問管理特性區(qū)的時候,才加載 AdminModule
一次。
現(xiàn)有的 AuthGuard
的 checkLogin()
方法中已經(jīng)有了支持 CanLoad
守衛(wèi)的基礎邏輯。
打開 "auth.guard.ts",從 @angular/router
中導入 CanLoad
接口。 把它添加到 AuthGuard
類的 implements
列表中。 然后實現(xiàn) canLoad
,代碼如下:
Path:"src/app/auth/auth.guard.ts (CanLoad guard)" 。
canLoad(route: Route): boolean {
let url = `/${route.path}`;
return this.checkLogin(url);
}
路由器會把 canLoad()
方法的 route
參數(shù)設置為準備訪問的目標 URL。 如果用戶已經(jīng)登錄了,checkLogin()
方法就會重定向到那個 URL
。
現(xiàn)在,把 AuthGuard
導入到 AppRoutingModule
中,并把 AuthGuard
添加到 admin
路由的 canLoad
數(shù)組中。 完整的 admin
路由是這樣的:
Path:"app-routing.module.ts (lazy admin route)" 。
{
path: 'admin',
loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
canLoad: [AuthGuard]
},
除了按需加載模塊外,還可以通過預加載方式異步加載模塊。
當應用啟動時,AppModule
被急性加載,這意味著它會立即加載。而 AdminModule
只在用戶點擊鏈接時加載,這叫做惰性加載。
預加載允許你在后臺加載模塊,以便當用戶激活某個特定的路由時,就可以渲染這些數(shù)據(jù)了。 考慮一下危機中心。 它不是用戶看到的第一個視圖。 默認情況下,英雄列表才是第一個視圖。為了獲得最小的初始有效負載和最快的啟動時間,你應該急性加載 AppModule
和 HeroesModule
。
你可以惰性加載危機中心。 但是,你幾乎可以肯定用戶會在啟動應用之后的幾分鐘內(nèi)訪問危機中心。 理想情況下,應用啟動時應該只加載 AppModule
和 HeroesModule
,然后幾乎立即開始后臺加載 CrisisCenterModule
。 在用戶瀏覽到危機中心之前,該模塊應該已經(jīng)加載完畢,可供訪問了。
在每次成功的導航后,路由器會在自己的配置中查找尚未加載并且可以預加載的模塊。 是否加載某個模塊,以及要加載哪些模塊,取決于預加載策略。
Router
提供了兩種預加載策略:
路由器或者完全不預加載或者預加載每個惰性加載模塊。 路由器還支持自定義預加載策略,以便完全控制要預加載哪些模塊以及何時加載。
本節(jié)將指導你把 CrisisCenterModule
改成惰性加載的,并使用 PreloadAllModules
策略來預加載所有惰性加載模塊。
修改路由配置,來惰性加載 CrisisCenterModule
。修改的步驟和配置惰性加載 AdminModule 時一樣。
CrisisCenterRoutingModule
中的路徑從 crisis-center
改為空字符串。AppRoutingModule
中添加一個 crisis-center
路由。loadChildren
字符串來加載 CrisisCenterModule
。CrisisCenterModule
的引用。下面是打開預加載之前的模塊修改版:
import { NgModule } from '@angular/core';
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';
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 { }
你可以現(xiàn)在嘗試它,并確認在點擊了 “Crisis Center” 按鈕之后加載了 CrisisCenterModule
。
要為所有惰性加載模塊啟用預加載功能,請從 Angular 的路由模塊中導入 PreloadAllModules
。
RouterModule.forRoot()
方法的第二個參數(shù)接受一個附加配置選項對象。 preloadingStrategy
就是其中之一。 把 PreloadAllModules
添加到 forRoot()
調(diào)用中:
Path:"src/app/app-routing.module.ts (preload all)" 。
RouterModule.forRoot(
appRoutes,
{
enableTracing: true, // <-- debugging purposes only
preloadingStrategy: PreloadAllModules
}
)
這項配置會讓 Router
預加載器立即加載所有惰性加載路由(帶 loadChildren
屬性的路由)。
當訪問 "http://localhost:4200 時,/heroes" 路由立即隨之啟動,并且路由器在加載了 HeroesModule
之后立即開始加載 CrisisCenterModule
。
目前,AdminModule
并沒有預加載,因為 CanLoad
阻塞了它。
CanLoad 會阻塞預加載
PreloadAllModules
策略不會加載被CanLoad
守衛(wèi)所保護的特性區(qū)。
幾步之前,你剛剛給 AdminModule
中的路由添加了 CanLoad
守衛(wèi),以阻塞加載那個模塊,直到用戶認證結(jié)束。 CanLoad
守衛(wèi)的優(yōu)先級高于預加載策略。
如果你要加載一個模塊并且保護它防止未授權(quán)訪問,請移除 CanLoad
守衛(wèi),只單獨依賴CanActivate
守衛(wèi)。
在很多場景下,預加載的每個惰性加載模塊都能正常工作。但是,考慮到低帶寬和用戶指標等因素,可以為特定的特性模塊使用自定義預加載策略。
本節(jié)將指導你添加一個自定義策略,它只預加載 data.preload
標志為 true
路由。回想一下,你可以在路由的 data
屬性中添加任何東西。
在 AppRoutingModule
的 crisis-center
路由中設置 data.preload
標志。
Path:"src/app/app-routing.module.ts (route 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
使用下列內(nèi)容替換 "selective-preloading-strategy.service.ts":
Path:"src/app/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 && route.data['preload']) {
// 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
實現(xiàn)了 PreloadingStrategy
,它有一個方法 preload()
。
路由器會用兩個參數(shù)來調(diào)用 preload()
方法:
loader
)函數(shù),它能異步加載帶路由的模塊。
preload
的實現(xiàn)要返回一個 Observable
。 如果該路由應該預加載,它就會返回調(diào)用加載器函數(shù)所返回的 Observable
。 如果該路由不應該預加載,它就返回一個 null
值的 Observable
對象。
在這個例子中,如果路由的 data.preload
標志是真值,則 preload()
方法會加載該路由。
它的副作用是 SelectivePreloadingStrategyService
會把所選路由的 path
記錄在它的公共數(shù)組 preloadedModules
中。
很快,你就會擴展 AdminDashboardComponent
來注入該服務,并且顯示它的 preloadedModules
數(shù)組。
但是首先,要對 AppRoutingModule
做少量修改。
SelectivePreloadingStrategyService
導入到 AppRoutingModule
中。PreloadAllModules
策略替換成對 forRoot()
的調(diào)用,并且傳入這個 SelectivePreloadingStrategyService
。SelectivePreloadingStrategyService
策略添加到 AppRoutingModule
的 providers
數(shù)組中,以便它可以注入到應用中的任何地方。
現(xiàn)在,編輯 AdminDashboardComponent
以顯示這些預加載路由的日志。
導入 SelectivePreloadingStrategyService
(它是一個服務)。
把它注入到儀表盤的構(gòu)造函數(shù)中。
修改模板來顯示這個策略服務的 preloadedModules
數(shù)組。
現(xiàn)在文件如下:
Path:"src/app/admin/admin-dashboard/admin-dashboard.component.ts (preloaded modules)" 。
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
特性區(qū)中的記錄就可以驗證它,“Preloaded Modules”中列出了 crisis-center
。 它也被記錄到了瀏覽器的控制臺。
你已經(jīng)設置好了路由,并且用命令式和聲明式的方式導航到了很多不同的路由。但是,任何應用的需求都會隨著時間而改變。 你把鏈接 "/heroes" 和 "hero/:id" 指向了 HeroListComponent
和 HeroDetailComponent
組件。 如果有這樣一個需求,要把鏈接 "heroes" 變成 "superheroes",你可能仍然希望以前的 URL
能正常導航。 但你也不想在應用中找到并修改每一個鏈接,這時候,重定向就可以省去這些瑣碎的重構(gòu)工作。
把 /heroes 改為 /superheroes
本節(jié)將指導你將 Hero
路由遷移到新的 URL。在導航之前,Router
會檢查路由配置中的重定向語句,以便將來按需觸發(fā)重定向。要支持這種修改,你就要在 "heroes-routing.module" 文件中把老的路由重定向到新的路由。
Path:"src/app/heroes/heroes-routing.module.ts (heroes redirects)" 。
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 { }
注意,這里有兩種類型的重定向。第一種是不帶參數(shù)的從 "/heroes" 重定向到 "/superheroes"。這是一種非常直觀的重定向。第二種是從 "/hero/:id" 重定向到 "/superhero/:id",它還要包含一個 :id
路由參數(shù)。 路由器重定向時使用強大的模式匹配功能,這樣,路由器就會檢查 URL
,并且把 path
中帶的路由參數(shù)替換成相應的目標形式。以前,你導航到形如 "/hero/15" 的 URL
時,帶了一個路由參數(shù) id
,它的值是 15。
在重定向的時候,路由器還支持查詢參數(shù)和片段(
fragment
)。
- 當使用絕對地址重定向時,路由器將會使用路由配置的 `redirectTo` 屬性中規(guī)定的查詢參數(shù)和片段。
- 當使用相對地址重定向時,路由器將會使用源地址(跳轉(zhuǎn)前的地址)中的查詢參數(shù)和片段。
目前,空路徑被重定向到了 "/heroes",它又被重定向到了 "/superheroes"。這樣不行,因為 Router
在每一層的路由配置中只會處理一次重定向。這樣可以防止出現(xiàn)無限循環(huán)的重定向。
所以,你要在 "app-routing.module.ts" 中修改空路徑路由,讓它重定向到 "/superheroes"。
Path:"src/app/app-routing.module.ts (superheroes redirect)" 。
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
與路由配置無關(guān),所以你要修改相關(guān)的路由鏈接,以便在新的路由激活時,它們也能保持激活狀態(tài)。還要修改 "app.component.ts" 模板中的 "/heroes" 這個 routerLink
。
Path:"src/app/app.component.html (superheroes active routerLink))" 。
<h1 class="title">Angular Router</h1>
<nav>
<a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a>
<a routerLink="/superheroes" routerLinkActive="active">Heroes</a>
<a routerLink="/admin" routerLinkActive="active">Admin</a>
<a routerLink="/login" routerLinkActive="active">Login</a>
<a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a>
</nav>
<div [@routeAnimation]="getAnimationData(routerOutlet)">
<router-outlet #routerOutlet="outlet"></router-outlet>
</div>
<router-outlet name="popup"></router-outlet>
修改 "hero-detail.component.ts" 中的 goToHeroes()
方法,使用可選的路由參數(shù)導航回 "/superheroes"。
Path:"src/app/heroes/hero-detail/hero-detail.component.ts (goToHeroes)" 。
gotoHeroes(hero: Hero) {
let 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
也仍然能正常工作。
要確定你的路由是否真的按照正確的順序執(zhí)行的,你可以審查路由器的配置。
可以通過注入路由器并在控制臺中記錄其 config
屬性來實現(xiàn)。 例如,把 AppModule
修改為這樣,并在瀏覽器的控制臺窗口中查看最終的路由配置。
Path:"src/app/app.module.ts (inspect the router config)" 。
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));
}
}
對這個已完成的路由器應用,參見 下載范例 的最終代碼。
更多建議: