Angular 教程:為英雄之旅添加路由支持-里程碑 3:英雄特征區(qū)

2022-07-04 13:58 更新

里程碑 3: 英雄特征區(qū)

本里程碑涵蓋了以下內(nèi)容:

  • 用模塊把應用和路由組織為一些特性區(qū)。
  • 命令式的從一個組件導航到另一個
  • 通過路由傳遞必要信息和可選信息

這個示例應用在“英雄指南”教程的“服務”部分重新創(chuàng)建了英雄特性區(qū),并復用了Tour of Heroes: Services example code / 下載范例中的大部分代碼。

典型的應用具有多個特性區(qū),每個特性區(qū)都專注于特定的業(yè)務用途并擁有自己的文件夾。

該部分將向你展示如何將應用重構為不同的特性模塊、將它們導入到主模塊中,并在它們之間導航。

添加英雄管理功能

遵循下列步驟:

  • 為了管理這些英雄,在 ?heroes ?目錄下創(chuàng)建一個帶路由的 ?HeroesModule?,并把它注冊到根模塊 ?AppModule ?中。
  • ng generate module heroes/heroes --module app --flat --routing
  • 把 ?app ?下占位用的 ?hero-list? 目錄移到 ?heroes ?目錄中。
  • 從 教程的 "服務" 部分 / 下載范例把 ?heroes/heroes.component.html? 的內(nèi)容復制到 ?hero-list.component.html? 模板中。
    • 給 ?<h2>? 加文字,改成 ?<h2>HEROES</h2>?。
    • 刪除模板底部的 ?<app-hero-detail>? 組件。
  • 把現(xiàn)場演練中 ?heroes/heroes.component.css? 文件的內(nèi)容復制到 ?hero-list.component.css? 文件中。
  • 把現(xiàn)場演練中 ?heroes/heroes.component.ts? 文件的內(nèi)容復制到 ?hero-list.component.ts? 文件中。
    • 把組件類名改為 ?HeroListComponent?。
    • 把 ?selector ?改為 ?app-hero-list?。
    • 對于路由組件來說,這些選擇器不是必須的,因為這些組件是在渲染頁面時動態(tài)插入的,不過選擇器對于在 HTML 元素樹中標記和選中它們是很有用的。

  • 把 ?hero-detail? 目錄中的 ?hero.ts?、?hero.service.ts? 和 ?mock-heroes.ts? 文件復制到 ?heroes ?子目錄下。
  • 把 ?message.service.ts? 文件復制到 ?src/app? 目錄下。
  • 在 ?hero.service.ts? 文件中修改導入 ?message.service? 的相對路徑。
  • 接下來,更新 ?HeroesModule ?的元數(shù)據(jù)。
  • 導入 ?HeroDetailComponent ?和 ?HeroListComponent?,并添加到 ?HeroesModule ?模塊的 ?declarations ?數(shù)組中。
  • 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 {}

英雄管理部分的文件結構如下:


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

英雄特性區(qū)中有兩個相互協(xié)作的組件:英雄列表和英雄詳情。當你導航到列表視圖時,它會獲取英雄列表并顯示出來。當你點擊一個英雄時,詳細視圖就會顯示那個特定的英雄。

通過把所選英雄的 id 編碼進路由的 URL 中,就能告訴詳情視圖該顯示哪個英雄。

從新位置 ?src/app/heroes/? 目錄中導入英雄相關的組件,并定義兩個“英雄管理”路由。

現(xiàn)在,你有了 ?Heroes ?模塊的路由,還得在 ?RouterModule ?中把它們注冊給路由器,和 ?AppRoutingModule ?中的做法幾乎完全一樣,只有一項重要的差別。

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

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

修改后的 ?HeroesRoutingModule ?是這樣的:

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

考慮為每個特性模塊提供自己的路由配置文件。雖然特性路由目前還很少,但即使在小型應用中,路由也會變得越來越復雜。

移除重復的“英雄管理”路由

英雄類的路由目前定義在兩個地方:?HeroesRoutingModule ?中(并最終給 ?HeroesModule?)和 ?AppRoutingModule ?中。

由特性模塊提供的路由會被路由器再組合上它們所導入的模塊的路由。這讓你可以繼續(xù)定義特性路由模塊中的路由,而不用修改主路由配置。

移除 ?HeroListComponent ?的導入和來自 ?app-routing.module.ts? 中的 ?/heroes? 路由。

保留默認路由和通配符路由,因為這些路由仍然要在應用的頂層使用。

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

移除英雄列表的聲明

因為 ?HeroesModule ?現(xiàn)在提供了 ?HeroListComponent?,所以把它從 ?AppModule ?的 ?declarations ?數(shù)組中移除?,F(xiàn)在你已經(jīng)有了一個單獨的 ?HeroesModule?,你可以用更多的組件和不同的路由來演進英雄特性區(qū)。

經(jīng)過這些步驟,?AppModule ?變成了這樣:

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 ?是最后一個,并且位于 ?HeroesModule ?之后。

imports: [
  BrowserModule,
  FormsModule,
  HeroesModule,
  AppRoutingModule
],

路由配置的順序很重要,因為路由器會接受第一個匹配上導航所要求的路徑的那個路由。

當所有路由都在同一個 ?AppRoutingModule ?時,你要把默認路由和通配符路由放在最后(這里是在 ?/heroes? 路由后面), 這樣路由器才有機會匹配到 ?/heroes? 路由,否則它就會先遇到并匹配上該通配符路由,并導航到“頁面未找到”路由。

每個路由模塊都會根據(jù)導入的順序把自己的路由配置追加進去。如果你先列出了 ?AppRoutingModule?,那么通配符路由就會被注冊在“英雄管理”路由之前。通配符路由(它匹配任意URL)將會攔截住每一個到“英雄管理”路由的導航,因此事實上屏蔽了所有“英雄管理”路由。

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

路由參數(shù)

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

回到 ?HeroesRoutingModule ?并再次檢查這些路由定義。?HeroDetailComponent ?路由的路徑中帶有 ?:id? 令牌。

{ path: 'hero/:id', component: HeroDetailComponent }

?:id? 令牌會為路由參數(shù)在路徑中創(chuàng)建一個“空位”。在這里,這種配置會讓路由器把英雄的 ?id ?插入到那個“空位”中。

如果要告訴路由器導航到詳情組件,并讓它顯示“Magneta”,你會期望這個英雄的 ?id ?像這樣顯示在瀏覽器的 URL 中:

localhost:4200/hero/15

如果用戶把此 URL 輸入到瀏覽器的地址欄中,路由器就會識別出這種模式,同樣進入“Magneta”的詳情視圖。

路由參數(shù):必須的還是可選的?
在這個場景下,把路由參數(shù)的令牌 ?:id? 嵌入到路由定義的 ?path ?中是一個好主意,因為對于 ?HeroDetailComponent ?來說 ?id ?是必須的,而且路徑中的值 ?15 ?已經(jīng)足夠把到“Magneta”的路由和到其它英雄的路由明確區(qū)分開。

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

然后導航到 ?HeroDetailComponent ?組件。在那里,你期望看到所選英雄的詳情,這需要兩部分信息:導航目標和該英雄的 ?id?。

因此,這個鏈接參數(shù)數(shù)組中有兩個條目:路由的路徑和一個用來指定所選英雄 ?id ?的路由參數(shù)

<a [routerLink]="['/hero', hero.id]">

路由器從該數(shù)組中組合出了目標 URL:?localhost:3000/hero/15?。

路由器從 URL 中解析出路由參數(shù)(?id:15?),并通過 ActivatedRoute 服務來把它提供給 ?HeroDetailComponent ?組件。

ActivatedRoute 實戰(zhàn)

從路由器(?router?)包中導入 ?Router?、?ActivatedRoute ?和 ?Params ?類。

import { Router, ActivatedRoute, ParamMap } from '@angular/router';

這里導入 ?switchMap ?操作符是因為你稍后將會處理路由參數(shù)的可觀察對象 ?Observable?。

import { switchMap } from 'rxjs/operators';

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

constructor(
  private route: ActivatedRoute,
  private router: Router,
  private service: HeroService
) {}

在 ?ngOnInit()? 方法中,使用 ?ActivatedRoute ?服務來檢索路由的參數(shù),從參數(shù)中提取出英雄的 ?id?,并檢索要顯示的英雄。

ngOnInit() {
  this.hero$ = this.route.paramMap.pipe(
    switchMap((params: ParamMap) =>
      this.service.getHero(params.get('id')!))
  );
}

當這個 map 發(fā)生變化時,?paramMap ?會從更改后的參數(shù)中獲取 ?id ?參數(shù)。

然后,讓 ?HeroService ?去獲取具有該 ?id ?的英雄,并返回 ?HeroService ?請求的結果。

?switchMap ?操作符做了兩件事。它把 ?HeroService ?返回的 ?Observable<Hero>? 拍平,并取消以前的未完成請求。當 ?HeroService ?仍在檢索舊的 ?id ?時,如果用戶使用新的 ?id ?重新導航到這個路由,?switchMap ?會放棄那個舊請求,并返回新 ?id ?的英雄。

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

ParamMap API

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

成員

詳情

has(name)

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

get(name)

如果這個 map 中有參數(shù)名對應的參數(shù)值(字符串),就返回它,否則返回 null。如果參數(shù)值實際上是一個數(shù)組,就返回它的第一個元素。

getAll(name)

如果這個 map 中有參數(shù)名對應的值,就返回一個字符串數(shù)組,否則返回空數(shù)組。當一個參數(shù)名可能對應多個值的時候,請使用 getAll。

keys

返回這個 map 中的所有參數(shù)名組成的字符串數(shù)組。

paramMap 可觀察對象與路由復用

在這個例子中,你接收了路由參數(shù)的 ?Observable ?對象。這種寫法暗示著這些路由參數(shù)在該組件的生存期內(nèi)可能會變化。

默認情況下,如果它沒有訪問過其它組件就導航到了同一個組件實例,那么路由器傾向于復用組件實例。如果復用,這些參數(shù)可以變化。

假設父組件的導航欄有“前進”和“后退”按鈕,用來輪流顯示英雄列表中中英雄的詳情。每次點擊都會強制導航到帶前一個或后一個 ?id ?的 ?HeroDetailComponent ?組件。

你肯定不希望路由器先從 DOM 中移除當前的 ?HeroDetailComponent ?實例,只是為了用下一個 ?id ?重新創(chuàng)建它,因為它將重新渲染視圖。為了更好的用戶體驗,路由器會復用同一個組件實例,而只是更新參數(shù)。

由于 ?ngOnInit()? 在每個組件實例化時只會被調(diào)用一次,所以你可以使用 ?paramMap ?可觀察對象來檢測路由參數(shù)在同一個實例中何時發(fā)生了變化。

當在組件中訂閱一個可觀察對象時,你通??偸且诮M件銷毀時取消這個訂閱。
不過,?ActivatedRoute ?中的可觀察對象是一個例外,因為 ?ActivatedRoute ?及其可觀察對象與 ?Router ?本身是隔離的。?Router ?會在不再需要時銷毀這個路由組件,這意味著此組件的所有成員也都會銷毀,包括注入進來的 ?ActivatedRoute ?以及那些對它的所有 ?Observable ?屬性的訂閱。
?Router ?不會 ?complete ??ActivatedRoute ?的任何 ?Observable?,所以其 ?finalize ?或 ?complete ?代碼塊都不會運行。如果你要在 ?finalize ?中做些什么處理,你仍然要在 ?ngOnDestroy ?中取消訂閱。如果你的 ?Observable ?型管道有某些代碼不希望在當前組件被銷毀后運行,仍然要主動取消訂閱。

snapshot:當不需要 Observable 時的替代品

本應用不需要復用 ?HeroDetailComponent?。用戶總是會先返回英雄列表,再選擇另一位英雄。所以,不存在從一個英雄詳情導航到另一個而不用經(jīng)過英雄列表的情況。這意味著路由器每次都會創(chuàng)建一個全新的 ?HeroDetailComponent ?實例。

假如你很確定這個 ?HeroDetailComponent ?實例永遠不會被復用,你可以使用 ?snapshot?。

?route.snapshot? 提供了路由參數(shù)的初始值。你可以通過它來直接訪問參數(shù),而不用訂閱或者添加 Observable 的操作符,代碼如下:

ngOnInit() {
  const id = this.route.snapshot.paramMap.get('id')!;

  this.hero$ = this.service.getHero(id);
}

用這種技術,?snapshot ?只會得到這些參數(shù)的初始值。如果路由器可能復用該組件,那么就該用 ?paramMap ?可觀察對象的方式。本教程的示例應用中就用了 ?paramMap ?可觀察對象。

導航回列表組件

?HeroDetailComponent ?的 “Back” 按鈕使用了 ?gotoHeroes()? 方法,該方法會強制導航回 ?HeroListComponent?。

路由的 ?navigate()? 方法同樣接受一個單條目的鏈接參數(shù)數(shù)組,你也可以把它綁定到 ?[routerLink]? 指令上。它保存著到 ?HeroListComponent ?組件的路徑:

