本節(jié)涵蓋了以下內(nèi)容:
這個(gè)示例應(yīng)用在“英雄指南”教程的“服務(wù)”部分重新創(chuàng)建了英雄特性區(qū),并復(fù)用了 Tour of Heroes: Services example code 中的大部分代碼。
典型的應(yīng)用具有多個(gè)特性區(qū),每個(gè)特性區(qū)都專注于特定的業(yè)務(wù)用途并擁有自己的文件夾。
該部分將向你展示如何將應(yīng)用重構(gòu)為不同的特性模塊、將它們導(dǎo)入到主模塊中,并在它們之間導(dǎo)航。
遵循下列步驟:
HeroesModule
,并把它注冊到根模塊 AppModule
中。 ng generate module heroes/heroes --module app --flat --routing
<h2>
加文字,改成 <h2>HEROES</h2>
。<app-hero-detail>
組件。HeroListComponent
。selector
改為 app-hero-list
。注:
- 對于路由組件來說,這些選擇器不是必須的,因?yàn)檫@些組件是在渲染頁面時(shí)動(dòng)態(tài)插入的,不過選擇器對于在 HTML 元素樹中標(biāo)記和選中它們是很有用的。
接下來,更新 HeroesModule
的元數(shù)據(jù)。
HeroDetailComponent
和 HeroListComponent
,并添加到 HeroesModule
模塊的 declarations
數(shù)組中。Path:"src/app/heroes/heroes.module.ts" 。
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { HeroListComponent } from './hero-list/hero-list.component';
import { HeroDetailComponent } from './hero-detail/hero-detail.component';
import { HeroesRoutingModule } from './heroes-routing.module';
@NgModule({
imports: [
CommonModule,
FormsModule,
HeroesRoutingModule
],
declarations: [
HeroListComponent,
HeroDetailComponent
]
})
export class HeroesModule {}
英雄管理部分的文件結(jié)構(gòu)如下:
英雄特性區(qū)中有兩個(gè)相互協(xié)作的組件:英雄列表和英雄詳情。當(dāng)你導(dǎo)航到列表視圖時(shí),它會(huì)獲取英雄列表并顯示出來。當(dāng)你點(diǎn)擊一個(gè)英雄時(shí),詳細(xì)視圖就會(huì)顯示那個(gè)特定的英雄。
通過把所選英雄的 id
編碼進(jìn)路由的 URL
中,就能告訴詳情視圖該顯示哪個(gè)英雄。
從新位置 "src/app/heroes/" 目錄中導(dǎo)入英雄相關(guān)的組件,并定義兩個(gè)“英雄管理”路由。
現(xiàn)在,你有了 Heroes
模塊的路由,還得在 RouterModule
中把它們注冊給路由器,和 AppRoutingModule
中的做法幾乎完全一樣,只有一項(xiàng)重要的差別。
在 AppRoutingModule
中,你使用了靜態(tài)的 RouterModule.forRoot()
方法來注冊路由和全應(yīng)用級服務(wù)提供者。在特性模塊中你要改用 forChild()
靜態(tài)方法。
只在根模塊
AppRoutingModule
中調(diào)用RouterModule.forRoot()
(如果在AppModule
中注冊應(yīng)用的頂層路由,那就在AppModule
中調(diào)用)。 在其它模塊中,你就必須調(diào)用RouterModule.forChild
方法來注冊附屬路由。
修改后的 HeroesRoutingModule
是這樣的:
Path:"src/app/heroes/heroes-routing.module.ts" 。
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', component: HeroListComponent },
{ path: 'hero/:id', component: HeroDetailComponent }
];
@NgModule({
imports: [
RouterModule.forChild(heroesRoutes)
],
exports: [
RouterModule
]
})
export class HeroesRoutingModule { }
考慮為每個(gè)特性模塊提供自己的路由配置文件。雖然特性路由目前還很少,但即使在小型應(yīng)用中,路由也會(huì)變得越來越復(fù)雜。
英雄類的路由目前定義在兩個(gè)地方:HeroesRoutingModule
中(并最終給 HeroesModule
)和 AppRoutingModule
中。
由特性模塊提供的路由會(huì)被路由器再組合上它們所導(dǎo)入的模塊的路由。 這讓你可以繼續(xù)定義特性路由模塊中的路由,而不用修改主路由配置。
移除 HeroListComponent
的導(dǎo)入和來自 "app-routing.module.ts" 中的 /heroes
路由。
保留默認(rèn)路由和通配符路由,因?yàn)檫@些路由仍然要在應(yīng)用的頂層使用。
Path:"src/app/app-routing.module.ts (v2)" 。
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CrisisListComponent } from './crisis-list/crisis-list.component';
// import { HeroListComponent } from './hero-list/hero-list.component'; // <-- delete this line
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
const appRoutes: Routes = [
{ path: 'crisis-center', component: CrisisListComponent },
// { path: 'heroes', component: HeroListComponent }, // <-- delete this line
{ path: '', redirectTo: '/heroes', pathMatch: 'full' },
{ path: '**', component: PageNotFoundComponent }
];
@NgModule({
imports: [
RouterModule.forRoot(
appRoutes,
{ enableTracing: true } // <-- debugging purposes only
)
],
exports: [
RouterModule
]
})
export class AppRoutingModule {}
因?yàn)?HeroesModule
現(xiàn)在提供了 HeroListComponent
,所以把它從 AppModule
的 declarations
數(shù)組中移除?,F(xiàn)在你已經(jīng)有了一個(gè)單獨(dú)的 HeroesModule
,你可以用更多的組件和不同的路由來演進(jìn)英雄特性區(qū)。
經(jīng)過這些步驟,AppModule
變成了這樣:
Path:"src/app/app.module.ts" 。
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { HeroesModule } from './heroes/heroes.module';
import { CrisisListComponent } from './crisis-list/crisis-list.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
@NgModule({
imports: [
BrowserModule,
FormsModule,
HeroesModule,
AppRoutingModule
],
declarations: [
AppComponent,
CrisisListComponent,
PageNotFoundComponent
],
bootstrap: [ AppComponent ]
})
export class AppModule { }
請注意該模塊的 imports
數(shù)組,AppRoutingModule
是最后一個(gè),并且位于 HeroesModule
之后。
Path:"src/app/app.module.ts (module-imports)" 。
imports: [
BrowserModule,
FormsModule,
HeroesModule,
AppRoutingModule
],
路由配置的順序很重要,因?yàn)槁酚善鲿?huì)接受第一個(gè)匹配上導(dǎo)航所要求的路徑的那個(gè)路由。
當(dāng)所有路由都在同一個(gè) AppRoutingModule
時(shí),你要把默認(rèn)路由和通配符路由放在最后(這里是在 /heroes
路由后面), 這樣路由器才有機(jī)會(huì)匹配到 /heroes
路由,否則它就會(huì)先遇到并匹配上該通配符路由,并導(dǎo)航到“頁面未找到”路由。
每個(gè)路由模塊都會(huì)根據(jù)導(dǎo)入的順序把自己的路由配置追加進(jìn)去。 如果你先列出了 AppRoutingModule
,那么通配符路由就會(huì)被注冊在“英雄管理”路由之前。 通配符路由(它匹配任意URL
)將會(huì)攔截住每一個(gè)到“英雄管理”路由的導(dǎo)航,因此事實(shí)上屏蔽了所有“英雄管理”路由。
反轉(zhuǎn)路由模塊的導(dǎo)入順序,就會(huì)看到當(dāng)點(diǎn)擊英雄相關(guān)的鏈接時(shí)被導(dǎo)向了“頁面未找到”路由。
回到 HeroesRoutingModule
并再次檢查這些路由定義。 HeroDetailComponent
路由的路徑中帶有 :id
令牌。
Path:"src/app/heroes/heroes-routing.module.ts (excerpt)" 。
{ path: 'hero/:id', component: HeroDetailComponent }
:id
令牌會(huì)為路由參數(shù)在路徑中創(chuàng)建一個(gè)“空位”。在這里,這種配置會(huì)讓路由器把英雄的 id
插入到那個(gè)“空位”中。
如果要告訴路由器導(dǎo)航到詳情組件,并讓它顯示 “Magneta”,你會(huì)期望這個(gè)英雄的 id
像這樣顯示在瀏覽器的 URL
中:
localhost:4200/hero/15
如果用戶把此 URL
輸入到瀏覽器的地址欄中,路由器就會(huì)識(shí)別出這種模式,同樣進(jìn)入 “Magneta” 的詳情視圖。
路由參數(shù):必須的還是可選的?
&在這個(gè)場景下,把路由參數(shù)的令牌 :id 嵌入到路由定義的
path
中是一個(gè)好主意,因?yàn)閷τ?HeroDetailComponent
來說id
是必須的, 而且路徑中的值 15 已經(jīng)足夠把到 “Magneta” 的路由和到其它英雄的路由明確區(qū)分開。
然后導(dǎo)航到 HeroDetailComponent
組件。在那里,你期望看到所選英雄的詳情,這需要兩部分信息:導(dǎo)航目標(biāo)和該英雄的 id
。
因此,這個(gè)鏈接參數(shù)數(shù)組中有兩個(gè)條目:路由的路徑和一個(gè)用來指定所選英雄 id
的路由參數(shù)。
Path:"src/app/heroes/hero-list/hero-list.component.html (link-parameters-array)" 。
<a [routerLink]="['/hero', hero.id]">
路由器從該數(shù)組中組合出了目標(biāo) "URL: localhost:3000/hero/15"。
路由器從 URL
中解析出路由參數(shù)(id:15),并通過 ActivatedRoute
服務(wù)來把它提供給 HeroDetailComponent
組件。
從路由器(router
)包中導(dǎo)入 Router
、ActivatedRoute
和 Params
類。
Path:"src/app/heroes/hero-detail/hero-detail.component.ts (activated route)" 。
import { Router, ActivatedRoute, ParamMap } from '@angular/router';
這里導(dǎo)入 switchMap
操作符是因?yàn)槟闵院髮?huì)處理路由參數(shù)的可觀察對象 Observable
。
Path:"src/app/heroes/hero-detail/hero-detail.component.ts (switchMap operator import)" 。
import { switchMap } from 'rxjs/operators';
把這些服務(wù)作為私有變量添加到構(gòu)造函數(shù)中,以便 Angular 注入它們(讓它們對組件可見)。
Path:"src/app/heroes/hero-detail/hero-detail.component.ts (constructor)" 。
constructor(
private route: ActivatedRoute,
private router: Router,
private service: HeroService
) {}
在 ngOnInit()
方法中,使用 ActivatedRoute
服務(wù)來檢索路由的參數(shù),從參數(shù)中提取出英雄的 id,并檢索要顯示的英雄。
Path:"src/app/heroes/hero-detail/hero-detail.component.ts (ngOnInit)" 。
ngOnInit() {
this.hero$ = this.route.paramMap.pipe(
switchMap((params: ParamMap) =>
this.service.getHero(params.get('id')))
);
}
當(dāng)這個(gè) map
發(fā)生變化時(shí),paramMap
會(huì)從更改后的參數(shù)中獲取 id
參數(shù)。
然后,讓 HeroService
去獲取具有該 id 的英雄,并返回 HeroService
請求的結(jié)果。
switchMap
操作符做了兩件事。它把 HeroService
返回的 Observable<Hero>
拍平,并取消以前的未完成請求。當(dāng) HeroService
仍在檢索舊的 id
時(shí),如果用戶使用新的 id
重新導(dǎo)航到這個(gè)路由,switchMap
會(huì)放棄那個(gè)舊請求,并返回新 id
的英雄。
AsyncPipe
處理這個(gè)可觀察的訂閱,而且該組件的 hero
屬性也會(huì)用檢索到的英雄(重新)進(jìn)行設(shè)置。
ParamMap API 的靈感來自于 URLSearchParams
接口。它提供了處理路由參數(shù)( paramMap
)和查詢參數(shù)( queryParamMap
)訪問的方法。
說明:如果參數(shù)名位于參數(shù)列表中,就返回 true
。
說明:如果這個(gè) map
中有參數(shù)名對應(yīng)的參數(shù)值(字符串),就返回它,否則返回 null
。如果參數(shù)值實(shí)際上是一個(gè)數(shù)組,就返回它的第一個(gè)元素。
說明:如果這個(gè) map
中有參數(shù)名對應(yīng)的值,就返回一個(gè)字符串?dāng)?shù)組,否則返回空數(shù)組。當(dāng)一個(gè)參數(shù)名可能對應(yīng)多個(gè)值的時(shí)候,請使用 getAll
。
說明:返回這個(gè) map
中的所有參數(shù)名組成的字符串?dāng)?shù)組。
在這個(gè)例子中,你接收了路由參數(shù)的 Observable
對象。 這種寫法暗示著這些路由參數(shù)在該組件的生存期內(nèi)可能會(huì)變化。
默認(rèn)情況下,如果它沒有訪問過其它組件就導(dǎo)航到了同一個(gè)組件實(shí)例,那么路由器傾向于復(fù)用組件實(shí)例。如果復(fù)用,這些參數(shù)可以變化。
假設(shè)父組件的導(dǎo)航欄有“前進(jìn)”和“后退”按鈕,用來輪流顯示英雄列表中中英雄的詳情。 每次點(diǎn)擊都會(huì)強(qiáng)制導(dǎo)航到帶前一個(gè)或后一個(gè) id
的 HeroDetailComponent
組件。
你肯定不希望路由器先從 DOM 中移除當(dāng)前的 HeroDetailComponent
實(shí)例,只是為了用下一個(gè) id
重新創(chuàng)建它,因?yàn)樗鼘⒅匦落秩疽晥D。為了更好的用戶體驗(yàn),路由器會(huì)復(fù)用同一個(gè)組件實(shí)例,而只是更新參數(shù)。
由于 ngOnInit()
在每個(gè)組件實(shí)例化時(shí)只會(huì)被調(diào)用一次,所以你可以使用 paramMap
可觀察對象來檢測路由參數(shù)在同一個(gè)實(shí)例中何時(shí)發(fā)生了變化。
當(dāng)在組件中訂閱一個(gè)可觀察對象時(shí),你通常總是要在組件銷毀時(shí)取消這個(gè)訂閱。
不過,
ActivatedRoute
中的可觀察對象是一個(gè)例外,因?yàn)?ActivatedRoute
及其可觀察對象與Router
本身是隔離的。Router
會(huì)在不再需要時(shí)銷毀這個(gè)路由組件,而注入進(jìn)去的ActivateRoute
也隨之銷毀了。
本應(yīng)用不需要復(fù)用 HeroDetailComponent
。 用戶總是會(huì)先返回英雄列表,再選擇另一位英雄。 所以,不存在從一個(gè)英雄詳情導(dǎo)航到另一個(gè)而不用經(jīng)過英雄列表的情況。 這意味著路由器每次都會(huì)創(chuàng)建一個(gè)全新的 HeroDetailComponent
實(shí)例。
假如你很確定這個(gè) HeroDetailComponent
實(shí)例永遠(yuǎn)不會(huì)被重用,你可以使用 snapshot
。
route.snapshot
提供了路由參數(shù)的初始值。 你可以通過它來直接訪問參數(shù),而不用訂閱或者添加 Observable
的操作符,代碼如下:
Path:"src/app/heroes/hero-detail/hero-detail.component.ts (ngOnInit snapshot)" 。
ngOnInit() {
let id = this.route.snapshot.paramMap.get('id');
this.hero$ = this.service.getHero(id);
}
用這種技術(shù),
snapshot
只會(huì)得到這些參數(shù)的初始值。如果路由器可能復(fù)用該組件,那么就該用paramMap
可觀察對象的方式。本教程的示例應(yīng)用中就用了paramMap
可觀察對象。
HeroDetailComponent
的 “Back” 按鈕使用了 gotoHeroes()
方法,該方法會(huì)強(qiáng)制導(dǎo)航回 HeroListComponent
。
路由的 navigate()
方法同樣接受一個(gè)單條目的鏈接參數(shù)數(shù)組,你也可以把它綁定到 [routerLink]
指令上。 它保存著到 HeroListComponent
組件的路徑:
Path:"src/app/heroes/hero-detail/hero-detail.component.ts (excerpt)"。
gotoHeroes() {
this.router.navigate(['/heroes']);
}
如果想導(dǎo)航到 HeroDetailComponent
以對 id
為 15 的英雄進(jìn)行查看并編輯,就要在路由的 URL
中使用路由參數(shù)來指定必要參數(shù)值。
localhost:4200/hero/15
你也能在路由請求中添加可選信息。 比如,當(dāng)從 "hero-detail.component.ts" 返回到列表時(shí),如果能自動(dòng)選中剛剛查看過的英雄就好了。
當(dāng)從 HeroDetailComponent 返回時(shí),你可以會(huì)通過把正在查看的英雄的 id 作為可選參數(shù)包含在 URL 中來實(shí)現(xiàn)這個(gè)特性。
可選信息還可以包含其它形式,例如:
name='wind_'
。after='12/31/2015' & before='1/1/2017'
- 沒有特定的順序 -
before='1/1/2017' & after='12/31/2015'
- 具有各種格式 -
during='currentYear'
。
由于這些參數(shù)不適合用作 URL
路徑,因此可以使用可選參數(shù)在導(dǎo)航過程中傳遞任意復(fù)雜的信息。可選參數(shù)不參與模式匹配,因此在表達(dá)上提供了巨大的靈活性。
和必要參數(shù)一樣,路由器也支持通過可選參數(shù)導(dǎo)航。 在你定義完必要參數(shù)之后,再通過一個(gè)獨(dú)立的對象來定義可選參數(shù)。
通常,對于必傳的值(比如用于區(qū)分兩個(gè)路由路徑的)使用必備參數(shù);當(dāng)這個(gè)值是可選的、復(fù)雜的或多值的時(shí),使用可選參數(shù)。
當(dāng)導(dǎo)航到 HeroDetailComponent
時(shí),你可以在路由參數(shù)中指定一個(gè)所要編輯的英雄 id
,只要把它作為鏈接參數(shù)數(shù)組中的第二個(gè)條目就可以了。
Path:"src/app/heroes/hero-list/hero-list.component.html (link-parameters-array)"。
<a [routerLink]="['/hero', hero.id]">
路由器在導(dǎo)航 URL
中內(nèi)嵌了 id
的值,這是因?yàn)槟惆阉靡粋€(gè) :id
占位符當(dāng)做路由參數(shù)定義在了路由的 path
中:
Path:"src/app/heroes/heroes-routing.module.ts (hero-detail-route)"。
{ path: 'hero/:id', component: HeroDetailComponent }
當(dāng)用戶點(diǎn)擊后退按鈕時(shí),HeroDetailComponent
構(gòu)造了另一個(gè)鏈接參數(shù)數(shù)組,可以用它導(dǎo)航回 HeroListComponent
。
Path:"src/app/heroes/hero-detail/hero-detail.component.ts (gotoHeroes)"。
gotoHeroes() {
this.router.navigate(['/heroes']);
}
該數(shù)組缺少一個(gè)路由參數(shù),這是因?yàn)橐郧澳悴恍枰?HeroListComponent
發(fā)送信息。
現(xiàn)在,使用導(dǎo)航請求發(fā)送當(dāng)前英雄的 id
,以便 HeroListComponent
在其列表中突出顯示該英雄。
傳送一個(gè)包含可選 id
參數(shù)的對象。 為了演示,這里還在對象中定義了一個(gè)沒用的額外參數(shù)(foo),HeroListComponent
應(yīng)該忽略它。 下面是修改過的導(dǎo)航語句:
Path:"src/app/heroes/hero-detail/hero-detail.component.ts (go to heroes)"。
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(['/heroes', { id: heroId, foo: 'foo' }]);
}
該應(yīng)用仍然能工作。點(diǎn)擊“back”按鈕返回英雄列表視圖。
注意瀏覽器的地址欄。
它應(yīng)該是這樣的,不過也取決于你在哪里運(yùn)行它:
localhost:4200/heroes;id=15;foo=foo
id
的值像這樣出現(xiàn)在 URL 中(;id=15;foo=foo),但不在 URL 的路徑部分。 “Heroes”路由的路徑部分并沒有定義 :id
。
可選的路由參數(shù)沒有使用?
和&
符號(hào)分隔,因?yàn)樗鼈儗⒂迷?URL 查詢字符串中。 它們是用;
分隔的。 這是矩陣 URL標(biāo)記法。
Matrix URL 寫法首次提出是在1996 提案中,提出者是 Web 的奠基人:Tim Berners-Lee。
雖然 Matrix 寫法未曾進(jìn)入過 HTML 標(biāo)準(zhǔn),但它是合法的。而且在瀏覽器的路由系統(tǒng)中,它作為從父路由和子路由中單獨(dú)隔離出參數(shù)的方式而廣受歡迎。Angular 的路由器正是這樣一個(gè)路由系統(tǒng),并支持跨瀏覽器的 Matrix 寫法。
開發(fā)到現(xiàn)在,英雄列表還沒有變化。沒有突出顯示的英雄行。
HeroListComponent
需要添加使用這些參數(shù)的代碼。
以前,當(dāng)從 HeroListComponent
導(dǎo)航到 HeroDetailComponent
時(shí),你通過 ActivatedRoute
服務(wù)訂閱了路由參數(shù)這個(gè) Observable
,并讓它能用在 HeroDetailComponent
中。 你把該服務(wù)注入到了 HeroDetailComponent
的構(gòu)造函數(shù)中。
這次,你要進(jìn)行反向?qū)Ш?,?HeroDetailComponent
到 HeroListComponent
。
首先,擴(kuò)展該路由的導(dǎo)入語句,以包含進(jìn) ActivatedRoute
服務(wù)的類;
Path:"src/app/heroes/hero-list/hero-list.component.ts (import)"。
import { ActivatedRoute } from '@angular/router';
導(dǎo)入 switchMap
操作符,在路由參數(shù)的 Observable
對象上執(zhí)行操作。
Path:"src/app/heroes/hero-list/hero-list.component.ts (rxjs imports)"。
import { Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators';
在 HeroListComponent
構(gòu)造函數(shù)中注入 ActivatedRoute
。
Path:"src/app/heroes/hero-list/hero-list.component.ts (constructor and ngOnInit)"。
export class HeroListComponent implements OnInit {
heroes$: Observable<Hero[]>;
selectedId: number;
constructor(
private service: HeroService,
private route: ActivatedRoute
) {}
ngOnInit() {
this.heroes$ = this.route.paramMap.pipe(
switchMap(params => {
// (+) before `params.get()` turns the string into a number
this.selectedId = +params.get('id');
return this.service.getHeroes();
})
);
}
}
ActivatedRoute.paramMap
屬性是一個(gè)路由參數(shù)的 Observable
。當(dāng)用戶導(dǎo)航到這個(gè)組件時(shí),paramMap
會(huì)發(fā)射一個(gè)新值,其中包含 id
。 在 ngOnInit()
中,你訂閱了這些值,設(shè)置到 selectedId
,并獲取英雄數(shù)據(jù)。
用 CSS 類綁定更新模板,把它綁定到 isSelected
方法上。 如果該方法返回 true
,此綁定就會(huì)添加 CSS 類 selected
,否則就移除它。 在 <li>
標(biāo)記中找到它,就像這樣:
Path:"src/app/heroes/hero-list/hero-list.component.html"。
<h2>HEROES</h2>
<ul class="heroes">
<li *ngFor="let hero of heroes$ | async"
[class.selected]="hero.id === selectedId">
<a [routerLink]="['/hero', hero.id]">
<span class="badge">{{ hero.id }}</span>{{ hero.name }}
</a>
</li>
</ul>
<button routerLink="/sidekicks">Go to sidekicks</button>
當(dāng)選中列表?xiàng)l目時(shí),要添加一些樣式。
Path:"src/app/heroes/hero-list/hero-list.component.css"。
.heroes li.selected {
background-color: #CFD8DC;
color: white;
}
.heroes li.selected:hover {
background-color: #BBD8DC;
}
當(dāng)用戶從英雄列表導(dǎo)航到英雄“Magneta”并返回時(shí),“Magneta”看起來是選中的:
這個(gè)可選的 foo
路由參數(shù)人畜無害,路由器會(huì)繼續(xù)忽略它。
在這一節(jié),你將為英雄詳情組件添加一些動(dòng)畫。
首先導(dǎo)入 BrowserAnimationsModule
,并添加到 imports
數(shù)組中:
Path:"src/app/app.module.ts (animations-module)"。
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@NgModule({
imports: [
BrowserAnimationsModule,
],
})
接下來,為指向 HeroListComponent
和 HeroDetailComponent
的路由定義添加一個(gè) data
對象。 轉(zhuǎn)場是基于 states
的,你將使用來自路由的 animation
數(shù)據(jù)為轉(zhuǎn)場提供一個(gè)有名字的動(dòng)畫 state
。
Path:"src/app/heroes/heroes-routing.module.ts (animation data)"。
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', component: HeroListComponent, data: { animation: 'heroes' } },
{ path: 'hero/:id', component: HeroDetailComponent, data: { animation: 'hero' } }
];
@NgModule({
imports: [
RouterModule.forChild(heroesRoutes)
],
exports: [
RouterModule
]
})
export class HeroesRoutingModule { }
在根目錄 "src/app/" 下創(chuàng)建一個(gè) "animations.ts"。內(nèi)容如下:
Path:"src/app/animations.ts (excerpt)" 。
import {
trigger, animateChild, group,
transition, animate, style, query
} from '@angular/animations';
// Routable animations
export const slideInAnimation =
trigger('routeAnimation', [
transition('heroes <=> hero', [
style({ position: 'relative' }),
query(':enter, :leave', [
style({
position: 'absolute',
top: 0,
left: 0,
width: '100%'
})
]),
query(':enter', [
style({ left: '-100%'})
]),
query(':leave', animateChild()),
group([
query(':leave', [
animate('300ms ease-out', style({ left: '100%'}))
]),
query(':enter', [
animate('300ms ease-out', style({ left: '0%'}))
])
]),
query(':enter', animateChild()),
])
]);
該文件做了如下工作:
slideInAnimation
的常量,并把它設(shè)置為一個(gè)名叫 routeAnimation
的動(dòng)畫觸發(fā)器。heroes
和 hero
路由之間來回切換時(shí),如果進(jìn)入(:enter
)應(yīng)用視圖則讓組件從屏幕的左側(cè)滑入,如果離開(:leave
)應(yīng)用視圖則讓組件從右側(cè)劃出。
回到 AppComponent
,從 @angular/router
包導(dǎo)入 RouterOutlet
,并從 "./animations.ts" 導(dǎo)入 slideInAnimation
。
為包含 slideInAnimation
的 @Component
元數(shù)據(jù)添加一個(gè) animations
數(shù)組。
Path:"src/app/app.component.ts (animations)" 。
import { RouterOutlet } from '@angular/router';
import { slideInAnimation } from './animations';
@Component({
selector: 'app-root',
templateUrl: 'app.component.html',
styleUrls: ['app.component.css'],
animations: [ slideInAnimation ]
})
要想使用路由動(dòng)畫,就要把 RouterOutlet
包裝到一個(gè)元素中。再把 @routeAnimation
觸發(fā)器綁定到該元素上。
為了把 @routeAnimation
轉(zhuǎn)場轉(zhuǎn)場到指定的狀態(tài),你需要從 ActivatedRoute
的 data
中提供它。 RouterOutlet
導(dǎo)出成了一個(gè)模板變量 outlet
,這樣你就可以綁定一個(gè)到路由出口的引用了。這個(gè)例子中使用了一個(gè) routerOutlet
變量。
Path:"src/app/app.component.html (router outlet)" 。
<h1>Angular Router</h1>
<nav>
<a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a>
<a routerLink="/heroes" routerLinkActive="active">Heroes</a>
</nav>
<div [@routeAnimation]="getAnimationData(routerOutlet)">
<router-outlet #routerOutlet="outlet"></router-outlet>
</div>
@routeAnimation
屬性使用所提供的 routerOutlet
引用來綁定到 getAnimationData()
,因此下一步就要在 AppComponent
中定義那個(gè)函數(shù)。getAnimationData
函數(shù)會(huì)根據(jù) ActivatedRoute
所提供的 data
對象返回動(dòng)畫的屬性。animation
屬性會(huì)根據(jù)你在 "animations.ts" 中定義 slideInAnimation()
時(shí)使用的 transition
名稱進(jìn)行匹配。
Path:"src/app/app.component.ts (router outlet)" 。
export class AppComponent {
getAnimationData(outlet: RouterOutlet) {
return outlet && outlet.activatedRouteData && outlet.activatedRouteData['animation'];
}
}
如果在兩個(gè)路由之間切換,導(dǎo)航進(jìn)來時(shí),HeroDetailComponent
和 HeroListComponent
會(huì)從左側(cè)滑入;導(dǎo)航離開時(shí)將會(huì)從右側(cè)劃出。
本節(jié)包括以下內(nèi)容:
AppModule
。做完這些修改之后,目錄結(jié)構(gòu)如下:
本節(jié)產(chǎn)生的文件列表:
import {
trigger, animateChild, group,
transition, animate, style, query
} from '@angular/animations';
// Routable animations
export const slideInAnimation =
trigger('routeAnimation', [
transition('heroes <=> hero', [
style({ position: 'relative' }),
query(':enter, :leave', [
style({
position: 'absolute',
top: 0,
left: 0,
width: '100%'
})
]),
query(':enter', [
style({ left: '-100%'})
]),
query(':leave', animateChild()),
group([
query(':leave', [
animate('300ms ease-out', style({ left: '100%'}))
]),
query(':enter', [
animate('300ms ease-out', style({ left: '0%'}))
])
]),
query(':enter', animateChild()),
])
]);
<h1>Angular Router</h1>
<nav>
<a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a>
<a routerLink="/heroes" routerLinkActive="active">Heroes</a>
</nav>
<div [@routeAnimation]="getAnimationData(routerOutlet)">
<router-outlet #routerOutlet="outlet"></router-outlet>
</div>
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { slideInAnimation } from './animations';
@Component({
selector: 'app-root',
templateUrl: 'app.component.html',
styleUrls: ['app.component.css'],
animations: [ slideInAnimation ]
})
export class AppComponent {
getAnimationData(outlet: RouterOutlet) {
return outlet && outlet.activatedRouteData && outlet.activatedRouteData['animation'];
}
}
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { HeroesModule } from './heroes/heroes.module';
import { CrisisListComponent } from './crisis-list/crisis-list.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
@NgModule({
imports: [
BrowserModule,
BrowserAnimationsModule,
FormsModule,
HeroesModule,
AppRoutingModule
],
declarations: [
AppComponent,
CrisisListComponent,
PageNotFoundComponent
],
bootstrap: [ AppComponent ]
})
export class AppModule { }
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CrisisListComponent } from './crisis-list/crisis-list.component';
/* . . . */
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
const appRoutes: Routes = [
{ path: 'crisis-center', component: CrisisListComponent },
/* . . . */
{ path: '', redirectTo: '/heroes', pathMatch: 'full' },
{ path: '**', component: PageNotFoundComponent }
];
@NgModule({
imports: [
RouterModule.forRoot(
appRoutes,
{ enableTracing: true } // <-- debugging purposes only
)
],
exports: [
RouterModule
]
})
export class AppRoutingModule {}
/* HeroListComponent's private CSS styles */
.heroes {
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 15em;
}
.heroes li {
position: relative;
cursor: pointer;
background-color: #EEE;
margin: .5em;
padding: .3em 0;
height: 1.6em;
border-radius: 4px;
}
.heroes li:hover {
color: #607D8B;
background-color: #DDD;
left: .1em;
}
.heroes a {
color: #888;
text-decoration: none;
position: relative;
display: block;
}
.heroes a:hover {
color:#607D8B;
}
.heroes .badge {
display: inline-block;
font-size: small;
color: white;
padding: 0.8em 0.7em 0 0.7em;
background-color: #607D8B;
line-height: 1em;
position: relative;
left: -1px;
top: -4px;
height: 1.8em;
min-width: 16px;
text-align: right;
margin-right: .8em;
border-radius: 4px 0 0 4px;
}
button {
background-color: #eee;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
cursor: hand;
font-family: Arial;
}
button:hover {
background-color: #cfd8dc;
}
button.delete {
position: relative;
left: 194px;
top: -32px;
background-color: gray !important;
color: white;
}
.heroes li.selected {
background-color: #CFD8DC;
color: white;
}
.heroes li.selected:hover {
background-color: #BBD8DC;
}
<h2>HEROES</h2>
<ul class="heroes">
<li *ngFor="let hero of heroes$ | async"
[class.selected]="hero.id === selectedId">
<a [routerLink]="['/hero', hero.id]">
<span class="badge">{{ hero.id }}</span>{{ hero.name }}
</a>
</li>
</ul>
<button routerLink="/sidekicks">Go to sidekicks</button>
// TODO: Feature Componetized like CrisisCenter
import { Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators';
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { HeroService } from '../hero.service';
import { Hero } from '../hero';
@Component({
selector: 'app-hero-list',
templateUrl: './hero-list.component.html',
styleUrls: ['./hero-list.component.css']
})
export class HeroListComponent implements OnInit {
heroes$: Observable<Hero[]>;
selectedId: number;
constructor(
private service: HeroService,
private route: ActivatedRoute
) {}
ngOnInit() {
this.heroes$ = this.route.paramMap.pipe(
switchMap(params => {
// (+) before `params.get()` turns the string into a number
this.selectedId = +params.get('id');
return this.service.getHeroes();
})
);
}
}
<h2>HEROES</h2>
<div *ngIf="hero$ | async as hero">
<h3>"{{ hero.name }}"</h3>
<div>
<label>Id: </label>{{ hero.id }}</div>
<div>
<label>Name: </label>
<input [(ngModel)]="hero.name" placeholder="name"/>
</div>
<p>
<button (click)="gotoHeroes(hero)">Back</button>
</p>
</div>
import { switchMap } from 'rxjs/operators';
import { Component, OnInit } from '@angular/core';
import { Router, ActivatedRoute, ParamMap } from '@angular/router';
import { Observable } from 'rxjs';
import { HeroService } from '../hero.service';
import { Hero } from '../hero';
@Component({
selector: 'app-hero-detail',
templateUrl: './hero-detail.component.html',
styleUrls: ['./hero-detail.component.css']
})
export class HeroDetailComponent implements OnInit {
hero$: Observable<Hero>;
constructor(
private route: ActivatedRoute,
private router: Router,
private service: HeroService
) {}
ngOnInit() {
this.hero$ = this.route.paramMap.pipe(
switchMap((params: ParamMap) =>
this.service.getHero(params.get('id')))
);
}
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(['/heroes', { id: heroId, foo: 'foo' }]);
}
}
/*
this.router.navigate(['/superheroes', { id: heroId, foo: 'foo' }]);
*/
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { map } from 'rxjs/operators';
import { Hero } from './hero';
import { HEROES } from './mock-heroes';
import { MessageService } from '../message.service';
@Injectable({
providedIn: 'root',
})
export class HeroService {
constructor(private messageService: MessageService) { }
getHeroes(): Observable<Hero[]> {
// TODO: send the message _after_ fetching the heroes
this.messageService.add('HeroService: fetched heroes');
return of(HEROES);
}
getHero(id: number | string) {
return this.getHeroes().pipe(
// (+) before `id` turns the string into a number
map((heroes: Hero[]) => heroes.find(hero => hero.id === +id))
);
}
}
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { HeroListComponent } from './hero-list/hero-list.component';
import { HeroDetailComponent } from './hero-detail/hero-detail.component';
import { HeroesRoutingModule } from './heroes-routing.module';
@NgModule({
imports: [
CommonModule,
FormsModule,
HeroesRoutingModule
],
declarations: [
HeroListComponent,
HeroDetailComponent
]
})
export class HeroesModule {}
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', component: HeroListComponent, data: { animation: 'heroes' } },
{ path: 'hero/:id', component: HeroDetailComponent, data: { animation: 'hero' } }
];
@NgModule({
imports: [
RouterModule.forChild(heroesRoutes)
],
exports: [
RouterModule
]
})
export class HeroesRoutingModule { }
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root',
})
export class MessageService {
messages: string[] = [];
add(message: string) {
this.messages.push(message);
}
clear() {
this.messages = [];
}
}
更多建議: