Route特性區(qū)

2020-07-07 16:16 更新

本節(jié)涵蓋了以下內(nèi)容:

  • 用模塊把應(yīng)用和路由組織為一些特性區(qū)。

  • 命令式的從一個(gè)組件導(dǎo)航到另一個(gè)

  • 通過路由傳遞必要信息和可選信息

這個(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)航。

添加英雄管理功能

遵循下列步驟:

  • 為了管理這些英雄,在 "heroes" 目錄下創(chuàng)建一個(gè)帶路由的 HeroesModule,并把它注冊(cè)到根模塊 AppModule 中。

    ng generate module heroes/heroes --module app --flat --routing

  • 把 "app" 下占位用的 "hero-list" 目錄移到 "heroes" 目錄中。

  • <h2> 加文字,改成 <h2>HEROES</h2>。

  • 刪除模板底部的 <app-hero-detail> 組件。

  • 把現(xiàn)場(chǎng)演練中 "heroes/heroes.component.css" 文件的內(nèi)容復(fù)制到 "hero-list.component.css" 文件中。

  • 把現(xiàn)場(chǎng)演練中 "heroes/heroes.component.ts" 文件的內(nèi)容復(fù)制到 "hero-list.component.ts" 文件中。

  • 把組件類名改為 HeroListComponent

  • selector 改為 app-hero-list。

注:

  • 對(duì)于路由組件來說,這些選擇器不是必須的,因?yàn)檫@些組件是在渲染頁面時(shí)動(dòng)態(tài)插入的,不過選擇器對(duì)于在 HTML 元素樹中標(biāo)記和選中它們是很有用的。

  • 把 "hero-detail" 目錄中的 "hero.ts"、"hero.service.ts" 和 "mock-heroes.ts" 文件復(fù)制到 "heroes" 子目錄下。

  • 把 "message.service.ts" 文件復(fù)制到 "src/app" 目錄下。

  • 在 "hero.service.ts" 文件中修改導(dǎo)入 "message.service" 的相對(duì)路徑。

接下來,更新 HeroesModule 的元數(shù)據(jù)。

  • 導(dǎo)入 HeroDetailComponentHeroListComponent,并添加到 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)如下:

  1. 英雄特性區(qū)的路由需求:

英雄特性區(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 中把它們注冊(cè)給路由器,和 AppRoutingModule 中的做法幾乎完全一樣,只有一項(xiàng)重要的差別。

AppRoutingModule 中,你使用了靜態(tài)的 RouterModule.forRoot() 方法來注冊(cè)路由和全應(yīng)用級(jí)服務(wù)提供者。在特性模塊中你要改用 forChild() 靜態(tài)方法。

只在根模塊 AppRoutingModule 中調(diào)用 RouterModule.forRoot()(如果在 AppModule 中注冊(cè)應(yīng)用的頂層路由,那就在 AppModule 中調(diào)用)。 在其它模塊中,你就必須調(diào)用 RouterModule.forChild 方法來注冊(cè)附屬路由。

修改后的 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ù)雜。

  1. 移除重復(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 {}

  1. 移除英雄列表的聲明。

因?yàn)?HeroesModule 現(xiàn)在提供了 HeroListComponent,所以把它從 AppModuledeclarations 數(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 { }

模塊導(dǎo)入順序

請(qǐng)注意該模塊的 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ì)被注冊(cè)在“英雄管理”路由之前。 通配符路由(它匹配任意URL)將會(huì)攔截住每一個(gè)到“英雄管理”路由的導(dǎo)航,因此事實(shí)上屏蔽了所有“英雄管理”路由。

反轉(zhuǎn)路由模塊的導(dǎo)入順序,就會(huì)看到當(dāng)點(diǎn)擊英雄相關(guān)的鏈接時(shí)被導(dǎo)向了“頁面未找到”路由。

路由參數(shù)

  1. 帶參數(shù)的路由定義。

回到 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è)場(chǎng)景下,把路由參數(shù)的令牌 :id 嵌入到路由定義的 path 中是一個(gè)好主意,因?yàn)閷?duì)于 HeroDetailComponent 來說 id 是必須的, 而且路徑中的值 15 已經(jīng)足夠把到 “Magneta” 的路由和到其它英雄的路由明確區(qū)分開。

  1. 在列表視圖中設(shè)置路由參數(shù)。

然后導(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 組件。

ActivatedRoute 實(shí)戰(zhàn)

從路由器(router)包中導(dǎo)入 Router、ActivatedRouteParams 類。

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ù)的可觀察對(duì)象 Observable。

Path:"src/app/heroes/hero-detail/hero-detail.component.ts (switchMap operator import)" 。

import { switchMap } from 'rxjs/operators';

把這些服務(wù)作為私有變量添加到構(gòu)造函數(shù)中,以便 Angular 注入它們(讓它們對(duì)組件可見)。

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 請(qǐng)求的結(jié)果。

switchMap 操作符做了兩件事。它把 HeroService 返回的 Observable<Hero> 拍平,并取消以前的未完成請(qǐng)求。當(dāng) HeroService 仍在檢索舊的 id 時(shí),如果用戶使用新的 id 重新導(dǎo)航到這個(gè)路由,switchMap 會(huì)放棄那個(gè)舊請(qǐng)求,并返回新 id 的英雄。

AsyncPipe 處理這個(gè)可觀察的訂閱,而且該組件的 hero 屬性也會(huì)用檢索到的英雄(重新)進(jìn)行設(shè)置。

  1. ParamMap API

ParamMap API 的靈感來自于 URLSearchParams 接口。它提供了處理路由參數(shù)( paramMap )和查詢參數(shù)( queryParamMap )訪問的方法。

  • 成員:has(name) 。

說明:如果參數(shù)名位于參數(shù)列表中,就返回 true

  • 成員:get(name) 。

說明:如果這個(gè) map 中有參數(shù)名對(duì)應(yīng)的參數(shù)值(字符串),就返回它,否則返回 null。如果參數(shù)值實(shí)際上是一個(gè)數(shù)組,就返回它的第一個(gè)元素。

  • 成員:getAll(name) 。

說明:如果這個(gè) map 中有參數(shù)名對(duì)應(yīng)的值,就返回一個(gè)字符串?dāng)?shù)組,否則返回空數(shù)組。當(dāng)一個(gè)參數(shù)名可能對(duì)應(yīng)多個(gè)值的時(shí)候,請(qǐng)使用 getAll

  • 成員:keys 。

說明:返回這個(gè) map 中的所有參數(shù)名組成的字符串?dāng)?shù)組。

  1. 參數(shù)的可觀察對(duì)象(Observable)與組件復(fù)用。

在這個(gè)例子中,你接收了路由參數(shù)的 Observable 對(duì)象。 這種寫法暗示著這些路由參數(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è) idHeroDetailComponent 組件。

你肯定不希望路由器先從 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 可觀察對(duì)象來檢測(cè)路由參數(shù)在同一個(gè)實(shí)例中何時(shí)發(fā)生了變化。

當(dāng)在組件中訂閱一個(gè)可觀察對(duì)象時(shí),你通??偸且诮M件銷毀時(shí)取消這個(gè)訂閱。

不過,ActivatedRoute 中的可觀察對(duì)象是一個(gè)例外,因?yàn)?ActivatedRoute 及其可觀察對(duì)象與 Router 本身是隔離的。 Router 會(huì)在不再需要時(shí)銷毀這個(gè)路由組件,而注入進(jìn)去的 ActivateRoute 也隨之銷毀了。

  1. snapshot:當(dāng)不需要 Observable 時(shí)的替代品。

本應(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 可觀察對(duì)象的方式。本教程的示例應(yīng)用中就用了 paramMap 可觀察對(duì)象。

導(dǎo)航回列表組件

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']);
}

  1. 路由參數(shù):必須還是可選?

如果想導(dǎo)航到 HeroDetailComponent 以對(duì) id 為 15 的英雄進(jìn)行查看并編輯,就要在路由的 URL 中使用路由參數(shù)來指定必要參數(shù)值。

    localhost:4200/hero/15

你也能在路由請(qǐng)求中添加可選信息。 比如,當(dāng)從 "hero-detail.component.ts" 返回到列表時(shí),如果能自動(dòng)選中剛剛查看過的英雄就好了。

當(dāng)從 HeroDetailComponent 返回時(shí),你可以會(huì)通過把正在查看的英雄的 id 作為可選參數(shù)包含在 URL 中來實(shí)現(xiàn)這個(gè)特性。

