1. 程式人生 > >Angular學習筆記10:路由

Angular學習筆記10:路由

在工作或者學習中,有時候會遇到如下需求:

在一個頁面上寫一個導航,導航欄中的每一個選項都對應一個頁面。就如Angular官方文件中:

(圖片來自於Angular官方文件)

  • 新增一個儀表盤檢視。

  • HeroesDashBoard檢視之間導航。

  • 無論在哪個檢視中點選一個英雄,都會導航到這個Hero的詳情頁。

  • 在郵件中點選一個深連結,會直接開啟一個特定英雄的詳情檢視。

具體實現過程:

1.新增 AppRoutingModule

 
  1. bogon:demo wjy$ ng generate module app-routing --flat --module=app

  2. CREATE src/app/app-routing.module.spec.ts (308 bytes)

  3. CREATE src/app/app-routing.module.ts (194 bytes)

  4. UPDATE src/app/app.module.ts (718 bytes)

注意:

--flat 把這個檔案放進了 src/app 中,而不是單獨的目錄中。
--module=app 告訴 CLI 把它註冊到 AppModule 的 imports 陣列中。

通常情況下不會在路由模組中宣告元件,所以可以刪除 @

NgModule.declarations 並刪除對 CommonModule 的引用。

 

在路由的模組中,通常會使用:RouterModule中的Routes類來配置路由器,所以要從@angular/router 中引入對這個兩個符號的引入

新增一個 @NgModule.exports 陣列,其中放上 RouterModule 。 匯出 RouterModule 讓路由器的相關指令可以在 AppModule 中的元件中使用。

此時的AppRoutingModule :

 
  1. import {NgModule} from '@angular/core';

  2. import {RouterModule, Routes} from '@angular/router';

  3.  
  4. @NgModule({

  5. imports: [],

  6. exports: [RouterModule],

  7. })

  8. export class AppRoutingModule {

  9. }

2.新增路由定義

路由定義 會告訴路由器,當用戶點選某個連結或者在瀏覽器位址列中輸入某個 URL 時,要顯示哪個檢視。

典型的 Angular 路由(Route)有兩個屬性:

  • path:一個用於匹配瀏覽器位址列中 URL 的字串。

  • component:當導航到此路由時,路由器應該建立哪個元件

eg :當URL 為 localhost:8000/heroes 時,就導航到 HeroesComponent

  • 匯入 HeroesComponent,以便能在 Route中引用它。 
  • 定義一個路由陣列,其中的某個路由是指向這個元件的。

3.RouterModule.forRoot()

此時想要路由生效,就必須先初始化路由,使路由器開始監聽瀏覽器位址列的變化。

把 RouterModule 新增到 @NgModule.imports 陣列中,並用 routes 來配置它。你只要呼叫 imports 陣列中的 RouterModule.forRoot() 函式就行了

imports: [ RouterModule.forRoot(routes) ]

關於:forRoot:因為現在要在應用的頂級配置這個路由器。所以 forRoot() 方法會提供路由所需的服務提供商和指令,還會基於瀏覽器的當前 URL 執行首次導航。