gotoHeroes() {
  this.router.navigate(['/heroes']);
}

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

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

localhost:4200/hero/15

你也能在路由請求中添加可選信息。比如,當從 ?hero-detail.component.ts? 返回到列表時,如果能自動選中剛剛查看過的英雄就好了。


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

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

  • 結構松散的搜索條件。比如 ?name='wind_'?。
  • 多個值。比如 ?after='12/31/2015' & before='1/1/2017'? - 沒有特定的順序 - ?before='1/1/2017' & after='12/31/2015'? - 具有各種格式 - ?during='currentYear'?。

由于這些參數(shù)不適合用作 URL 路徑,因此可以使用可選參數(shù)在導航過程中傳遞任意復雜的信息??蛇x參數(shù)不參與模式匹配,因此在表達上提供了巨大的靈活性。

和必要參數(shù)一樣,路由器也支持通過可選參數(shù)導航。在你定義完必要參數(shù)之后,再通過一個獨立的對象來定義可選參數(shù)。

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

英雄列表:選定一個英雄(也可不選)

當導航到 ?HeroDetailComponent ?時,你可以在路由參數(shù)中指定一個所要編輯的英雄 ?id?,只要把它作為鏈接參數(shù)數(shù)組中的第二個條目就可以了。

<a [routerLink]="['/hero', hero.id]">

路由器在導航 URL 中內(nèi)嵌了 ?id ?的值,這是因為你把它用一個 ?:id? 占位符當做路由參數(shù)定義在了路由的 ?path ?中:

{ path: 'hero/:id', component: HeroDetailComponent }

當用戶點擊后退按鈕時,?HeroDetailComponent ?構造了另一個鏈接參數(shù)數(shù)組,可以用它導航回 ?HeroListComponent?。

gotoHeroes() {
  this.router.navigate(['/heroes']);
}

該數(shù)組缺少一個路由參數(shù),這是因為以前你不需要往 ?HeroListComponent ?發(fā)送信息。

現(xiàn)在,使用導航請求發(fā)送當前英雄的 ?id?,以便 ?HeroListComponent ?在其列表中突出顯示該英雄。

傳送一個包含可選 ?id ?參數(shù)的對象。為了演示,這里還在對象中定義了一個沒用的額外參數(shù)(?foo?),?HeroListComponent ?應該忽略它。下面是修改過的導航語句:

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(['/heroes', { id: heroId, foo: 'foo' }]);
}

該應用仍然能工作。點擊“back”按鈕返回英雄列表視圖。

注意瀏覽器的地址欄。

它應該是這樣的,不過也取決于你在哪里運行它:

localhost:4200/heroes;id=15;foo=foo

?id ?的值像這樣出現(xiàn)在 URL 中(?;id=15;foo=foo?),但不在 URL 的路徑部分?!癏eroes”路由的路徑部分并沒有定義 ?:id?。

可選的路由參數(shù)沒有使用“?”和“&”符號分隔,因為它們將用在 URL 查詢字符串中。它們是用“;”分隔的。這是矩陣 URL標記法。

Matrix URL 寫法首次提出是在1996 提案中,提出者是 Web 的奠基人:Tim Berners-Lee。
雖然 Matrix 寫法未曾進入過 HTML 標準,但它是合法的。而且在瀏覽器的路由系統(tǒng)中,它作為從父路由和子路由中單獨隔離出參數(shù)的方式而廣受歡迎。Angular 的路由器正是這樣一個路由系統(tǒng),并支持跨瀏覽器的 Matrix 寫法。

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

開發(fā)到現(xiàn)在,英雄列表還沒有變化。沒有突出顯示的英雄行。

?HeroListComponent ?需要添加使用這些參數(shù)的代碼。

以前,當從 ?HeroListComponent ?導航到 ?HeroDetailComponent ?時,你通過 ?ActivatedRoute ?服務訂閱了路由參數(shù)這個 ?Observable?,并讓它能用在 ?HeroDetailComponent ?中。你把該服務注入到了 ?HeroDetailComponent ?的構造函數(shù)中。

這次,你要進行反向?qū)Ш?,?nbsp;?HeroDetailComponent ?到 ?HeroListComponent?。

首先,擴展該路由的導入語句,以包含進 ?ActivatedRoute ?服務的類;

import { ActivatedRoute } from '@angular/router';

導入 ?switchMap ?操作符,在路由參數(shù)的 ?Observable ?對象上執(zhí)行操作。

import { Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators';

在 ?HeroListComponent ?構造函數(shù)中注入 ?ActivatedRoute?。

export class HeroListComponent implements OnInit {
  heroes$!: Observable<Hero[]>;
  selectedId = 0;

  constructor(
    private service: HeroService,
    private route: ActivatedRoute
  ) {}

  ngOnInit() {
    this.heroes$ = this.route.paramMap.pipe(
      switchMap(params => {
        this.selectedId = parseInt(params.get('id')!, 10);
        return this.service.getHeroes();
      })
    );
  }
}

?ActivatedRoute.paramMap? 屬性是一個路由參數(shù)的 ?Observable?。當用戶導航到這個組件時,paramMap 會發(fā)射一個新值,其中包含 ?id?。在 ?ngOnInit()? 中,你訂閱了這些值,設置到 ?selectedId?,并獲取英雄數(shù)據(jù)。

用 CSS 類綁定更新模板,把它綁定到 ?isSelected ?方法上。 如果該方法返回 ?true?,此綁定就會添加 CSS 類 ?selected?,否則就移除它。 在 ?<li>? 標記中找到它,就像這樣:

<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 type="button" routerLink="/sidekicks">Go to sidekicks</button>

當選中列表條目時,要添加一些樣式。

.heroes .selected a {
  background-color: #d6e6f7;
}

.heroes .selected a:hover {
  background-color: #bdd7f5;
}

當用戶從英雄列表導航到英雄“Magneta”并返回時,“Magneta”看起來是選中的:


這個可選的 ?foo ?路由參數(shù)人畜無害,路由器會繼續(xù)忽略它。

添加路由動畫

在這一節(jié),你將為英雄詳情組件添加一些動畫。

首先導入 ?BrowserAnimationsModule?,并添加到 ?imports ?數(shù)組中:

import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

@NgModule({
  imports: [
    BrowserAnimationsModule,
  ],
})

接下來,為指向 ?HeroListComponent ?和 ?HeroDetailComponent ?的路由定義添加一個 ?data ?對象。 轉(zhuǎn)場是基于 ?state ?的,你將使用來自路由的 ?animation ?數(shù)據(jù)為轉(zhuǎn)場提供一個有名字的動畫 ?state?。

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)建一個 ?animations.ts?。內(nèi)容如下:

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

該文件做了如下工作:

  • 導入動畫符號以構建動畫觸發(fā)器、控制狀態(tài)并管理狀態(tài)之間的過渡。
  • 導出了一個名叫 ?slideInAnimation ?的常量,并把它設置為一個名叫 ?routeAnimation ?的動畫觸發(fā)器。
  • 定義一個轉(zhuǎn)場動畫,當在 ?heroes ?和 ?hero ?路由之間來回切換時,如果進入(?:enter?)應用視圖則讓組件從屏幕的左側(cè)滑入,如果離開(?:leave?)應用視圖則讓組件從右側(cè)劃出。

回到 ?AppComponent?,從 ?@angular/router? 包導入 ?RouterOutlet?,并從 ?'./animations.ts? 導入 ?slideInAnimation?。

為包含 ?slideInAnimation ?的 ?@Component? 元數(shù)據(jù)添加一個 ?animations ?數(shù)組。

import { ChildrenOutletContexts } from '@angular/router';
import { slideInAnimation } from './animations';

@Component({
  selector: 'app-root',
  templateUrl: 'app.component.html',
  styleUrls: ['app.component.css'],
  animations: [ slideInAnimation ]
})

要想使用路由動畫,就要把 ?RouterOutlet ?包裝到一個元素中。再把 ?@routeAnimation? 觸發(fā)器綁定到該元素上。

為了把 ?@routeAnimation? 轉(zhuǎn)場轉(zhuǎn)場到指定的狀態(tài),你需要從 ?ActivatedRoute ?的 ?data ?中提供它。?RouterOutlet ?導出成了一個模板變量 ?outlet?,這樣你就可以綁定一個到路由出口的引用了。這個例子中使用了一個 ?routerOutlet ?變量。

<h1>Angular Router</h1>
<nav>
  <a routerLink="/crisis-center" routerLinkActive="active" ariaCurrentWhenActive="page">Crisis Center</a>
  <a routerLink="/heroes" routerLinkActive="active" ariaCurrentWhenActive="page">Heroes</a>
</nav>
<div [@routeAnimation]="getAnimationData()">
  <router-outlet></router-outlet>
</div>