可選信息還可以包含其它形式,例如:

  • 結(jié)構(gòu)松散的搜索條件。比如 name='wind_'。

  • 多個(gè)值。比如 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ú)立的對(duì)象來定義可選參數(shù)。

通常,對(duì)于必傳的值(比如用于區(qū)分兩個(gè)路由路徑的)使用必備參數(shù);當(dāng)這個(gè)值是可選的、復(fù)雜的或多值的時(shí),使用可選參數(shù)。

  1. 英雄列表:選定一個(gè)英雄(也可不選)

當(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)航請(qǐng)求發(fā)送當(dāng)前英雄的 id,以便 HeroListComponent 在其列表中突出顯示該英雄。

傳送一個(gè)包含可選 id 參數(shù)的對(duì)象。 為了演示,這里還在對(duì)象中定義了一個(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 寫法。

ActivatedRoute 服務(wù)中的路由參數(shù)

開發(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ū)Ш?,?HeroDetailComponentHeroListComponent。

首先,擴(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 對(duì)象上執(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ù)忽略它。

添加路由動(dòng)畫

在這一節(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,
  ],
})

接下來,為指向 HeroListComponentHeroDetailComponent 的路由定義添加一個(gè) data 對(duì)象。 轉(zhuǎn)場(chǎng)是基于 states 的,你將使用來自路由的 animation 數(shù)據(jù)為轉(zhuǎn)場(chǎng)提供一個(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()),
    ])
  ]);

該文件做了如下工作:

  • 導(dǎo)入動(dòng)畫符號(hào)以構(gòu)建動(dòng)畫觸發(fā)器、控制狀態(tài)并管理狀態(tài)之間的過渡。

  • 導(dǎo)出了一個(gè)名叫 slideInAnimation 的常量,并把它設(shè)置為一個(gè)名叫 routeAnimation 的動(dòng)畫觸發(fā)器。

  • 定義一個(gè)轉(zhuǎn)場(chǎng)動(dòng)畫,當(dāng)在 heroeshero 路由之間來回切換時(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)場(chǎng)轉(zhuǎn)場(chǎng)到指定的狀態(tài),你需要從 ActivatedRoutedata 中提供它。 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 對(duì)象返回動(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í),HeroDetailComponentHeroListComponent 會(huì)從左側(cè)滑入;導(dǎo)航離開時(shí)將會(huì)從右側(cè)劃出。

小結(jié)

本節(jié)包括以下內(nèi)容:

  • 把應(yīng)用組織成特性區(qū)。

  • 命令式的從一個(gè)組件導(dǎo)航到另一個(gè)。

  • 通過路由參數(shù)傳遞信息,并在組件中訂閱它們。

  • 把這個(gè)特性分區(qū)模塊導(dǎo)入根模塊 AppModule。

  • 把動(dòng)畫應(yīng)用到路由組件上。

做完這些修改之后,目錄結(jié)構(gòu)如下:

本節(jié)產(chǎn)生的文件列表:

  1. Path:"animations.ts" 。

    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()),
        ])
      ]);

  1. Path:"app.component.html" 。

    <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>

  1. Path:"app.component.ts" 。

    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'];
      }
    }

  1. Path:"app.module.ts" 。

    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 { }

  1. Path:"app-routing.module.ts" 。

    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 {}

  1. Path:"hero-list.component.css" 。

    /* 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;
    }

  1. Path:"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>

  1. Path:"hero-list.component.ts" 。

    // 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();
          })
        );
      }
    }

  1. Path:"hero-detail.component.html" 。

    <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>

  1. Path:"hero-detail.component.ts" 。

    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' }]);
    */

  1. Path:"hero.service.ts" 。

    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))
        );
      }
    }

  1. Path:"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 {}

  1. Path:"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, data: { animation: 'heroes' } },
      { path: 'hero/:id', component: HeroDetailComponent, data: { animation: 'hero' } }
    ];


    @NgModule({
      imports: [
        RouterModule.forChild(heroesRoutes)
      ],
      exports: [
        RouterModule
      ]
    })
    export class HeroesRoutingModule { }

  1. Path:"message.service.ts" 。

    import { Injectable } from '@angular/core';


    @Injectable({
      providedIn: 'root',
    })
    export class MessageService {
      messages: string[] = [];


      add(message: string) {
        this.messages.push(message);
      }


      clear() {
        this.messages = [];
      }
    }
以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)