4.新增路由出口 (RouterOutlet

開啟 AppComponent 的模板,把 <app-heroes> 元素替換為 <router-outlet> 元素。

 
  1. <h1>{{title}}</h1>

  2. <router-outlet></router-outlet>

  3. <app-messages></app-messages>

在這裡移除 <app-heroes>,是因為只有當用戶導航到這裡時,才需要顯示 HeroesComponent

<router-outlet> 會告訴路由器要在哪裡顯示路由到的檢視。

儲存,重新整理瀏覽器,在localhost:8000後面加上/heroes 就會看到之前的介面了。

5.新增路由連結 (routerLInk)

在正常的應用中,是不可能然使用者去位址列輸入某些地址的,而且使用者也不可能知道需要輸入什麼,使用者應該在點選某個按鈕或者超級連結的時候,是應用跳轉到相應的頁面,使路由器自動的去跳轉。在當前的應用中:新增一個 <nav> 元素,並在其中放一個連結 <a> 元素,當點選它時,就會觸發一個到 HeroesComponent 的導航。 

 
  1. <h1>{{title}}</h1>

  2. <nav>

  3. <a routerLink="/heroes">Heroes</a>

  4. </nav>

  5. <router-outlet></router-outlet>

  6. <app-messages></app-messages>

routerLink的值為 "/heroes",路由器會用它來匹配出指向 HeroesComponent 的路由。 routerLink 是 RouterLink指令的選擇器,它會把使用者的點選轉換為路由器的導航操作。 它是RouterModule中公開的另一個指令。

儲存,重新整理瀏覽器,顯示出了應用的標題和指向英雄列表的連結,但並沒有顯示英雄列表。

點選這個連結。位址列變成了 /heroes,並且顯示出了英雄列表。

6.新增儀表盤檢視

在一個應用中,不可能只有一個頁面,再有多個檢視的情況下,才能使路由器更好的發揮其作用,使用 CLI 新增一個 DashboardComponent。

ng generate component dashboard

注意:CLI 生成了 DashboardComponent 的相關檔案,並把它宣告到 AppModule 中。

修改DashboardComponent模板檔案:

 
  1. <h3>Top Heroes</h3>

  2. <div class="grid grid-pad">

  3. <a *ngFor="let hero of heroes" class="col-1-4">

  4. <div class="module hero">

  5. <h4>{{hero.name}}</h4>

  6. </div>

  7. </a>

  8. </div>

修改DashboardComponent類檔案:

 
  1. import { Component, OnInit } from '@angular/core';

  2. import { Hero } from '../hero';

  3. import { HeroService } from '../hero.service';

  4.  
  5. @Component({

  6. selector: 'app-dashboard',

  7. templateUrl: './dashboard.component.html',

  8. styleUrls: [ './dashboard.component.css' ]

  9. })

  10. export class DashboardComponent implements OnInit {

  11. heroes: Hero[] = [];

  12.  
  13. constructor(private heroService: HeroService) { }

  14.  
  15. ngOnInit() {

  16. this.getHeroes();

  17. }

  18.  
  19. getHeroes(): void {

  20. this.heroService.getHeroes()

  21. .subscribe(heroes => this.heroes = heroes.slice(1, 5));

  22. }

  23. }

使DashboardComponent更好看點,增加私有樣式

 
  1. [class*='col-'] {

  2. float: left;

  3. padding-right: 20px;

  4. padding-bottom: 20px;

  5. }

  6. [class*='col-']:last-of-type {

  7. padding-right: 0;

  8. }

  9. a {

  10. text-decoration: none;

  11. }

  12. *, *:after, *:before {

  13. -webkit-box-sizing: border-box;

  14. -moz-box-sizing: border-box;

  15. box-sizing: border-box;

  16. }

  17. h3 {

  18. text-align: center; margin-bottom: 0;

  19. }

  20. h4 {

  21. position: relative;

  22. }

  23. .grid {

  24. margin: 0;

  25. }

  26. .col-1-4 {

  27. width: 25%;

  28. }

  29. .module {

  30. padding: 20px;

  31. text-align: center;

  32. color: #eee;

  33. max-height: 120px;

  34. min-width: 120px;

  35. background-color: #607d8b;

  36. border-radius: 2px;

  37. }

  38. .module:hover {

  39. background-color: #eee;

  40. cursor: pointer;

  41. color: #607d8b;

  42. }

  43. .grid-pad {

  44. padding: 10px 0;

  45. }

  46. .grid-pad > [class*='col-']:last-of-type {

  47. padding-right: 20px;

  48. }

  49. @media (max-width: 600px) {

  50. .module {

  51. font-size: 10px;

  52. max-height: 75px; }

  53. }

  54. @media (max-width: 1024px) {

  55. .grid {

  56. margin: 0;

  57. }

  58. .module {

  59. min-width: 60px;

  60. }

  61. }

解釋:

DashboardComponent模板用來表示由英雄名字連結組成的一個陣列。

  • *ngFor 複寫器為元件的 heroes 陣列中的每個條目建立了一個連結。

  • 這些連結被 dashboard.component.css 中的樣式格式化成了一些色塊。

DashboardComponent類和 HeroesComponent 類很像。

  • 它定義了一個 heroes 陣列屬性。

  • 它的建構函式希望 Angular 把 HeroService 注入到私有的 heroService 屬性中。

  • 在 ngOnInit() 生命週期鉤子中呼叫 getHeroes

這個 getHeroes 函式把要顯示的英雄的數量縮減為四個(第二、第三、第四、第五)。

7.新增儀表盤路由

如果需要導航到儀表盤的檢視就需要在路由器中新增一個路由。把 DashboardComponent 匯入到 AppRoutingModule 中。

把一個指向 DashboardComponent 的路由新增到 AppRoutingModule.routes 陣列中。

 
  1. import { DashboardComponent } from './dashboard/dashboard.component';

  2.  
  3.  
  4. ....

  5. ....

  6. ....

  7.  
  8.  
  9. const routes: Routes = [

  10. {path: 'heroes', component: HeroesComponent},

  11. {path: 'dashboard', component: DashboardComponent},

  12. ];

8.新增預設路由

當應用啟動時,瀏覽器的位址列指向了網站的根路徑。 它沒有匹配到任何現存路由,因此路由器也不會導航到任何地方。 <router-outlet> 下方是空白的。要讓應用自動導航到這個儀表盤,請把下列路由新增到 AppRoutingModule.Routes 陣列中。

 
  1. const routes: Routes = [

  2. {path: 'heroes', component: HeroesComponent},

  3. {path: 'dashboard', component: DashboardComponent},

  4. {path: '', redirectTo: '/dashboard', pathMatch: 'full'},

  5. ];

這個路由會把一個與空路徑“完全匹配”的 URL 重定向到路徑為 '/dashboard' 的路由。

瀏覽器重新整理之後,路由器載入了 DashboardComponent,並且瀏覽器的位址列會顯示出 /dashboard 這個 URL。

9.把儀表盤連結新增到殼元件中

在這個應用中應該允許使用者通過點選頁面頂部導航區的各個連結在 DashboardComponent 和 HeroesComponent 之間來回導航。

把儀表盤的導航連結新增到殼元件 AppComponent 的模板中,就放在 Heroes 連結的前面。

 
  1. <h1>{{title}}</h1>

  2. <nav>

  3. <a routerLink="/dashboard">Dashboard</a>

  4. <a routerLink="/heroes">Heroes</a>

  5. </nav>

  6. <router-outlet></router-outlet>

  7. <app-messages></app-messages>

儲存,重新整理瀏覽器,你就能通過點選這些連結在這兩個檢視之間自由導航了。

11.導航到英雄詳情

HeroDetailComponent 可以顯示所選英雄的詳情。 此刻,HeroDetailsComponent 只能在 HeroesComponent 的底部看到

使用者應該能通過三種途徑看到這些詳情。

  1. 通過在儀表盤中點選某個英雄。

  2. 通過在英雄列表中點選某個英雄。

  3. 通過把一個“深連結” URL 貼上到瀏覽器的位址列中來指定要顯示的英雄。

12.從 HeroesComponent 中刪除英雄詳情

開啟 HeroesComponent 的模板檔案(heroes/heroes.component.html),並從底部刪除 <app-hero-detail> 元素。

13.新增英雄詳情檢視

要導航到 id 為 11 的英雄的詳情檢視,類似於 ~/detail/11 的 URL 將是一個不錯的 URL。

開啟 AppRoutingModule 並匯入 HeroDetailComponent

然後把一個引數化路由新增到 AppRoutingModule.routes 陣列中,它要匹配指向英雄詳情檢視的路徑。

 
  1. import { HeroDetailComponent } from './hero-detail/hero-detail.component';

  2. ...

  3. ...

  4. ...

  5. const routes: Routes = [

  6. {path: 'heroes', component: HeroesComponent},

  7. {path: 'dashboard', component: DashboardComponent},

  8. {path: '', redirectTo: '/dashboard', pathMatch: 'full'},

  9. {path: 'detail/:id', component: HeroDetailComponent},

  10. ];

 注意:path 中的冒號(:)表示 :id 是一個佔位符,它表示某個特定英雄的 id

14.DashboardComponent 中的英雄連結

 
  1. <h3>Top Heroes</h3>

  2. <div class="grid grid-pad">

  3. <a *ngFor="let hero of heroes" class="col-1-4"

  4. routerLink="/detail/{{hero.id}}">

  5. <div class="module hero">

  6. <h4>{{hero.name}}</h4>

  7. </div>

  8. </a>

  9. </div>

15.HeroesComponent 中的英雄連結

 
  1. <h2>My Heroes</h2>

  2. <ul class="heroes">

  3. <li *ngFor="let hero of heroes">

  4. <a routerLink="/detail/{{hero.id}}">

  5. <span class="badge">{{hero.id}}</span> {{hero.name}}

  6. </a>

  7. </li>

  8. </ul>

16.移除死程式碼

在這個過程中,會有一些之用到,但是現在用不到的程式碼,雖然程式碼量很少,但是最好還是將其刪除。

17.支援路由的 HeroDetailComponent

當路由器會在響應形如 ~/detail/11 的 URL 時建立 HeroDetailComponent

HeroDetailComponent 需要從一種新的途徑獲取要顯示的英雄

  • 獲取建立本元件的路由,

  • 從這個路由中提取出 id

  • 通過 HeroService 從伺服器上獲取具有這個 id 的英雄資料。

匯入需要的語句,修改建構函式,把 ActivatedRouteHeroService 和 Location 服務注入到建構函式中,將它們的值儲存到私有變數裡:

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

  2. import { Location } from '@angular/common';

  3.  
  4. import { HeroService } from '../hero.service';

  5.  
  6. ...

  7. ...

  8. ...

  9. constructor(

  10. private route: ActivatedRoute,

  11. private heroService: HeroService,

  12. private location: Location

  13. ) {}

ActivatedRoute 儲存著到這個 HeroDetailComponent 例項的路由資訊。 這個元件對從 URL 中提取的路由引數感興趣。 其中的 id 引數就是要現實的英雄的 id

HeroService 從遠端伺服器獲取英雄資料,本元件將使用它來獲取要顯示的英雄

location 是一個 Angular 的服務,用來與瀏覽器打交道。

18.從路由引數中提取 id

 
  1. ngOnInit(): void {

  2. this.getHero();

  3. }

  4.  
  5. getHero(): void {

  6. const id = +this.route.snapshot.paramMap.get('id');

  7. this.heroService.getHero(id)

  8. .subscribe(hero => this.hero = hero);

  9. }

route.snapshot 是一個路由資訊的靜態快照,抓取自元件剛剛建立完畢之後。

paramMap 是一個從 URL 中提取的路由引數值的字典。 "id" 對應的值就是要獲取的英雄的 id

路由引數總會是字串。 JavaScript 的 (+) 操作符會把字串轉換成數字,英雄的 id 就是數字型別。

重新整理瀏覽器,應用掛了。出現一個編譯錯誤,因為 HeroService 沒有一個名叫 getHero() 的方法。

19.新增 HeroService.getHero()

 
  1. getHero(id: number): Observable<Hero> {

  2. this.messageService.add(`HeroService: fetched hero id=${id}`);

  3. return of(HEROES.find(hero => hero.id === id));

  4. }

注意:反引號 ( ` ) 用於定義 JavaScript 的 模板字串字面量,以便嵌入 id

getHero() 也有一個非同步函式簽名。 它用 RxJS 的 of() 函式返回一個 Observable 形式的模擬英雄資料。

將來可以用一個真實的 Http 請求來重現實現 getHero(),而不用修改呼叫了它的 HeroDetailComponent

儲存,重新整理瀏覽器,應用又恢復正常了。 你可以在儀表盤或英雄列表中點選一個英雄來導航到該英雄的詳情檢視。

20.回到原路

通過點選瀏覽器的後退按鈕,你可以回到英雄列表或儀表盤檢視,這取決於你從哪裡進入的詳情檢視。

如果能在 HeroDetail 檢視中也有這麼一個按鈕就更好了。

把一個後退按鈕新增到元件模板的底部,並且把它繫結到元件的 goBack() 方法。

src/app/hero-detail/hero-detail.component.html (back button)

<button (click)="goBack()">go back</button>

在元件類中新增一個 goBack() 方法,利用你以前注入的 Location 服務在瀏覽器的歷史棧中後退一步。

 
  1. goBack(): void {

  2. this.location.back();

  3. }

21.增加一些樣式,使頁面更好看。

AppComponent

 
  1. h1 {

  2. font-size: 1.2em;

  3. color: #999;

  4. margin-bottom: 0;

  5. }

  6. h2 {

  7. font-size: 2em;

  8. margin-top: 0;

  9. padding-top: 0;

  10. }

  11. nav a {

  12. padding: 5px 10px;

  13. text-decoration: none;

  14. margin-top: 10px;

  15. display: inline-block;

  16. background-color: #eee;

  17. border-radius: 4px;

  18. }

  19. nav a:visited, a:link {

  20. color: #607d8b;

  21. }

  22. nav a:hover {

  23. color: #039be5;

  24. background-color: #cfd8dc;

  25. }

  26. nav a.active {

  27. color: #039be5;

  28. }

DashboardComponent

 
  1. [class*='col-'] {

  2. float: left;

  3. padding-right: 20px;

  4. padding-bottom: 20px;

  5. }

  6. [class*='col-']:last-of-type {

  7. padding-right: 0;

  8. }

  9. a {

  10. text-decoration: none;

  11. }

  12. *, *:after, *:before {

  13. -webkit-box-sizing: border-box;

  14. -moz-box-sizing: border-box;

  15. box-sizing: border-box;

  16. }

  17. h3 {

  18. text-align: center; margin-bottom: 0;

  19. }

  20. h4 {

  21. position: relative;

  22. }

  23. .grid {

  24. margin: 0;

  25. }

  26. .col-1-4 {

  27. width: 25%;

  28. }

  29. .module {

  30. padding: 20px;

  31. text-align: center;

  32. color: #eee;

  33. max-height: 120px;

  34. min-width: 120px;

  35. background-color: #607d8b;

  36. border-radius: 2px;

  37. }

  38. .module:hover {

  39. background-color: #eee;

  40. cursor: pointer;

  41. color: #607d8b;

  42. }

  43. .grid-pad {

  44. padding: 10px 0;

  45. }

  46. .grid-pad > [class*='col-']:last-of-type {

  47. padding-right: 20px;

  48. }

  49. @media (max-width: 600px) {

  50. .module {

  51. font-size: 10px;

  52. max-height: 75px; }

  53. }

  54. @media (max-width: 1024px) {

  55. .grid {

  56. margin: 0;

  57. }

  58. .module {

  59. min-width: 60px;

  60. }

  61. }

HeroesComponent

 
  1. .heroes {

  2. margin: 0 0 2em 0;

  3. list-style-type: none;

  4. padding: 0;

  5. width: 15em;

  6. }

  7. .heroes li {

  8. position: relative;

  9. cursor: pointer;

  10. background-color: #EEE;

  11. margin: .5em;

  12. padding: .3em 0;

  13. height: 1.6em;

  14. border-radius: 4px;

  15. }

  16.  
  17. .heroes li:hover {

  18. color: #607D8B;

  19. background-color: #DDD;

  20. left: .1em;

  21. }

  22.  
  23. .heroes a {

  24. color: #888;

  25. text-decoration: none;

  26. position: relative;

  27. display: block;

  28. width: 250px;

  29. }

  30.  
  31. .heroes a:hover {

  32. color:#607D8B;

  33. }

  34.  
  35. .heroes .badge {

  36. display: inline-block;

  37. font-size: small;

  38. color: white;

  39. padding: 0.8em 0.7em 0 0.7em;

  40. background-color: #607D8B;

  41. line-height: 1em;

  42. position: relative;

  43. left: -1px;

  44. top: -4px;

  45. height: 1.8em;

  46. min-width: 16px;

  47. text-align: right;

  48. margin-right: .8em;

  49. border-radius: 4px 0 0 4px;

  50. }

HeroDetailComponent

 
  1. label {

  2. display: inline-block;

  3. width: 3em;

  4. margin: .5em 0;

  5. color: #607D8B;

  6. font-weight: bold;

  7. }

  8. input {

  9. height: 2em;

  10. font-size: 1em;

  11. padding-left: .4em;

  12. }

  13. button {

  14. margin-top: 20px;

  15. font-family: Arial;

  16. background-color: #eee;

  17. border: none;

  18. padding: 5px 10px;

  19. border-radius: 4px;

  20. cursor: pointer; cursor: hand;

  21. }

  22. button:hover {

  23. background-color: #cfd8dc;

  24. }

  25. button:disabled {

  26. background-color: #eee;

  27. color: #ccc;

  28. cursor: auto;

  29. }