?@routeAnimation? 屬性使用所提供的 ?routerOutlet ?引用來綁定到 ?getAnimationData()?,它會根據(jù)主路由所提供的 ?data ?對象返回動畫的屬性。?animation ?屬性會根據(jù)你在 ?animations.ts? 中定義 ?slideInAnimation ?時使用的 ?transition ?名稱進行匹配。

export class AppComponent {
  constructor(private contexts: ChildrenOutletContexts) {}

  getAnimationData() {
      return this.contexts.getContext('primary')?.route?.snapshot?.data?.['animation'];
  }
}

如果在兩個路由之間切換,導航進來時,?HeroDetailComponent ?和 ?HeroListComponent ?會從左側(cè)滑入;導航離開時將會從右側(cè)劃出。

里程碑 3 的小結

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

  • 把應用組織成特性區(qū)
  • 命令式的從一個組件導航到另一個
  • 通過路由參數(shù)傳遞信息,并在組件中訂閱它們
  • 把這個特性分區(qū)模塊導入根模塊 AppModule
  • 把動畫應用到路由組件上

做完這些修改之后,目錄結構如下:


這里是當前版本的范例程序相關文件。

  • 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()),
        ])
      ]);
  • app.component.html
  • <h1>Angular Router</h1>
    <nav>
      <a routerLink="/crisis-center" routerLinkActive="active" ariaCurrentWhenActive="page">Crisis Center</a>
      <a routerLink="/heroes" routerLinkActive="active" ariaCurrentWhenActive="page">Heroes</a>
    </nav>
    <div [@routeAnimation]="getAnimationData()">
      <router-outlet></router-outlet>
    </div>
  • app.component.ts
  • import { Component } from '@angular/core';
    import { ChildrenOutletContexts } from '@angular/router';
    import { slideInAnimation } from './animations';
    
    @Component({
      selector: 'app-root',
      templateUrl: 'app.component.html',
      styleUrls: ['app.component.css'],
      animations: [ slideInAnimation ]
    })
    export class AppComponent {
      constructor(private contexts: ChildrenOutletContexts) {}
    
      getAnimationData() {
          return this.contexts.getContext('primary')?.route?.snapshot?.data?.['animation'];
      }
    }
  • 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 { }
  • 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 {}
  • hero-list.component.css
  • /* HeroListComponent's private CSS styles */
    .heroes {
      margin: 0 0 2em 0;
      list-style-type: none;
      padding: 0;
      width: 100%;
    }
    .heroes li {
      position: relative;
      cursor: pointer;
    }
    
    .heroes li:hover {
      left: .1em;
    }
    
    .heroes a {
      color: black;
      text-decoration: none;
      display: block;
      font-size: 1.2rem;
      background-color: #eee;
      margin: .5rem .5rem .5rem 0;
      padding: .5rem 0;
      border-radius: 4px;
    }
    
    .heroes a:hover {
      color: #2c3a41;
      background-color: #e6e6e6;
    }
    
    .heroes a:active {
      background-color: #525252;
      color: #fafafa;
    }
    
    .heroes .selected a {
      background-color: #d6e6f7;
    }
    
    .heroes .selected a:hover {
      background-color: #bdd7f5;
    }
    
    .heroes .badge {
      padding: .5em .6em;
      color: white;
      background-color: #435b60;
      min-width: 16px;
      margin-right: .8em;
      border-radius: 4px 0 0 4px;
    }
  • 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 type="button" routerLink="/sidekicks">Go to sidekicks</button>
  • 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 = 0;
    
      constructor(
        private service: HeroService,
        private route: ActivatedRoute
      ) {}
    
      ngOnInit() {
        this.heroes$ = this.route.paramMap.pipe(
          switchMap(params => {
            this.selectedId = parseInt(params.get('id')!, 10);
            return this.service.getHeroes();
          })
        );
      }
    }
  • hero-detail.component.html
  • <h2>Heroes</h2>
    <div *ngIf="hero$ | async as hero">
      <h3>{{ hero.name }}</h3>
      <p>Id: {{ hero.id }}</p>
      <label for="hero-name">Hero name: </label>
      <input type="text" id="hero-name" [(ngModel)]="hero.name" placeholder="name"/>
      <button type="button" (click)="gotoHeroes(hero)">Back</button>
    </div>
  • 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) {
        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(['/heroes', { id: heroId, foo: 'foo' }]);
      }
    }
  • 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)!)
        );
      }
    }
  • 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 {}
  • 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 { }
  • 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)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號