填寫這份《一分鐘調查》,幫我們(開發組)做得更好!去填寫Home

路由器課程:英雄之旅

Router tutorial: tour of heroes

本課程提供了關於 Angular 路由器的概要性概述。在本課程中,你將基於基本的路由器配置來探索諸如子路由、路由引數、延遲載入 NgModule、路由守衛和預載入資料等功能,以改善使用者體驗。

This tutorial provides an extensive overview of the Angular router. In this tutorial, you will build upon a basic router configuration to explore features such as child routes, route parameters, lazy load NgModules, guard routes, and preloading data to improve the user experience.

有關該應用最終版本的有效示例,請參閱現場演練 / 下載範例

For a working example of the final version of the app, see the現場演練 / 下載範例.

目標

Objectives

本指南描述了一個多頁路由示例應用程式的開發過程。在此過程中,它重點介紹了路由器的關鍵特性,例如:

This guide describes development of a multi-page routed sample application. Along the way, it highlights key features of the router such as:

  • 將應用程式功能組織到模組中。

    Organizing the application features into modules.

  • 導航到元件(從 Heroes 連結導航到“英雄列表”)。

    Navigating to a component (Heroes link to "Heroes List").

  • 包括一個路由引數(在路由到“英雄詳細資訊”時傳入英雄的 id

    Including a route parameter (passing the Hero id while routing to the "Hero Detail").

  • 子路由(危機中心特性區有自己的路由)。

    Child routes (the Crisis Center has its own routes).

  • CanActivate 守衛(檢查路由訪問)。

    The CanActivate guard (checking route access).

  • CanActivateChild 守衛(檢查子路由訪問)。

    The CanActivateChild guard (checking child route access).

  • CanDeactivate 守衛(在放棄未儲存的更改之前請求許可)。

    The CanDeactivate guard (ask permission to discard unsaved changes).

  • Resolve 守衛(預先獲取路由資料)。

    The Resolve guard (pre-fetching route data).

  • 延遲載入 NgModule

    Lazy loading an NgModule.

  • CanLoad 守衛(在載入功能模組的檔案之前檢查)。

    The CanLoad guard (check before loading feature module assets).

本指南按照里程碑的順序進行,就像你逐步建構應用程式一樣,但這裡假定你已經熟悉 Angular 的基本概念。關於 Angular 的一般性介紹,請參見《入門指南》。關於更深入的概述,請參見《英雄之旅》課程。

This guide proceeds as a sequence of milestones as if you were building the app step-by-step, but assumes you are familiar with basic Angular concepts. For a general introduction to angular, see the Getting Started. For a more in-depth overview, see the Tour of Heroes tutorial.

先決條件

Prerequisites

要完成本課程,你應該對以下概念有基本的瞭解:

To complete this tutorial, you should have a basic understanding of the following concepts:

你可能會發現《英雄之旅》課程很有用,但這不是必需的。

You might find the Tour of Heroes tutorial helpful, but it is not required.

The sample application in action

範例應用實戰

本課程的示例應用會幫助“英雄僱傭管理局”找到需要各位英雄去解決的危機。

The sample application for this tutorial helps the Hero Employment Agency find crises for heroes to solve.

本應用有三個主要的特性區:

The application has three main feature areas:

  1. 危機中心,用於維護要指派給英雄的危機列表。

    A Crisis Center for maintaining the list of crises for assignment to heroes.

  2. 英雄特性區,用於維護管理局僱傭的英雄列表。

    A Heroes area for maintaining the list of heroes employed by the agency.

  3. 管理特性區會管理危機和英雄的列表。

    An Admin area to manage the list of crises and heroes.

點選到線上例子的連結到線上例子的連結 / 下載範例試用一下。

Try it by clicking on thislive example linklive example link / 下載範例.

該應用會渲染出一排導航按鈕和和一個英雄列表檢視。

The app renders with a row of navigation buttons and the Heroes view with its list of heroes.

選擇其中之一,該應用就會把你帶到此英雄的編輯頁面。

Select one hero and the app takes you to a hero editing screen.

修改完名字,再點選“後退”按鈕,應用又回到了英雄列表頁,其中顯示的英雄名已經變了。注意,對名字的修改會立即生效。

Alter the name. Click the "Back" button and the app returns to the heroes list which displays the changed hero name. Notice that the name change took effect immediately.

另外你也可以點選瀏覽器本身的後退按鈕(而不是應用中的 “Back” 按鈕),這也同樣會回到英雄列表頁。 在 Angular 應用中導航也會和標準的 Web 導航一樣更新瀏覽器中的歷史。

Had you clicked the browser's back button instead of the app's "Back" button, the app would have returned you to the heroes list as well. Angular app navigation updates the browser history as normal web navigation does.

現在,點選危機中心連結,前往危機列表頁。

Now click the Crisis Center link for a list of ongoing crises.

選擇其中之一,該應用就會把你帶到此危機的編輯頁面。 危機詳情是當前頁的子元件,就在列表的緊下方。

Select a crisis and the application takes you to a crisis editing screen. The Crisis Detail appears in a child component on the same page, beneath the list.

修改危機的名稱。 注意,危機列表中的相應名稱並沒有修改。

Alter the name of a crisis. Notice that the corresponding name in the crisis list does not change.

這和英雄詳情頁略有不同。英雄詳情會立即儲存你所做的更改。 而危機詳情頁中,你的更改都是臨時的 —— 除非按“儲存”按鈕儲存它們,或者按“取消”按鈕放棄它們。 這兩個按鈕都會導航回危機中心,顯示危機列表。

Unlike Hero Detail, which updates as you type, Crisis Detail changes are temporary until you either save or discard them by pressing the "Save" or "Cancel" buttons. Both buttons navigate back to the Crisis Center and its list of crises.

單擊瀏覽器後退按鈕或 “Heroes” 連結,可以啟用一個對話方塊。

Click the browser back button or the "Heroes" link to activate a dialog.

你可以回答“確定”以放棄這些更改,或者回答“取消”來繼續編輯。

You can say "OK" and lose your changes or click "Cancel" and continue editing.

這種行為的幕後是路由器的 CanDeactivate 守衛。 該守衛讓你有機會進行清理工作或在離開當前檢視之前請求使用者的許可。

Behind this behavior is the router's CanDeactivate guard. The guard gives you a chance to clean-up or ask the user's permission before navigating away from the current view.

AdminLogin 按鈕用於示範路由器的其它能力,本章稍後的部分會講解它們。

The Admin and Login buttons illustrate other router capabilities covered later in the guide.

里程碑 1:起步

Milestone 1: Getting started

開始本應用的一個簡版,它在兩個空路由之間導航。

Begin with a basic version of the app that navigates between two empty views.

用 Angular CLI 產生一個範例應用。

Generate a sample application with the Angular CLI.

ng new angular-router-sample
      
      ng new angular-router-sample
    

定義路由

Define Routes

路由器必須用“路由定義”的列表進行配置。

A router must be configured with a list of route definitions.

每個定義都被翻譯成了一個Route物件。該物件有一個 path 欄位,表示該路由中的 URL 路徑部分,和一個 component 欄位,表示與該路由相關聯的元件。

Each definition translates to a Route object which has two things: a path, the URL path segment for this route; and a component, the component associated with this route.

當瀏覽器的 URL 變化時或在程式碼中告訴路由器導航到一個路徑時,路由器就會翻出它用來儲存這些路由定義的登錄檔。

The router draws upon its registry of definitions when the browser URL changes or when application code tells the router to navigate along a route path.

第一個路由執行以下操作:

The first route does the following:

  • 當瀏覽器位址列的 URL 變化時,如果它匹配上了路徑部分 /crisis-center,路由器就會啟用一個 CrisisListComponent 的實例,並顯示它的檢視。

    When the browser's location URL changes to match the path segment /crisis-center, then the router activates an instance of the CrisisListComponent and displays its view.

  • 當應用程式請求導航到路徑 /crisis-center 時,路由器啟用一個 CrisisListComponent 的實例,顯示它的檢視,並將該路徑更新到瀏覽器位址列和歷史。

    When the application requests navigation to the path /crisis-center, the router activates an instance of CrisisListComponent, displays its view, and updates the browser's address location and history with the URL for that path.

第一個配置定義了由兩個路由構成的陣列,它們用最短路徑指向了 CrisisListComponentHeroListComponent

The first configuration defines an array of two routes with minimal paths leading to the CrisisListComponent and HeroListComponent.

產生 CrisisListHeroList 元件,以便路由器能夠渲染它們。

Generate the CrisisList and HeroList components so that the router has something to render.

ng generate component crisis-list
      
      ng generate component crisis-list
    
ng generate component hero-list
      
      ng generate component hero-list
    

把每個元件的內容都替換成下列範例 HTML。

Replace the contents of each component with the sample HTML below.

<h2>CRISIS CENTER</h2> <p>Get your crisis here</p><h2>HEROES</h2> <p>Get your heroes here</p>
      
      <h2>CRISIS CENTER</h2>
<p>Get your crisis here</p>
    

註冊 RouterRoutes

Register Router and Routes

為了使用 Router,你必須註冊來自 @angular/router 套件中的 RouterModule。定義一個路由陣列 appRoutes,並把它傳給 RouterModule.forRoot() 方法。RouterModule.forRoot() 方法會返回一個模組,其中包含配置好的 Router 服務提供者,以及路由函式庫所需的其它提供者。一旦啟動了應用,Router 就會根據當前的瀏覽器 URL 進行首次導航。

In order to use the Router, you must first register the RouterModule from the @angular/router package. Define an array of routes, appRoutes, and pass them to the RouterModule.forRoot() method. The RouterModule.forRoot() method returns a module that contains the configured Router service provider, plus other providers that the routing library requires. Once the application is bootstrapped, the Router performs the initial navigation based on the current browser URL.

注意: RouterModule.forRoot() 方法是用於註冊全應用級提供者的編碼模式。要詳細瞭解全應用級提供者,參見單例服務 一章。

Note: The RouterModule.forRoot() method is a pattern used to register application-wide providers. Read more about application-wide providers in the Singleton services guide.

import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { RouterModule, Routes } from '@angular/router'; import { AppComponent } from './app.component'; import { CrisisListComponent } from './crisis-list/crisis-list.component'; import { HeroListComponent } from './hero-list/hero-list.component'; const appRoutes: Routes = [ { path: 'crisis-center', component: CrisisListComponent }, { path: 'heroes', component: HeroListComponent }, ]; @NgModule({ imports: [ BrowserModule, FormsModule, RouterModule.forRoot( appRoutes, { enableTracing: true } // <-- debugging purposes only ) ], declarations: [ AppComponent, HeroListComponent, CrisisListComponent, ], bootstrap: [ AppComponent ] }) export class AppModule { }
src/app/app.module.ts (first-config)
      
      import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { RouterModule, Routes } from '@angular/router';

import { AppComponent } from './app.component';
import { CrisisListComponent } from './crisis-list/crisis-list.component';
import { HeroListComponent } from './hero-list/hero-list.component';

const appRoutes: Routes = [
  { path: 'crisis-center', component: CrisisListComponent },
  { path: 'heroes', component: HeroListComponent },
];

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    RouterModule.forRoot(
      appRoutes,
      { enableTracing: true } // <-- debugging purposes only
    )
  ],
  declarations: [
    AppComponent,
    HeroListComponent,
    CrisisListComponent,
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }
    

對於最小化的路由配置,把配置好的 RouterModule 新增到 AppModule 中就足夠了。但是,隨著應用的成長,你將需要將路由配置重構到單獨的檔案中,並建立路由模組,路由模組是一種特殊的、專做路由的服務模組

Adding the configured RouterModule to the AppModule is sufficient for minimal route configurations. However, as the application grows, refactor the routing configuration into a separate file and create a Routing Module. A routing module is a special type of Service Module dedicated to routing.

RouterModule.forRoot() 註冊到 AppModuleimports 陣列中,能讓該 Router 服務在應用的任何地方都能使用。

Registering the RouterModule.forRoot() in the AppModule imports array makes the Router service available everywhere in the application.

新增路由出口

Add the Router Outlet

根元件 AppComponent 是本應用的殼。它在頂部有一個標題、一個帶兩個連結的導覽列,在底部有一個路由器出口,路由器會在它所指定的位置上渲染各個元件。

The root AppComponent is the application shell. It has a title, a navigation bar with two links, and a router outlet where the router renders components.

路由出口扮演一個佔位符的角色,表示路由元件將會渲染到哪裡。

The router outlet serves as a placeholder where the routed components are rendered.

該元件所對應的範本是這樣的:

The corresponding component template looks like this:

<h1>Angular Router</h1> <nav> <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a> <a routerLink="/heroes" routerLinkActive="active">Heroes</a> </nav> <router-outlet></router-outlet>
src/app/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>
<router-outlet></router-outlet>
    

定義萬用字元路由

Define a Wildcard route

你以前在應用中建立過兩個路由,一個是 /crisis-center,另一個是 /heroes。 所有其它 URL 都會導致路由器丟擲錯誤,並讓應用崩潰。

You've created two routes in the app so far, one to /crisis-center and the other to /heroes. Any other URL causes the router to throw an error and crash the app.

可以新增一個萬用字元路由來攔截所有無效的 URL,並優雅的處理它們。 萬用字元路由的 path 是兩個星號(**),它會匹配任何 URL。 而當路由器匹配不上以前定義的那些路由時,它就會選擇這個萬用字元路由。 萬用字元路由可以導航到自訂的“404 Not Found”元件,也可以重新導向到一個現有路由。

Add a wildcard route to intercept invalid URLs and handle them gracefully. A wildcard route has a path consisting of two asterisks. It matches every URL. Thus, the router selects this wildcard route if it can't match a route earlier in the configuration. A wildcard route can navigate to a custom "404 Not Found" component or redirect to an existing route.

路由器會使用先到先得的策略來選擇路由。 由於萬用字元路由是最不具體的那個,因此務必確保它是路由配置中的最後一個路由。

The router selects the route with a first match wins strategy. Because a wildcard route is the least specific route, place it last in the route configuration.

要測試本特性,請往 HeroListComponent 的範本中新增一個帶 RouterLink 的按鈕,並且把它的連結設定為一個不存在的路由 "/sidekicks"

To test this feature, add a button with a RouterLink to the HeroListComponent template and set the link to a non-existant route called "/sidekicks".

<h2>HEROES</h2> <p>Get your heroes here</p> <button routerLink="/sidekicks">Go to sidekicks</button>
src/app/hero-list/hero-list.component.html (excerpt)
      
      <h2>HEROES</h2>
<p>Get your heroes here</p>

<button routerLink="/sidekicks">Go to sidekicks</button>
    

當用戶點選該按鈕時,應用就會失敗,因為你尚未定義過 "/sidekicks" 路由。

The application fails if the user clicks that button because you haven't defined a "/sidekicks" route yet.

不要新增 "/sidekicks" 路由,而是定義一個“萬用字元”路由,讓它導航到 PageNotFoundComponent 元件。

Instead of adding the "/sidekicks" route, define a wildcard route and have it navigate to a PageNotFoundComponent.

{ path: '**', component: PageNotFoundComponent }
src/app/app.module.ts (wildcard)
      
      { path: '**', component: PageNotFoundComponent }
    

建立 PageNotFoundComponent,以便在使用者訪問無效網址時顯示它。

Create the PageNotFoundComponent to display when users visit invalid URLs.

ng generate component page-not-found
      
      ng generate component page-not-found
    
<h2>Page not found</h2>
src/app/page-not-found.component.html (404 component)
      
      <h2>Page not found</h2>
    

現在,當用戶訪問 /sidekicks 或任何無效的 URL 時,瀏覽器就會顯示“Page not found”。 瀏覽器的位址列仍指向無效的 URL。

Now when the user visits /sidekicks, or any other invalid URL, the browser displays "Page not found". The browser address bar continues to point to the invalid URL.

設定跳轉

Set up redirects

應用啟動時,瀏覽器位址列中的初始 URL 預設是這樣的:

When the application launches, the initial URL in the browser bar is by default:

localhost:4200
      
      localhost:4200
    

它不能匹配上任何硬編碼進來的路由,於是就會走到萬用字元路由中去,並且顯示 PageNotFoundComponent

That doesn't match any of the hard-coded routes which means the router falls through to the wildcard route and displays the PageNotFoundComponent.

這個應用需要一個有效的預設路由,在這裡應該用英雄列表作為預設頁。當用戶點選"Heroes"連結或把 localhost:4200/heroes 貼上到位址列時,它應該導航到列表頁。

The application needs a default route to a valid page. The default page for this app is the list of heroes. The app should navigate there as if the user clicked the "Heroes" link or pasted localhost:4200/heroes into the address bar.

新增一個 redirect 路由,把最初的相對 URL('')轉換成所需的預設路徑(/heroes)。

Add a redirect route that translates the initial relative URL ('') to the desired default path (/heroes).

在萬用字元路由上方新增一個預設路由。 在下方的程式碼片段中,它出現在萬用字元路由的緊上方,展示了這個里程碑的完整 appRoutes

Add the default route somewhere above the wildcard route. It's just above the wildcard route in the following excerpt showing the complete appRoutes for this milestone.

const appRoutes: Routes = [ { path: 'crisis-center', component: CrisisListComponent }, { path: 'heroes', component: HeroListComponent }, { path: '', redirectTo: '/heroes', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ];
src/app/app-routing.module.ts (appRoutes)
      
      const appRoutes: Routes = [
  { path: 'crisis-center', component: CrisisListComponent },
  { path: 'heroes',        component: HeroListComponent },
  { path: '',   redirectTo: '/heroes', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent }
];
    

瀏覽器的位址列會顯示 .../heroes,好像你直接在那裡導航一樣。

The browser address bar shows .../heroes as if you'd navigated there directly.

重新導向路由需要一個 pathMatch 屬性,來告訴路由器如何用 URL 去匹配路由的路徑。 在本應用中,路由器應該只有在*完整的 URL_等於 '' 時才選擇 HeroListComponent 元件,因此要把 pathMatch 設定為 'full'

A redirect route requires a pathMatch property to tell the router how to match a URL to the path of a route. In this app, the router should select the route to the HeroListComponent only when the entire URL matches '', so set the pathMatch value to 'full'.

聚焦 pathMatch
Spotlight on pathMatch

從技術角度看,pathMatch = 'full' 會導致 URL 中剩下的、未匹配的部分必須等於 ''。 在這個例子中,跳轉路由在一個最上層路由中,因此剩下的_URL 和完整的_URL 是一樣的。

Technically, pathMatch = 'full' results in a route hit when the remaining, unmatched segments of the URL match ''. In this example, the redirect is in a top level route so the remaining URL and the entire URL are the same thing.

pathMatch 的另一個可能的值是 'prefix',它會告訴路由器:當*剩下的_URL 以這個跳轉路由中的 prefix 值開頭時,就會匹配上這個跳轉路由。 但這不適用於此示例應用,因為如果 pathMatch 值是 'prefix',那麼每個 URL 都會匹配 ''

The other possible pathMatch value is 'prefix' which tells the router to match the redirect route when the remaining URL begins with the redirect route's prefix path. This doesn't apply to this sample app because if the pathMatch value were 'prefix', every URL would match ''.

嘗試把它設定為 'prefix',並點選 Go to sidekicks 按鈕。這是因為它是一個無效 URL,本應顯示“Page not found”頁。 但是,你仍然在“英雄列表”頁中。在位址列中輸入一個無效的 URL,你又被路由到了 /heroes每一個 URL,無論有效與否,都會匹配上這個路由定義。

Try setting it to 'prefix' and clicking the Go to sidekicks button. Since that's a bad URL, you should see the "Page not found" page. Instead, you're still on the "Heroes" page. Enter a bad URL in the browser address bar. You're instantly re-routed to /heroes. Every URL, good or bad, that falls through to this route definition is a match.

預設路由應該只有在整個URL 等於 '' 時才重新導向到 HeroListComponent,別忘了把重新導向路由設定為 pathMatch = 'full'

The default route should redirect to the HeroListComponent only when the entire url is ''. Remember to restore the redirect to pathMatch = 'full'.

要了解更多,參見 Victor Savkin 的帖子關於重新導向

Learn more in Victor Savkin's post on redirects.

里程碑 1 小結

Milestone 1 wrap up

當用戶單擊某個連結時,該示例應用可以在兩個檢視之間切換。

Your sample app can switch between two views when the user clicks a link.

里程碑 1 涵蓋了以下幾點的做法:

Milestone 1 has covered how to do the following:

  • 載入路由函式庫

    Load the router library.

  • 往殼元件的範本中新增一個導覽列,導覽列中有一些 A 標籤、routerLink 指令和 routerLinkActive 指令

    Add a nav bar to the shell template with anchor tags, routerLink and routerLinkActive directives.

  • 往殼元件的範本中新增一個 router-outlet 指令,檢視將會被顯示在那裡

    Add a router-outlet to the shell template where views are displayed.

  • RouterModule.forRoot() 配置路由器模組

    Configure the router module with RouterModule.forRoot().

  • 設定路由器,使其合成 HTML5 模式的瀏覽器 URL

    Set the router to compose HTML5 browser URLs.

  • 使用萬用字元路由來處理無效路由

    Handle invalid routes with a wildcard route.

  • 當應用在空路徑下啟動時,導航到預設路由

    Navigate to the default route when the app launches with an empty path.

這個初學者應用的結構是這樣的:

The starter app's structure looks like this:

angular-router-sample
src
app
crisis-list

crisis-list.component.css

crisis-list.component.html

crisis-list.component.ts

hero-list

hero-list.component.css

hero-list.component.html

hero-list.component.ts

page-not-found

page-not-found.component.css

page-not-found.component.html

page-not-found.component.ts

app.component.css
app.component.html
app.component.ts

app.module.ts

main.ts

index.html

styles.css

tsconfig.json

node_modules ...

package.json

下面是本里程碑中的檔案列表:

Here are the files in this milestone.

<h1>Angular Router</h1> <nav> <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a> <a routerLink="/heroes" routerLinkActive="active">Heroes</a> </nav> <router-outlet></router-outlet>import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { RouterModule, Routes } from '@angular/router'; import { AppComponent } from './app.component'; import { CrisisListComponent } from './crisis-list/crisis-list.component'; import { HeroListComponent } from './hero-list/hero-list.component'; import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; const appRoutes: Routes = [ { path: 'crisis-center', component: CrisisListComponent }, { path: 'heroes', component: HeroListComponent }, { path: '', redirectTo: '/heroes', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ]; @NgModule({ imports: [ BrowserModule, FormsModule, RouterModule.forRoot( appRoutes, { enableTracing: true } // <-- debugging purposes only ) ], declarations: [ AppComponent, HeroListComponent, CrisisListComponent, PageNotFoundComponent ], bootstrap: [ AppComponent ] }) export class AppModule { }<h2>HEROES</h2> <p>Get your heroes here</p> <button routerLink="/sidekicks">Go to sidekicks</button><h2>CRISIS CENTER</h2> <p>Get your crisis here</p><h2>Page not found</h2><html lang="en"> <head> <!-- Set the base href --> <base href="/"> <title>Angular Router</title> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> </head> <body> <app-root></app-root> </body> </html>
      
      <h1>Angular Router</h1>
<nav>
  <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a>
  <a routerLink="/heroes" routerLinkActive="active">Heroes</a>
</nav>
<router-outlet></router-outlet>
    

里程碑 2:路由模組

Milestone 2: Routing module

這個里程碑會向你展示如何配置一個名叫路由模組的專用模組,它會儲存你應用的路由配置。

This milestone shows you how to configure a special-purpose module called a Routing Module, which holds your app's routing configuration.

路由模組有以下幾個特點:

The Routing Module has several characteristics:

  • 把路由這個關注點從其它應用類別關注點中分離出去。

    Separates routing concerns from other application concerns.

  • 測試特性模組時,可以替換或移除路由模組。

    Provides a module to replace or remove when testing the application.

  • 為路由服務提供者(如守衛和解析器等)提供一個眾所周知的位置。

    Provides a well-known location for routing service providers such as guards and resolvers.

  • 不要宣告元件。

    Does not declare components.

把路由整合到應用中

Integrate routing with your app

路由應用範例中預設不包含路由。 要想在使用 Angular CLI 建立專案時支援路由,請為專案或應用的每個 NgModule 設定 --routing 選項。 當你用 CLI 命令 ng new建立新專案或用 ng generate app命令建立新應用,請指定 --routing 選項。這會告訴 CLI 包含上 @angular/router 套件,並建立一個名叫 app-routing.module.ts 的檔案。 然後你就可以在新增到專案或應用中的任何 NgModule 中使用路由功能了。

The sample routing application does not include routing by default. When you use the Angular CLI to create a project that does use routing, set the --routing option for the project or app, and for each NgModule. When you create or initialize a new project (using the CLI ng newcommand) or a new app (using the ng generate appcommand), specify the --routing option. This tells the CLI to include the @angular/router npm package and create a file named app-routing.module.ts. You can then use routing in any NgModule that you add to the project or app.

比如,可以用下列命令產生帶路由的 NgModule。

For example, the following command generates an NgModule that can use routing.

ng generate module my-module --routing
      
      ng generate module my-module --routing
    

這將建立一個名叫 my-module-routing.module.ts 的獨立檔案,來儲存這個 NgModule 的路由資訊。 該檔案包含一個空的 Routes 物件,你可以使用一些指向各個元件和 NgModule 的路由來填充該物件。

This creates a separate file named my-module-routing.module.ts to store the NgModule's routes. The file includes an empty Routes object that you can fill with routes to different components and NgModules.

將路由配置重構為路由模組

Refactor the routing configuration into a routing module

/app 目錄下建立一個 AppRouting 模組,以包含路由配置。

Create an AppRouting module in the /app folder to contain the routing configuration.

ng generate module app-routing --module app --flat
      
      ng generate module app-routing --module app --flat
    

匯入 CrisisListComponentHeroListComponentPageNotFoundCompponent 元件,就像 app.module.ts 中那樣。然後把 Router 的匯入語句和路由配置以及 RouterModule.forRoot() 移入這個路由模組中。

Import the CrisisListComponent, HeroListComponent, and PageNotFoundComponent symbols just like you did in the app.module.ts. Then move the Router imports and routing configuration, including RouterModule.forRoot(), into this routing module.

把 Angular 的 RouterModule 新增到該模組的 exports 陣列中,以便再次匯出它。 透過再次匯出 RouterModule,當在 AppModule 中匯入了 AppRoutingModule 之後,那些宣告在 AppModule 中的元件就可以訪問路由指令了,比如 RouterLinkRouterOutlet

Re-export the Angular RouterModule by adding it to the module exports array. By re-exporting the RouterModule here, the components declared in AppModule have access to router directives such as RouterLink and RouterOutlet.

做完這些之後,該檔案變成了這樣:

After these steps, the file should look like this.

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'; import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; const appRoutes: Routes = [ { path: 'crisis-center', component: CrisisListComponent }, { path: 'heroes', component: HeroListComponent }, { path: '', redirectTo: '/heroes', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ]; @NgModule({ imports: [ RouterModule.forRoot( appRoutes, { enableTracing: true } // <-- debugging purposes only ) ], exports: [ RouterModule ] }) export class AppRoutingModule {}
src/app/app-routing.module.ts
      
      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';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';

const appRoutes: Routes = [
  { path: 'crisis-center', component: CrisisListComponent },
  { path: 'heroes',        component: HeroListComponent },
  { path: '',   redirectTo: '/heroes', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent }
];

@NgModule({
  imports: [
    RouterModule.forRoot(
      appRoutes,
      { enableTracing: true } // <-- debugging purposes only
    )
  ],
  exports: [
    RouterModule
  ]
})
export class AppRoutingModule {}
    

接下來,修改 app.module.ts 檔案,從 imports 陣列中移除 RouterModule.forRoot

Next, update the app.module.ts file by removing RouterModule.forRoot in the imports array.

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 { CrisisListComponent } from './crisis-list/crisis-list.component'; import { HeroListComponent } from './hero-list/hero-list.component'; import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; @NgModule({ imports: [ BrowserModule, FormsModule, AppRoutingModule ], declarations: [ AppComponent, HeroListComponent, CrisisListComponent, PageNotFoundComponent ], bootstrap: [ AppComponent ] }) export class AppModule { }
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 { CrisisListComponent } from './crisis-list/crisis-list.component';
import { HeroListComponent } from './hero-list/hero-list.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';

@NgModule({
  imports: [
    BrowserModule,
    FormsModule,
    AppRoutingModule
  ],
  declarations: [
    AppComponent,
    HeroListComponent,
    CrisisListComponent,
    PageNotFoundComponent
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }
    

稍後,本指南將向你展示如何建立多個路由模組,並以正確的順序匯入這些路由模組。

Later, this guide shows you how to create multiple routing modules and import those routing modules in the correct order.

應用繼續照常執行,你可以把路由模組作為將來每個模組維護路由配置的中心位置。

The application continues to work just the same, and you can use AppRoutingModule as the central place to maintain future routing configuration.

路由模組的優點

Benefits of a routing module

路由模組(通常稱為 AppRoutingModule )代替了根範本或特性模組中的路由模組。

The routing module, often called the AppRoutingModule, replaces the routing configuration in the root or feature module.

這種路由模組在你的應用不斷增長,以及配置中包括了專門的守衛和解析器服務時會非常有用。

The routing module is helpful as your app grows and when the configuration includes specialized guard and resolver services.

在配置很簡單時,一些開發者會跳過路由模組,並將路由配置直接混合在關聯模組中(比如 AppModule )。

Some developers skip the routing module when the configuration is minimal and merge the routing configuration directly into the companion module (for example, AppModule).

大多數應用都應該採用路由模組,以保持一致性。 它在配置複雜時,能確保程式碼乾淨。 它讓測試特性模組更加容易。 它的存在讓人一眼就能看出這個模組是帶路由的。 開發者可以很自然的從路由模組中查詢和擴充套件路由配置。

Most apps should implement a routing module for consistency. It keeps the code clean when configuration becomes complex. It makes testing the feature module easier. Its existence calls attention to the fact that a module is routed. It is where developers expect to find and expand routing configuration.

里程碑 3: 英雄特徵區

Milestone 3: Heroes feature

本里程碑涵蓋了以下內容:

This milestone covers the following:

  • 用模組把應用和路由組織為一些特性區。

    Organizing the app and routes into feature areas using modules.

  • 命令式的從一個元件導航到另一個

    Navigating imperatively from one component to another.

  • 透過路由傳遞必要資訊和可選資訊

    Passing required and optional information in route parameters.

這個示例應用在“英雄指南”課程的“服務”部分重新建立了英雄特性區,並複用了Tour of Heroes: Services example code / 下載範例中的大部分程式碼。

This sample app recreates the heroes feature in the "Services" section of the Tour of Heroes tutorial, and reuses much of the code from theTour of Heroes: Services example code / 下載範例.

典型的應用具有多個特性區,每個特性區都專注於特定的業務用途並擁有自己的資料夾。

A typical application has multiple feature areas, each dedicated to a particular business purpose with its own folder.

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

This section shows you how refactor the app into different feature modules, import them into the main module and navigate among them.

新增英雄管理功能

Add heroes functionality

遵循下列步驟:

Follow these steps:

  • 為了管理這些英雄,在 heroes 目錄下建立一個帶路由的 HeroesModule,並把它註冊到根模組 AppModule 中。

    To manage the heroes, create a HeroesModule with routing in the heroes folder and register it with the root AppModule.

ng generate module heroes/heroes --module app --flat --routing
      
      ng generate module heroes/heroes --module app --flat --routing
    
  • app 下佔位用的 hero-list 目錄移到 heroes 目錄中。

    Move the placeholder hero-list folder that's in the app folder into the heroes folder.

  • 課程的 "服務" 部分課程的 "服務" 部分 / 下載範例heroes/heroes.component.html 的內容複製到 hero-list.component.html 範本中。

    Copy the contents of the heroes/heroes.component.html from the"Services" tutorial"Services" tutorial / 下載範例into the hero-list.component.html template.

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

      Re-label the <h2> to <h2>HEROES</h2>.

    • 刪除範本底部的 <app-hero-detail> 元件。

      Delete the <app-hero-detail> component at the bottom of the template.

  • 把現場演練中 heroes/heroes.component.css 檔案的內容複製到 hero-list.component.css 檔案中。

    Copy the contents of the heroes/heroes.component.css from the live example into the hero-list.component.css file.

  • 把現場演練中 heroes/heroes.component.ts 檔案的內容複製到 hero-list.component.ts 檔案中。

    Copy the contents of the heroes/heroes.component.ts from the live example into the hero-list.component.ts file.

    • 把元件類別名稱改為 HeroListComponent

      Change the component class name to HeroListComponent.

    • selector 改為 app-hero-list

      Change the selector to app-hero-list.

對於路由元件來說,這些選擇器不是必須的,因為這些元件是在渲染頁面時動態插入的,不過選擇器對於在 HTML 元素樹中標記和選中它們是很有用的。

Selectors are not required for routed components because components are dynamically inserted when the page is rendered. However, they are useful for identifying and targeting them in your HTML element tree.

  • hero-detail 目錄中的 hero.tshero.service.tsmock-heroes.ts 檔案複製到 heroes 子目錄下。

    Copy the hero-detail folder, the hero.ts, hero.service.ts, and mock-heroes.ts files into the heroes subfolder.

  • message.service.ts 檔案複製到 src/app 目錄下。

    Copy the message.service.ts into the src/app folder.

  • hero.service.ts 檔案中修改匯入 message.service 的相對路徑。

    Update the relative path import to the message.service in the hero.service.ts file.

接下來,更新 HeroesModule 的元資料。

Next, update the HeroesModule metadata.

  • 匯入 HeroDetailComponentHeroListComponent,並新增到 HeroesModule 模組的 declarations 陣列中。

    Import and add the HeroDetailComponent and HeroListComponent to the declarations array in the HeroesModule.

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

英雄管理部分的檔案結構如下:

The hero management file structure is as follows:

src/app/heroes

hero-detail
hero-detail.component.css
hero-detail.component.html
hero-detail.component.ts
hero-list
hero-list.component.css
hero-list.component.html
hero-list.component.ts

hero.service.ts

hero.ts
heroes-routing.module.ts

heroes.module.ts

mock-heroes.ts

英雄特性區的路由需求

Hero feature routing requirements

英雄特性區中有兩個相互協作的元件:英雄列表和英雄詳情。當你導航到列表檢視時,它會獲取英雄列表並顯示出來。當你點選一個英雄時,詳細檢視就會顯示那個特定的英雄。

The heroes feature has two interacting components, the hero list and the hero detail. When you navigate to list view, it gets a list of heroes and displays them. When you click on a hero, the detail view has to display that particular hero.

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

You tell the detail view which hero to display by including the selected hero's id in the route URL.

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

Import the hero components from their new locations in the src/app/heroes/ folder and define the two hero routes.

現在,你有了 Heroes 模組的路由,還得在 RouterModule 中把它們註冊給路由器,和 AppRoutingModule 中的做法幾乎完全一樣,只有一項重要的差別。

Now that you have routes for the Heroes module, register them with the Router via the RouterModule as you did in the AppRoutingModule, with an important difference.

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

In the AppRoutingModule, you used the static RouterModule.forRoot() method to register the routes and application level service providers. In a feature module you use the static forChild() method.

只在根模組 AppRoutingModule 中呼叫 RouterModule.forRoot()(如果在 AppModule 中註冊應用的最上層路由,那就在 AppModule 中呼叫)。 在其它模組中,你就必須呼叫 RouterModule.forChild 方法來註冊附屬路由。

Only call RouterModule.forRoot() in the root AppRoutingModule (or the AppModule if that's where you register top level application routes). In any other module, you must call the RouterModule.forChild() method to register additional routes.

修改後的 HeroesRoutingModule 是這樣的:

The updated HeroesRoutingModule looks like this:

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

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

Consider giving each feature module its own route configuration file. Though the feature routes are currently minimal, routes have a tendency to grow more complex even in small apps.

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

Remove duplicate hero routes

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

The hero routes are currently defined in two places: in the HeroesRoutingModule, by way of the HeroesModule, and in the AppRoutingModule.

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

Routes provided by feature modules are combined together into their imported module's routes by the router. This allows you to continue defining the feature module routes without modifying the main route configuration.

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

Remove the HeroListComponent import and the /heroes route from the app-routing.module.ts.

保留預設路由和萬用字元路由,因為這些路由仍然要在應用的最上層使用。

Leave the default and the wildcard routes as these are still in use at the top level of the application.

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

移除英雄列表的宣告

Remove heroes declarations

因為 HeroesModule 現在提供了 HeroListComponent,所以把它從 AppModuledeclarations 陣列中移除。現在你已經有了一個單獨的 HeroesModule,你可以用更多的元件和不同的路由來演進英雄特性區。

Because the HeroesModule now provides the HeroListComponent, remove it from the AppModule's declarations array. Now that you have a separate HeroesModule, you can evolve the hero feature with more components and different routes.

經過這些步驟,AppModule 變成了這樣:

After these steps, the AppModule should look like this:

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

模組匯入順序

Module import order

請注意該模組的 imports 陣列,AppRoutingModule 是最後一個,並且位於 HeroesModule 之後。

Notice that in the module imports array, the AppRoutingModule is last and comes after the HeroesModule.

imports: [ BrowserModule, FormsModule, HeroesModule, AppRoutingModule ],
src/app/app.module.ts (module-imports)
      
      imports: [
  BrowserModule,
  FormsModule,
  HeroesModule,
  AppRoutingModule
],
    

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

The order of route configuration is important because the router accepts the first route that matches a navigation request path.

當所有路由都在同一個 AppRoutingModule 時,你要把預設路由和萬用字元路由放在最後(這裡是在 /heroes 路由後面), 這樣路由器才有機會匹配到 /heroes 路由,否則它就會先遇到並匹配上該萬用字元路由,並導航到“頁面未找到”路由。

When all routes were in one AppRoutingModule, you put the default and wildcard routes last, after the /heroes route, so that the router had a chance to match a URL to the /heroes route before hitting the wildcard route and navigating to "Page not found".

每個路由模組都會根據匯入的順序把自己的路由配置追加進去。 如果你先列出了 AppRoutingModule,那麼萬用字元路由就會被註冊在“英雄管理”路由之前。 萬用字元路由(它匹配任意URL)將會攔截住每一個到“英雄管理”路由的導航,因此事實上遮蔽了所有“英雄管理”路由。

Each routing module augments the route configuration in the order of import. If you listed AppRoutingModule first, the wildcard route would be registered before the hero routes. The wildcard route—which matches every URL—would intercept the attempt to navigate to a hero route.

反轉路由模組的匯入順序,就會看到當點選英雄相關的連結時被導向了“頁面未找到”路由。 要學習如何在執行時檢視路由器配置,參見稍後的內容

Reverse the routing modules to see a click of the heroes link resulting in "Page not found". Learn about inspecting the runtime router configuration below.

路由引數

Route Parameters

帶引數的路由定義

Route definition with a parameter

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

Return to the HeroesRoutingModule and look at the route definitions again. The route to HeroDetailComponent has an :id token in the path.

{ path: 'hero/:id', component: HeroDetailComponent }
src/app/heroes/heroes-routing.module.ts (excerpt)
      
      { path: 'hero/:id', component: HeroDetailComponent }
    

:id 令牌會為路由引數在路徑中建立一個“空位”。在這裡,這種配置會讓路由器把英雄的 id 插入到那個“空位”中。

The :id token creates a slot in the path for a Route Parameter. In this case, this configuration causes the router to insert the id of a hero into that slot.

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

If you tell the router to navigate to the detail component and display "Magneta", you expect a hero id to appear in the browser URL like this:

localhost:4200/hero/15
      
      localhost:4200/hero/15
    

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

If a user enters that URL into the browser address bar, the router should recognize the pattern and go to the same "Magneta" detail view.

路由引數:必須的還是可選的?
Route parameter: Required or optional?

在這個場景下,把路由引數的令牌 :id 嵌入到路由定義的 path 中是一個好主意,因為對於 HeroDetailComponent 來說 id必須的, 而且路徑中的值 15 已經足夠把到“Magneta”的路由和到其它英雄的路由明確區分開。

Embedding the route parameter token, :id, in the route definition path is a good choice for this scenario because the id is required by the HeroDetailComponent and because the value 15 in the path clearly distinguishes the route to "Magneta" from a route for some other hero.

在列表檢視中設定路由引數

Setting the route parameters in the list view

然後導航到 HeroDetailComponent 元件。在那裡,你期望看到所選英雄的詳情,這需要兩部分資訊:導航目標和該英雄的 id

After navigating to the HeroDetailComponent, you expect to see the details of the selected hero. You need two pieces of information: the routing path to the component and the hero's id.

因此,這個連結引數陣列中有兩個條目:路由的路徑和一個用來指定所選英雄 id路由引數

Accordingly, the link parameters array has two items: the routing path and a route parameter that specifies the id of the selected hero.

<a [routerLink]="['/hero', hero.id]">
src/app/heroes/hero-list/hero-list.component.html (link-parameters-array)
      
      <a [routerLink]="['/hero', hero.id]">
    

路由器從該陣列中組合出了目標 URL: localhost:3000/hero/15

The router composes the destination URL from the array like this: localhost:4200/hero/15.

路由器從 URL 中解析出路由引數(id:15),並透過 ActivatedRoute 服務來把它提供給 HeroDetailComponent 元件。

The router extracts the route parameter (id:15) from the URL and supplies it to the HeroDetailComponent via the ActivatedRoute service.

ActivatedRoute 實戰

Activated Route in action

從路由器(router)套件中匯入 RouterActivatedRouteParams 類別。

Import the Router, ActivatedRoute, and ParamMap tokens from the router package.

import { Router, ActivatedRoute, ParamMap } from '@angular/router';
src/app/heroes/hero-detail/hero-detail.component.ts (activated route)
      
      import { Router, ActivatedRoute, ParamMap } from '@angular/router';
    

這裡匯入 switchMap 運算子是因為你稍後將會處理路由引數的可觀察物件 Observable

Import the switchMap operator because you need it later to process the Observable route parameters.

import { switchMap } from 'rxjs/operators';
src/app/heroes/hero-detail/hero-detail.component.ts (switchMap operator import)
      
      import { switchMap } from 'rxjs/operators';
    

把這些服務作為私有變數新增到建構函式中,以便 Angular 注入它們(讓它們對元件可見)。

Add the services as private variables to the constructor so that Angular injects them (makes them visible to the component).

constructor( private route: ActivatedRoute, private router: Router, private service: HeroService ) {}
src/app/heroes/hero-detail/hero-detail.component.ts (constructor)
      
      constructor(
  private route: ActivatedRoute,
  private router: Router,
  private service: HeroService
) {}
    

ngOnInit() 方法中,使用 ActivatedRoute 服務來檢索路由的引數,從引數中提取出英雄的 id,並檢索要顯示的英雄。

In the ngOnInit() method, use the ActivatedRoute service to retrieve the parameters for the route, pull the hero id from the parameters, and retrieve the hero to display.

ngOnInit() { this.hero$ = this.route.paramMap.pipe( switchMap((params: ParamMap) => this.service.getHero(params.get('id'))) ); }
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')))
  );
}
    

當這個 map 發生變化時,paramMap 會從更改後的引數中獲取 id 引數。

When the map changes, paramMap gets the id parameter from the changed parameters.

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

Then you tell the HeroService to fetch the hero with that id and return the result of the HeroService request.

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

The switchMap operator does two things. It flattens the Observable<Hero> that HeroService returns and cancels previous pending requests. If the user re-navigates to this route with a new id while the HeroService is still retrieving the old id, switchMap discards that old request and returns the hero for the new id.

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

AsyncPipe handles the observable subscription and the component's hero property will be (re)set with the retrieved hero.

ParamMap API

ParamMap API 的靈感來自於 URLSearchParams 介面。它提供了處理路由引數( paramMap )和查詢引數( queryParamMap )訪問的方法。

The ParamMap API is inspired by the URLSearchParams interface. It provides methods to handle parameter access for both route parameters (paramMap) and query parameters (queryParamMap).

成員

Member

說明

Description

has(name)

如果引數名位於引數列表中,就返回 true

Returns true if the parameter name is in the map of parameters.

get(name)

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

Returns the parameter name value (a string) if present, or null if the parameter name is not in the map. Returns the first element if the parameter value is actually an array of values.

getAll(name)

如果這個 map 中有引數名對應的值,就返回一個字串陣列,否則返回空陣列。當一個引數名可能對應多個值的時候,請使用 getAll

Returns a string array of the parameter name value if found, or an empty array if the parameter name value is not in the map. Use getAll when a single parameter could have multiple values.

keys

返回這個 map 中的所有引數名組成的字串陣列。

Returns a string array of all parameter names in the map.

引數的可觀察物件(Observable)與元件複用

Observable paramMap and component reuse

在這個例子中,你接收了路由引數的 Observable 物件。 這種寫法暗示著這些路由引數在該元件的生存期內可能會變化。

In this example, you retrieve the route parameter map from an Observable. That implies that the route parameter map can change during the lifetime of this component.

預設情況下,如果它沒有訪問過其它元件就導航到了同一個元件實例,那麼路由器傾向於複用元件實例。如果複用,這些引數可以變化。

By default, the router re-uses a component instance when it re-navigates to the same component type without visiting a different component first. The route parameters could change each time.

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

Suppose a parent component navigation bar had "forward" and "back" buttons that scrolled through the list of heroes. Each click navigated imperatively to the HeroDetailComponent with the next or previous id.

你肯定不希望路由器先從 DOM 中移除當前的 HeroDetailComponent 實例,只是為了用下一個 id 重新建立它,因為它將重新渲染檢視。為了更好的使用者體驗,路由器會複用同一個元件實例,而只是更新引數。

You wouldn't want the router to remove the current HeroDetailComponent instance from the DOM only to re-create it for the next id as this would re-render the view. For better UX, the router re-uses the same component instance and updates the parameter.

由於 ngOnInit() 在每個元件實例化時只會被呼叫一次,所以你可以使用 paramMap 可觀察物件來檢測路由引數在同一個實例中何時發生了變化。

Since ngOnInit() is only called once per component instantiation, you can detect when the route parameters change from within the same instance using the observable paramMap property.

當在元件中訂閱一個可觀察物件時,你通常總是要在元件銷燬時取消這個訂閱。

When subscribing to an observable in a component, you almost always unsubscribe when the component is destroyed.

不過,ActivatedRoute 中的可觀察物件是一個例外,因為 ActivatedRoute 及其可觀察物件與 Router 本身是隔離的。 Router 會在不再需要時銷燬這個路由元件,而注入進去的 ActivateRoute 也隨之銷燬了。

However, ActivatedRoute observables are among the exceptions because ActivatedRoute and its observables are insulated from the Router itself. The Router destroys a routed component when it is no longer needed along with the injected ActivatedRoute.

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

snapshot: the no-observable alternative

本應用不需要複用 HeroDetailComponent。 使用者總是會先返回英雄列表,再選擇另一位英雄。 所以,不存在從一個英雄詳情導航到另一個而不用經過英雄列表的情況。 這意味著路由器每次都會建立一個全新的 HeroDetailComponent 實例。

This application won't re-use the HeroDetailComponent. The user always returns to the hero list to select another hero to view. There's no way to navigate from one hero detail to another hero detail without visiting the list component in between. Therefore, the router creates a new HeroDetailComponent instance every time.

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

When you know for certain that a HeroDetailComponent instance will never be re-used, you can use snapshot.

route.snapshot 提供了路由引數的初始值。 你可以透過它來直接訪問引數,而不用訂閱或者新增 Observable 的運算子,程式碼如下:

route.snapshot provides the initial value of the route parameter map. You can access the parameters directly without subscribing or adding observable operators as in the following:

ngOnInit() { const id = this.route.snapshot.paramMap.get('id'); this.hero$ = this.service.getHero(id); }
src/app/heroes/hero-detail/hero-detail.component.ts (ngOnInit snapshot)
      
      ngOnInit() {
  const id = this.route.snapshot.paramMap.get('id');

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

用這種技術,snapshot 只會得到這些引數的初始值。如果路由器可能複用該元件,那麼就該用 paramMap 可觀察物件的方式。本課程的示例應用中就用了 paramMap 可觀察物件。

snapshot only gets the initial value of the parameter map with this technique. Use the observable paramMap approach if there's a possibility that the router could re-use the component. This tutorial sample app uses with the observable paramMap.

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

The HeroDetailComponent "Back" button uses the gotoHeroes() method that navigates imperatively back to the HeroListComponent.

路由的 navigate() 方法同樣接受一個單條目的連結引數陣列,你也可以把它繫結到 [routerLink] 指令上。 它儲存著到 HeroListComponent 元件的路徑:

The router navigate() method takes the same one-item link parameters array that you can bind to a [routerLink] directive. It holds the path to the HeroListComponent:

gotoHeroes() { this.router.navigate(['/heroes']); }
src/app/heroes/hero-detail/hero-detail.component.ts (excerpt)
      
      gotoHeroes() {
  this.router.navigate(['/heroes']);
}
    

路由引數:必須還是可選?

Route Parameters: Required or optional?

如果想導航到 HeroDetailComponent 以對 id 為 15 的英雄進行檢視並編輯,就要在路由的 URL 中使用路由引數來指定必要引數值。

Use route parameters to specify a required parameter value within the route URL as you do when navigating to the HeroDetailComponent in order to view the hero with id 15:

localhost:4200/hero/15
      
      localhost:4200/hero/15
    

你也能在路由請求中新增可選資訊。 比如,當從 hero-detail.component.ts 返回到列表時,如果能自動選中剛剛檢視過的英雄就好了。

You can also add optional information to a route request. For example, when returning to the hero-detail.component.ts list from the hero detail view, it would be nice if the viewed hero were preselected in the list.

當從 HeroDetailComponent 返回時,你可以會透過把正在檢視的英雄的 id 作為可選引數包含在 URL 中來實現這個特性。

You implement this feature by including the viewed hero's id in the URL as an optional parameter when returning from the HeroDetailComponent.

可選資訊還可以包含其它形式,例如:

Optional information can also include other forms such as:

  • 結構鬆散的搜尋條件。比如 name='wind_'

    Loosely structured search criteria; for example, name='wind*'.

  • 多個值。比如 after='12/31/2015' & before='1/1/2017' - 沒有特定的順序 - before='1/1/2017' & after='12/31/2015' - 具有各種格式 - during='currentYear'

    Multiple values; for example, after='12/31/2015' & before='1/1/2017'—in no particular order—before='1/1/2017' & after='12/31/2015'— in a variety of formats—during='currentYear'.

由於這些引數不適合用作 URL 路徑,因此可以使用可選引數在導航過程中傳遞任意複雜的資訊。可選引數不參與模式匹配,因此在表達上提供了巨大的靈活性。

As these kinds of parameters don't fit easily in a URL path, you can use optional parameters for conveying arbitrarily complex information during navigation. Optional parameters aren't involved in pattern matching and afford flexibility of expression.

和必要引數一樣,路由器也支援透過可選引數導航。 在你定義完必要引數之後,再透過一個獨立的物件來定義可選引數。

The router supports navigation with optional parameters as well as required route parameters. Define optional parameters in a separate object after you define the required route parameters.

通常,對於必傳的值(比如用於區分兩個路由路徑的)使用必備引數;當這個值是可選的、複雜的或多值的時,使用可選引數。

In general, use a required route parameter when the value is mandatory (for example, if necessary to distinguish one route path from another); and an optional parameter when the value is optional, complex, and/or multivariate.

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

Heroes list: optionally selecting a hero

當導航到 HeroDetailComponent 時,你可以在路由引數中指定一個所要編輯的英雄 id,只要把它作為連結引數陣列中的第二個條目就可以了。

When navigating to the HeroDetailComponent you specified the required id of the hero-to-edit in the route parameter and made it the second item of the link parameters array.

<a [routerLink]="['/hero', hero.id]">
src/app/heroes/hero-list/hero-list.component.html (link-parameters-array)
      
      <a [routerLink]="['/hero', hero.id]">
    

路由器在導航 URL 中內嵌了 id 的值,這是因為你把它用一個 :id 佔位符當做路由引數定義在了路由的 path 中:

The router embedded the id value in the navigation URL because you had defined it as a route parameter with an :id placeholder token in the route path:

{ path: 'hero/:id', component: HeroDetailComponent }
src/app/heroes/heroes-routing.module.ts (hero-detail-route)
      
      { path: 'hero/:id', component: HeroDetailComponent }
    

當用戶點選後退按鈕時,HeroDetailComponent 構造了另一個連結引數陣列,可以用它導航回 HeroListComponent

When the user clicks the back button, the HeroDetailComponent constructs another link parameters array which it uses to navigate back to the HeroListComponent.

gotoHeroes() { this.router.navigate(['/heroes']); }
src/app/heroes/hero-detail/hero-detail.component.ts (gotoHeroes)
      
      gotoHeroes() {
  this.router.navigate(['/heroes']);
}
    

該陣列缺少一個路由引數,這是因為以前你不需要往 HeroListComponent 傳送資訊。

This array lacks a route parameter because previously you didn't need to send information to the HeroListComponent.

現在,使用導航請求傳送當前英雄的 id,以便 HeroListComponent 在其列表中突出顯示該英雄。

Now, send the id of the current hero with the navigation request so that the HeroListComponent can highlight that hero in its list.

傳送一個包含可選 id 引數的物件。 為了示範,這裡還在物件中定義了一個沒用的額外引數(foo),HeroListComponent 應該忽略它。 下面是修改過的導航語句:

Send the id with an object that contains an optional id parameter. For demonstration purposes, there's an extra junk parameter (foo) in the object that the HeroListComponent should ignore. Here's the revised navigation statement:

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' }]); }
src/app/heroes/hero-detail/hero-detail.component.ts (go to heroes)
      
      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”按鈕返回英雄列表檢視。

The application still works. Clicking "back" returns to the hero list view.

注意瀏覽器的位址列。

Look at the browser address bar.

它應該是這樣的,不過也取決於你在哪裡執行它:

It should look something like this, depending on where you run it:

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

id 的值像這樣出現在 URL 中(;id=15;foo=foo),但不在 URL 的路徑部分。 “Heroes”路由的路徑部分並沒有定義 :id

The id value appears in the URL as (;id=15;foo=foo), not in the URL path. The path for the "Heroes" route doesn't have an :id token.

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

The optional route parameters are not separated by "?" and "&" as they would be in the URL query string. They are separated by semicolons ";". This is matrix URL notation.

Matrix URL 寫法首次提出是在1996 提案中,提出者是 Web 的奠基人:Tim Berners-Lee。

Matrix URL notation is an idea first introduced in a 1996 proposal by the founder of the web, Tim Berners-Lee.

雖然 Matrix 寫法未曾進入過 HTML 標準,但它是合法的。而且在瀏覽器的路由系統中,它作為從父路由和子路由中單獨隔離出引數的方式而廣受歡迎。Angular 的路由器正是這樣一個路由系統,並支援跨瀏覽器的 Matrix 寫法。

Although matrix notation never made it into the HTML standard, it is legal and it became popular among browser routing systems as a way to isolate parameters belonging to parent and child routes. As such, the Router provides support for the matrix notation across browsers.

ActivatedRoute 服務中的路由引數

Route parameters in the ActivatedRoute service

開發到現在,英雄列表還沒有變化。沒有突出顯示的英雄行。

In its current state of development, the list of heroes is unchanged. No hero row is highlighted.

HeroListComponent 需要新增使用這些引數的程式碼。

The HeroListComponent needs code that expects parameters.

以前,當從 HeroListComponent 導航到 HeroDetailComponent 時,你透過 ActivatedRoute 服務訂閱了路由引數這個 Observable,並讓它能用在 HeroDetailComponent 中。 你把該服務注入到了 HeroDetailComponent 的建構函式中。

Previously, when navigating from the HeroListComponent to the HeroDetailComponent, you subscribed to the route parameter map Observable and made it available to the HeroDetailComponent in the ActivatedRoute service. You injected that service in the constructor of the HeroDetailComponent.

這次,你要進行反向導航,從 HeroDetailComponentHeroListComponent

This time you'll be navigating in the opposite direction, from the HeroDetailComponent to the HeroListComponent.

首先,擴充套件該路由的匯入語句,以包含進 ActivatedRoute 服務的類別;

First, extend the router import statement to include the ActivatedRoute service symbol:

import { ActivatedRoute } from '@angular/router';
src/app/heroes/hero-list/hero-list.component.ts (import)
      
      import { ActivatedRoute } from '@angular/router';
    

匯入 switchMap 運算子,在路由引數的 Observable 物件上執行操作。

Import the switchMap operator to perform an operation on the Observable of route parameter map.

import { Observable } from 'rxjs'; import { switchMap } from 'rxjs/operators';
src/app/heroes/hero-list/hero-list.component.ts (rxjs imports)
      
      import { Observable } from 'rxjs';
import { switchMap } from 'rxjs/operators';
    

HeroListComponent 建構函式中注入 ActivatedRoute

Inject the ActivatedRoute in the HeroListComponent constructor.

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(); }) ); } }
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 屬性是一個路由引數的 Observable。當用戶導航到這個元件時,paramMap 會發射一個新值,其中包含 id。 在 ngOnInit() 中,你訂閱了這些值,設定到 selectedId,並獲取英雄資料。

The ActivatedRoute.paramMap property is an Observable map of route parameters. The paramMap emits a new map of values that includes id when the user navigates to the component. In ngOnInit() you subscribe to those values, set the selectedId, and get the heroes.

CSS 類別繫結更新範本,把它繫結到 isSelected 方法上。 如果該方法返回 true,此繫結就會新增 CSS 類別 selected,否則就移除它。 在 <li> 標記中找到它,就像這樣:

Update the template with a class binding. The binding adds the selected CSS class when the comparison returns true and removes it when false. Look for it within the repeated <li> tag as shown here:

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

當選中列表條目時,要新增一些樣式。

Add some styles to apply when the list item is selected.

.heroes li.selected { background-color: #CFD8DC; color: white; } .heroes li.selected:hover { background-color: #BBD8DC; }
src/app/heroes/hero-list/hero-list.component.css
      
      .heroes li.selected {
  background-color: #CFD8DC;
  color: white;
}
.heroes li.selected:hover {
  background-color: #BBD8DC;
}
    

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

When the user navigates from the heroes list to the "Magneta" hero and back, "Magneta" appears selected:

這個可選的 foo 路由引數人畜無害,路由器會繼續忽略它。

The optional foo route parameter is harmless and the router continues to ignore it.

新增路由動畫

Adding routable animations

在這一節,你將為英雄詳情元件新增一些動畫

This section shows you how to add some animations to the HeroDetailComponent.

首先匯入 BrowserAnimationsModule,並新增到 imports 陣列中:

First, import the BrowserAnimationsModule and add it to the imports array:

import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; @NgModule({ imports: [ BrowserAnimationsModule, ], })
src/app/app.module.ts (animations-module)
      
      import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

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

接下來,為指向 HeroListComponentHeroDetailComponent 的路由定義新增一個 data 物件。 轉場是基於 states 的,你將使用來自路由的 animation 資料為轉場提供一個有名字的動畫 state

Next, add a data object to the routes for HeroListComponent and HeroDetailComponent. Transitions are based on states and you use the animation data from the route to provide a named animation state for the transitions.

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/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/ 下建立一個 animations.ts。內容如下:

Create an animations.ts file in the root src/app/ folder. The contents look like this:

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

該檔案做了如下工作:

This file does the following:

  • 匯入動畫符號以建構動畫觸發器、控制狀態並管理狀態之間的過渡。

    Imports the animation symbols that build the animation triggers, control state, and manage transitions between states.

  • 匯出了一個名叫 slideInAnimation 的常量,並把它設定為一個名叫 routeAnimation 的動畫觸發器。

    Exports a constant named slideInAnimation set to an animation trigger named routeAnimation.

  • 定義一個轉場動畫,當在 heroeshero 路由之間來回切換時,如果進入(:enter)應用檢視則讓元件從螢幕的左側滑入,如果離開(:leave)應用檢視則讓元件從右側劃出。

    Defines one transition when switching back and forth from the heroes and hero routes to ease the component in from the left of the screen as it enters the application view (:enter), the other to animate the component to the right as it leaves the application view (:leave).

回到 AppComponent,從 @angular/router 套件匯入 RouterOutlet,並從 './animations.ts 匯入 slideInAnimation

Back in the AppComponent, import the RouterOutlet token from the @angular/router package and the slideInAnimation from './animations.ts.

為包含 slideInAnimation@Component 元資料新增一個 animations 陣列。

Add an animations array to the @Component metadata that contains the slideInAnimation.

import { RouterOutlet } from '@angular/router'; import { slideInAnimation } from './animations'; @Component({ selector: 'app-root', templateUrl: 'app.component.html', styleUrls: ['app.component.css'], animations: [ slideInAnimation ] })
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 ]
})
    

要想使用路由動畫,就要把 RouterOutlet 包裝到一個元素中。再把 @routeAnimation 觸發器繫結到該元素上。

In order to use the routable animations, wrap the RouterOutlet inside an element, use the @routeAnimation trigger, and bind it to the element.

為了把 @routeAnimation 轉場轉場到指定的狀態,你需要從 ActivatedRoutedata 中提供它。 RouterOutlet 匯出成了一個範本變數 outlet,這樣你就可以繫結一個到路由出口的參考了。這個例子中使用了一個 routerOutlet 變數。

For the @routeAnimation transitions to key off states, provide it with the data from the ActivatedRoute. The RouterOutlet is exposed as an outlet template variable, so you bind a reference to the router outlet. This example uses a variable of routerOutlet.

<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>
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 中定義那個函式。getAnimationData 函式會根據 ActivatedRoute 所提供的 data 物件返回動畫的屬性。animation 屬性會根據你在 animations.ts 中定義 slideInAnimation() 時使用的 transition 名稱進行匹配。

The @routeAnimation property is bound to the getAnimationData() with the provided routerOutlet reference, so the next step is to define that function in the AppComponent. The getAnimationData() function returns the animation property from the data provided through the ActivatedRoute. The animation property matches the transition names you used in the slideInAnimation defined in animations.ts.

export class AppComponent { getAnimationData(outlet: RouterOutlet) { return outlet && outlet.activatedRouteData && outlet.activatedRouteData.animation; } }
src/app/app.component.ts (router outlet)
      
      export class AppComponent {
  getAnimationData(outlet: RouterOutlet) {
    return outlet && outlet.activatedRouteData && outlet.activatedRouteData.animation;
  }
}
    

如果在兩個路由之間切換,導航進來時,HeroDetailComponentHeroListComponent 會從左側滑入;導航離開時將會從右側劃出。

When switching between the two routes, the HeroDetailComponent and HeroListComponent now ease in from the left when routed to and will slide to the right when navigating away.

里程碑 3 的小結

Milestone 3 wrap up

本節包括以下內容:

This section has covered the following:

  • 把應用組織成特性區

    Organizing the app into feature areas.

  • 命令式的從一個元件導航到另一個

    Navigating imperatively from one component to another.

  • 透過路由引數傳遞資訊,並在元件中訂閱它們

    Passing information along in route parameters and subscribe to them in the component.

  • 把這個特性分區模組匯入根模組 AppModule

    Importing the feature area NgModule into the AppModule.

  • 把動畫應用到路由元件上

    Applying routable animations based on the page.

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

After these changes, the folder structure is as follows:

angular-router-sample

src
app
crisis-list
crisis-list.component.css
crisis-list.component.html
crisis-list.component.ts

heroes

hero-detail
hero-detail.component.css
hero-detail.component.html
hero-detail.component.ts
hero-list
hero-list.component.css
hero-list.component.html
hero-list.component.ts
hero.service.ts
hero.ts
heroes-routing.module.ts
heroes.module.ts
mock-heroes.ts
page-not-found

page-not-found.component.css

page-not-found.component.html

page-not-found.component.ts

animations.ts
app.component.css
app.component.html
app.component.ts
app.module.ts
app-routing.module.ts
main.ts

message.service.ts

index.html

styles.css

tsconfig.json

node_modules ...

package.json

這裡是當前版本的範例程式相關檔案。

Here are the relevant files for this version of the sample application.

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) { 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' }]); } } /* 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 = []; } }
      
      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()),
    ])
  ]);
    

里程碑 4:危機中心

Milestone 4: Crisis center feature

本節將向你展示如何在應用中新增子路由並使用相對路由。

This section shows you how to add child routes and use relative routing in your app.

要為應用當前的危機中心新增更多特性,請執行類似於 heroes 特性的步驟:

To add more features to the app's current crisis center, take similar steps as for the heroes feature:

  • src/app 目錄下建立一個 crisis-center 子目錄。

    Create a crisis-center subfolder in the src/app folder.

  • app/heroes 中的檔案和目錄複製到新的 crisis-center 資料夾中。

    Copy the files and folders from app/heroes into the new crisis-center folder.

  • 在這些新建的檔案中,把每個 "hero" 都改成 "crisis",每個 "heroes" 都改成 "crises"。

    In the new files, change every mention of "hero" to "crisis", and "heroes" to "crises".

  • 把這些 NgModule 檔案改名為 crisis-center.module.tscrisis-center-routing.module.ts

    Rename the NgModule files to crisis-center.module.ts and crisis-center-routing.module.ts.

使用 mock 的 crises 來代替 mock 的 heroes:

Use mock crises instead of mock heroes:

import { Crisis } from './crisis'; export const CRISES: Crisis[] = [ { id: 1, name: 'Dragon Burning Cities' }, { id: 2, name: 'Sky Rains Great White Sharks' }, { id: 3, name: 'Giant Asteroid Heading For Earth' }, { id: 4, name: 'Procrastinators Meeting Delayed Again' }, ];
src/app/crisis-center/mock-crises.ts
      
      import { Crisis } from './crisis';

export const CRISES: Crisis[] = [
  { id: 1, name: 'Dragon Burning Cities' },
  { id: 2, name: 'Sky Rains Great White Sharks' },
  { id: 3, name: 'Giant Asteroid Heading For Earth' },
  { id: 4, name: 'Procrastinators Meeting Delayed Again' },
];
    

最終的危機中心可以作為引入子路由這個新概念的基礎。 你可以把英雄管理保持在當前狀態,以便和危機中心進行對比。

The resulting crisis center is a foundation for introducing a new concept—child routing. You can leave Heroes in its current state as a contrast with the Crisis Center.

遵循關注點分離(Separation of Concerns)原則, 對危機中心的修改不會影響 AppModule 或其它特性模組中的元件。

In keeping with the Separation of Concerns principle, changes to the Crisis Center don't affect the AppModule or any other feature's component.

帶有子路由的危機中心

A crisis center with child routes

本節會展示如何組織危機中心,來滿足 Angular 應用所推薦的模式:

This section shows you how to organize the crisis center to conform to the following recommended pattern for Angular applications:

  • 把每個特性放在自己的目錄中。

    Each feature area resides in its own folder.

  • 每個特性都有自己的 Angular 特性模組。

    Each feature has its own Angular feature module.

  • 每個特性區都有自己的根元件。

    Each area has its own area root component.

  • 每個特性區的根元件中都有自己的路由出口及其子路由。

    Each area root component has its own router outlet and child routes.

  • 特性區的路由很少(或完全不)與其它特性區的路由交叉。

    Feature area routes rarely (if ever) cross with routes of other features.

如果你還有更多特性區,它們的元件樹是這樣的:

If your app had many feature areas, the app component trees might look like this:

子路由元件

Child routing component

crisis-center 目錄下產生一個 CrisisCenter 元件:

Generate a CrisisCenter component in the crisis-center folder:

ng generate component crisis-center/crisis-center
      
      ng generate component crisis-center/crisis-center
    

使用如下程式碼更新元件範本:

Update the component template with the following markup:

<h2>CRISIS CENTER</h2> <router-outlet></router-outlet>
src/app/crisis-center/crisis-center/crisis-center.component.html
      
      <h2>CRISIS CENTER</h2>
<router-outlet></router-outlet>
    

CrisisCenterComponentAppComponent 有下列共同點:

The CrisisCenterComponent has the following in common with the AppComponent:

  • 它是危機中心特性區的,正如 AppComponent 是整個應用的根。

    It is the root of the crisis center area, just as AppComponent is the root of the entire application.

  • 它是危機管理特性區的殼,正如 AppComponent 是管理高層工作流的殼。

    It is a shell for the crisis management feature area, just as the AppComponent is a shell to manage the high-level workflow.

就像大多數的殼一樣,CrisisCenterComponent 類別是最小化的,因為它沒有業務邏輯,它的範本中沒有連結,只有一個標題和用於放置危機中心的子元件的 <router-outlet>

Like most shells, the CrisisCenterComponent class is minimal because it has no business logic, and its template has no links, just a title and <router-outlet> for the crisis center child component.

子路由配置

Child route configuration

crisis-center 目錄下產生一個 CrisisCenterHome 元件,作為 "危機中心" 特性的宿主頁面。

As a host page for the "Crisis Center" feature, generate a CrisisCenterHome component in the crisis-center folder.

ng generate component crisis-center/crisis-center-home
      
      ng generate component crisis-center/crisis-center-home
    

用一條歡迎資訊修改 Crisis Center 中的範本。

Update the template with a welcome message to the Crisis Center.

<p>Welcome to the Crisis Center</p>
src/app/crisis-center/crisis-center-home/crisis-center-home.component.html
      
      <p>Welcome to the Crisis Center</p>
    

heroes-routing.module.ts 檔案複製過來,改名為 crisis-center-routing.module.ts,並修改它。 這次你要把子路由定義在父路由 crisis-center 中。

Update the crisis-center-routing.module.ts you renamed after copying it from heroes-routing.module.ts file. This time, you define child routes within the parent crisis-center route.

const crisisCenterRoutes: Routes = [ { path: 'crisis-center', component: CrisisCenterComponent, children: [ { path: '', component: CrisisListComponent, children: [ { path: ':id', component: CrisisDetailComponent }, { path: '', component: CrisisCenterHomeComponent } ] } ] } ];
src/app/crisis-center/crisis-center-routing.module.ts (Routes)
      
      const crisisCenterRoutes: Routes = [
  {
    path: 'crisis-center',
    component: CrisisCenterComponent,
    children: [
      {
        path: '',
        component: CrisisListComponent,
        children: [
          {
            path: ':id',
            component: CrisisDetailComponent
          },
          {
            path: '',
            component: CrisisCenterHomeComponent
          }
        ]
      }
    ]
  }
];
    

注意,父路由 crisis-center 有一個 children 屬性,它有一個包含 CrisisListComponent 的路由。 CrisisListModule 路由還有一個帶兩個路由的 children 陣列。

Notice that the parent crisis-center route has a children property with a single route containing the CrisisListComponent. The CrisisListComponent route also has a children array with two routes.

這兩個路由分別導航到了危機中心的兩個子元件:CrisisCenterHomeComponentCrisisDetailComponent

These two routes navigate to the crisis center child components, CrisisCenterHomeComponent and CrisisDetailComponent, respectively.

對這些子路由的處理中有一些重要的差異。

There are important differences in the way the router treats child routes.

路由器會把這些路由對應的元件放在 CrisisCenterComponentRouterOutlet 中,而不是 AppComponent 殼元件中的。

The router displays the components of these routes in the RouterOutlet of the CrisisCenterComponent, not in the RouterOutlet of the AppComponent shell.

CrisisListComponent 包含危機列表和一個 RouterOutlet,用以顯示 Crisis Center HomeCrisis Detail 這兩個路由元件。

The CrisisListComponent contains the crisis list and a RouterOutlet to display the Crisis Center Home and Crisis Detail route components.

Crisis Detail 路由是 Crisis List 的子路由。由於路由器預設會複用元件,因此當你選擇了另一個危機時,CrisisDetailComponent 會被複用。 作為對比,回頭看看 Hero Detail 路由,每當你從列表中選擇了不同的英雄時,都會重新建立該元件

The Crisis Detail route is a child of the Crisis List. The router reuses components by default, so the Crisis Detail component will be re-used as you select different crises. In contrast, back in the Hero Detail route, the component was recreated each time you selected a different hero from the list of heroes.

在最上層,以 / 開頭的路徑指向的總是應用的根。 但這裡是子路由。 它們是在父路由路徑的基礎上做出的擴充套件。 在路由樹中每深入一步,你就會在該路由的路徑上新增一個斜線 /(除非該路由的路徑是空的)。

At the top level, paths that begin with / refer to the root of the application. But child routes extend the path of the parent route. With each step down the route tree, you add a slash followed by the route path, unless the path is empty.

如果把該邏輯應用到危機中心中的導航,那麼父路徑就是 /crisis-center

Apply that logic to navigation within the crisis center for which the parent path is /crisis-center.

  • 要導航到 CrisisCenterHomeComponent,完整的 URL 是 /crisis-center (/crisis-center + '' + '')。

    To navigate to the CrisisCenterHomeComponent, the full URL is /crisis-center (/crisis-center + '' + '').

  • 要導航到 CrisisDetailComponent 以展示 id=2 的危機,完整的 URL 是 /crisis-center/2 (/crisis-center + '' + '/2')。

    To navigate to the CrisisDetailComponent for a crisis with id=2, the full URL is /crisis-center/2 (/crisis-center + '' + '/2').

本例子中包含站點部分的絕對 URL,就是:

The absolute URL for the latter example, including the localhost origin, is as follows:

localhost:4200/crisis-center/2
      
      localhost:4200/crisis-center/2
    

這裡是完整的 crisis-center.routing.ts 及其匯入語句。

Here's the complete crisis-center-routing.module.ts file with its imports.

import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CrisisCenterHomeComponent } from './crisis-center-home/crisis-center-home.component'; import { CrisisListComponent } from './crisis-list/crisis-list.component'; import { CrisisCenterComponent } from './crisis-center/crisis-center.component'; import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component'; const crisisCenterRoutes: Routes = [ { path: 'crisis-center', component: CrisisCenterComponent, children: [ { path: '', component: CrisisListComponent, children: [ { path: ':id', component: CrisisDetailComponent }, { path: '', component: CrisisCenterHomeComponent } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(crisisCenterRoutes) ], exports: [ RouterModule ] }) export class CrisisCenterRoutingModule { }
src/app/crisis-center/crisis-center-routing.module.ts (excerpt)
      
      import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { CrisisCenterHomeComponent } from './crisis-center-home/crisis-center-home.component';
import { CrisisListComponent } from './crisis-list/crisis-list.component';
import { CrisisCenterComponent } from './crisis-center/crisis-center.component';
import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component';

const crisisCenterRoutes: Routes = [
  {
    path: 'crisis-center',
    component: CrisisCenterComponent,
    children: [
      {
        path: '',
        component: CrisisListComponent,
        children: [
          {
            path: ':id',
            component: CrisisDetailComponent
          },
          {
            path: '',
            component: CrisisCenterHomeComponent
          }
        ]
      }
    ]
  }
];

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

把危機中心模組匯入到 AppModule 的路由中

Import crisis center module into the AppModule routes

就像 HeroesModule 模組中一樣,你必須把 CrisisCenterModule 新增到 AppModuleimports 陣列中,就在 AppRoutingModule 前面

As with the HeroesModule, you must add the CrisisCenterModule to the imports array of the AppModule before the AppRoutingModule:

import { NgModule } from '@angular/core'; import { FormsModule } from '@angular/forms'; import { CommonModule } from '@angular/common'; import { CrisisCenterHomeComponent } from './crisis-center-home/crisis-center-home.component'; import { CrisisListComponent } from './crisis-list/crisis-list.component'; import { CrisisCenterComponent } from './crisis-center/crisis-center.component'; import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component'; import { CrisisCenterRoutingModule } from './crisis-center-routing.module'; @NgModule({ imports: [ CommonModule, FormsModule, CrisisCenterRoutingModule ], declarations: [ CrisisCenterComponent, CrisisListComponent, CrisisCenterHomeComponent, CrisisDetailComponent ] }) export class CrisisCenterModule {}import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; import { ComposeMessageComponent } from './compose-message/compose-message.component'; import { AppRoutingModule } from './app-routing.module'; import { HeroesModule } from './heroes/heroes.module'; import { CrisisCenterModule } from './crisis-center/crisis-center.module'; @NgModule({ imports: [ CommonModule, FormsModule, HeroesModule, CrisisCenterModule, AppRoutingModule ], declarations: [ AppComponent, PageNotFoundComponent ], bootstrap: [ AppComponent ] }) export class AppModule { }
      
      import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { CommonModule } from '@angular/common';

import { CrisisCenterHomeComponent } from './crisis-center-home/crisis-center-home.component';
import { CrisisListComponent } from './crisis-list/crisis-list.component';
import { CrisisCenterComponent } from './crisis-center/crisis-center.component';
import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component';

import { CrisisCenterRoutingModule } from './crisis-center-routing.module';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    CrisisCenterRoutingModule
  ],
  declarations: [
    CrisisCenterComponent,
    CrisisListComponent,
    CrisisCenterHomeComponent,
    CrisisDetailComponent
  ]
})
export class CrisisCenterModule {}
    

app.routing.ts 中移除危機中心的初始路由。 因為現在是 HeroesModuleCrisisCenter 模組提供了這些特性路由。

Remove the initial crisis center route from the app-routing.module.ts because now the HeroesModule and the CrisisCenter modules provide the feature routes.

app-routing.module.ts 檔案中只有應用的最上層路由,比如預設路由和萬用字元路由。

The app-routing.module.ts file retains the top-level application routes such as the default and wildcard routes.

import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; const appRoutes: Routes = [ { path: '', redirectTo: '/heroes', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ]; @NgModule({ imports: [ RouterModule.forRoot( appRoutes, { enableTracing: true } // <-- debugging purposes only ) ], exports: [ RouterModule ] }) export class AppRoutingModule {}
src/app/app-routing.module.ts (v3)
      
      import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { PageNotFoundComponent } from './page-not-found/page-not-found.component';

const appRoutes: Routes = [
  { path: '',   redirectTo: '/heroes', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent }
];

@NgModule({
  imports: [
    RouterModule.forRoot(
      appRoutes,
      { enableTracing: true } // <-- debugging purposes only
    )
  ],
  exports: [
    RouterModule
  ]
})
export class AppRoutingModule {}
    

相對導航

Relative navigation

雖然構建出了危機中心特性區,你卻仍在使用以斜槓開頭的絕對路徑來導航到危機詳情的路由。

While building out the crisis center feature, you navigated to the crisis detail route using an absolute path that begins with a slash.

路由器會從路由配置的最上層來匹配像這樣的絕對路徑。

The router matches such absolute paths to routes starting from the top of the route configuration.

你固然可以繼續像危機中心特性區一樣使用絕對路徑,但是那樣會把連結釘死在特定的父路由結構上。 如果你修改了父路徑 /crisis-center,那就不得不修改每一個連結引數陣列。

You could continue to use absolute paths like this to navigate inside the Crisis Center feature, but that pins the links to the parent routing structure. If you changed the parent /crisis-center path, you would have to change the link parameters array.

透過改成定義相對於當前 URL 的路徑,你可以把連結從這種依賴中解放出來。 當你修改了該特性區的父路由路徑時,該特性區內部的導航仍然完好無損。

You can free the links from this dependency by defining paths that are relative to the current URL segment. Navigation within the feature area remains intact even if you change the parent route path to the feature.

路由器支援在連結引數陣列中使用“目錄式”語法來為查詢路由名提供幫助:

The router supports directory-like syntax in a link parameters list to help guide route name lookup:

./無前導斜線 形式是相對於當前級別的。

./ or no leading slash is relative to the current level.

../ 會回到當前路由路徑的上一級。

../ to go up one level in the route path.

你可以把相對導航語法和一個祖先路徑組合起來用。 如果不得不導航到一個兄弟路由,你可以用 ../<sibling> 來回到上一級,然後進入兄弟路由路徑中。

You can combine relative navigation syntax with an ancestor path. If you must navigate to a sibling route, you could use the ../<sibling> convention to go up one level, then over and down the sibling route path.

Router.navigate 方法導航到相對路徑時,你必須提供當前的 ActivatedRoute,來讓路由器知道你現在位於路由樹中的什麼位置。

To navigate a relative path with the Router.navigate method, you must supply the ActivatedRoute to give the router knowledge of where you are in the current route tree.

連結引數陣列後面,新增一個帶有 relativeTo 屬性的物件,並把它設定為當前的 ActivatedRoute。 這樣路由器就會基於當前啟用路由的位置來計算出目標 URL。

After the link parameters array, add an object with a relativeTo property set to the ActivatedRoute. The router then calculates the target URL based on the active route's location.

當呼叫路由器的 navigateByUrl() 時,總是要指定完整的絕對路徑。

Always specify the complete absolute path when calling router's navigateByUrl() method.

你已經注入了組成相對導航路徑所需的 ActivatedRoute

You've already injected the ActivatedRoute that you need to compose the relative navigation path.

如果用 RouterLink 來代替 Router 服務進行導航,就要使用相同的連結引數陣列,不過不再需要提供 relativeTo 屬性。 ActivatedRoute 已經隱含在了 RouterLink 指令中。

When using a RouterLink to navigate instead of the Router service, you'd use the same link parameters array, but you wouldn't provide the object with the relativeTo property. The ActivatedRoute is implicit in a RouterLink directive.

修改 CrisisDetailComponentgotoCrises() 方法,來使用相對路徑返回危機中心列表。

Update the gotoCrises() method of the CrisisDetailComponent to navigate back to the Crisis Center list using relative path navigation.

// Relative navigation back to the crises this.router.navigate(['../', { id: crisisId, foo: 'foo' }], { relativeTo: this.route });
src/app/crisis-center/crisis-detail/crisis-detail.component.ts (relative navigation)
      
      // Relative navigation back to the crises
this.router.navigate(['../', { id: crisisId, foo: 'foo' }], { relativeTo: this.route });
    

注意這個路徑使用了 ../ 語法返回上一級。 如果當前危機的 id3,那麼最終返回到的路徑就是 /crisis-center/;id=3;foo=foo

Notice that the path goes up a level using the ../ syntax. If the current crisis id is 3, the resulting path back to the crisis list is /crisis-center/;id=3;foo=foo.

用命名出口(outlet)顯示多重路由

Displaying multiple routes in named outlets

你決定給使用者提供一種方式來聯絡危機中心。 當用戶點選“Contact”按鈕時,你要在一個彈出框中顯示一條訊息。

You decide to give users a way to contact the crisis center. When a user clicks a "Contact" button, you want to display a message in a popup view.

即使在應用中的不同頁面之間切換,這個彈出框也應該始終保持開啟狀態,直到使用者傳送了訊息或者手動取消。 顯然,你不能把這個彈出框跟其它放到頁面放到同一個路由出口中。

The popup should stay open, even when switching between pages in the application, until the user closes it by sending the message or canceling. Clearly you can't put the popup in the same outlet as the other pages.

迄今為止,你只定義過單路由出口,並且在其中嵌套了子路由以便對路由分組。 在每個範本中,路由器只能支援一個無名主路由出口。

Until now, you've defined a single outlet and you've nested child routes under that outlet to group routes together. The router only supports one primary unnamed outlet per template.

範本還可以有多個命名的路由出口。 每個命名出口都自己有一組帶元件的路由。 多重出口可以在同一時間根據不同的路由來顯示不同的內容。

A template can also have any number of named outlets. Each named outlet has its own set of routes with their own components. Multiple outlets can display different content, determined by different routes, all at the same time.

AppComponent 中新增一個名叫“popup”的出口,就在無名出口的下方。

Add an outlet named "popup" in the AppComponent, directly below the unnamed outlet.

<div [@routeAnimation]="getAnimationData(routerOutlet)"> <router-outlet #routerOutlet="outlet"></router-outlet> </div> <router-outlet name="popup"></router-outlet>
src/app/app.component.html (outlets)
      
      <div [@routeAnimation]="getAnimationData(routerOutlet)">
  <router-outlet #routerOutlet="outlet"></router-outlet>
</div>
<router-outlet name="popup"></router-outlet>
    

一旦你學會了如何把一個彈出框元件路由到該出口,那裡就是將會出現彈出框的地方。

That's where a popup will go, once you learn how to route a popup component to it.

第二路由

Secondary routes

命名出口是第二路由的目標。

Named outlets are the targets of secondary routes.

第二路由很像主路由,配置方式也一樣。它們只有一些關鍵的不同點:

Secondary routes look like primary routes and you configure them the same way. They differ in a few key respects.

  • 它們彼此互不依賴。

    They are independent of each other.

  • 它們與其它路由組合使用。

    They work in combination with other routes.

  • 它們顯示在命名出口中。

    They are displayed in named outlets.

產生一個新的元件來組合這個訊息。

Generate a new component to compose the message.

ng generate component compose-message
      
      ng generate component compose-message
    

它顯示一個簡單的表單,包括一個頭、一個訊息輸入框和兩個按鈕:“Send”和“Cancel”。

It displays a short form with a header, an input box for the message, and two buttons, "Send" and "Cancel".

下面是該元件及其範本和樣式:

Here's the component, its template and styles:

:host { position: relative; bottom: 10%; }<h3>Contact Crisis Center</h3> <div *ngIf="details"> {{ details }} </div> <div> <div> <label>Message: </label> </div> <div> <textarea [(ngModel)]="message" rows="10" cols="35" [disabled]="sending"></textarea> </div> </div> <p *ngIf="!sending"> <button (click)="send()">Send</button> <button (click)="cancel()">Cancel</button> </p>import { Component, HostBinding } from '@angular/core'; import { Router } from '@angular/router'; @Component({ selector: 'app-compose-message', templateUrl: './compose-message.component.html', styleUrls: ['./compose-message.component.css'] }) export class ComposeMessageComponent { details: string; message: string; sending = false; constructor(private router: Router) {} send() { this.sending = true; this.details = 'Sending Message...'; setTimeout(() => { this.sending = false; this.closePopup(); }, 1000); } cancel() { this.closePopup(); } closePopup() { // Providing a `null` value to the named outlet // clears the contents of the named outlet this.router.navigate([{ outlets: { popup: null }}]); } }
      
      :host {
  position: relative; bottom: 10%;
}
    

它看起來幾乎和你以前見過其它元件一樣,但有兩個值得注意的區別。

It looks similar to any other component in this guide, but there are two key differences.

注意,send() 方法在傳送訊息和關閉彈出框之前透過等待模擬了一秒鐘的延遲。

Note that the send() method simulates latency by waiting a second before "sending" the message and closing the popup.

closePopup() 方法用把 popup 出口導航到 null 的方式關閉了彈出框,它在稍後的部分有講解。

The closePopup() method closes the popup view by navigating to the popup outlet with a null which the section on clearing secondary routes covers.

新增第二路由

Add a secondary route

開啟 AppRoutingModule,並把一個新的 compose 路由新增到 appRoutes 中。

Open the AppRoutingModule and add a new compose route to the appRoutes.

{ path: 'compose', component: ComposeMessageComponent, outlet: 'popup' },
src/app/app-routing.module.ts (compose route)
      
      {
  path: 'compose',
  component: ComposeMessageComponent,
  outlet: 'popup'
},
    

除了 pathcomponent 屬性之外還有一個新的屬性 outlet,它被設定成了 'popup'。 這個路由現在指向了 popup 出口,而 ComposeMessageComponent 也將顯示在那裡。

In addition to the path and component properties, there's a new property called outlet, which is set to 'popup'. This route now targets the popup outlet and the ComposeMessageComponent will display there.

為了給使用者某種途徑來開啟這個彈出框,還要往 AppComponent 範本中新增一個“Contact”連結。

To give users a way to open the popup, add a "Contact" link to the AppComponent template.

<a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a>
src/app/app.component.html (contact-link)
      
      <a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a>
    

雖然 compose 路由被配置到了 popup 出口上,但這仍然不足以把該路由和 RouterLink 指令聯絡起來。 你還要在連結引數陣列中指定這個命名出口,並透過屬性繫結的形式把它繫結到 RouterLink 上。

Although the compose route is configured to the "popup" outlet, that's not sufficient for connecting the route to a RouterLink directive. You have to specify the named outlet in a link parameters array and bind it to the RouterLink with a property binding.

連結引數陣列包含一個只有一個 outlets 屬性的物件,它的值是另一個物件,這個物件以一個或多個路由的出口名作為屬性名。 在這裡,它只有一個出口名“popup”,它的值則是另一個連結引數陣列,用於指定 compose 路由。

The link parameters array contains an object with a single outlets property whose value is another object keyed by one (or more) outlet names. In this case there is only the "popup" outlet property and its value is another link parameters array that specifies the compose route.

換句話說,當用戶點選此連結時,路由器會在路由出口 popup 中顯示與 compose 路由相關聯的元件。

In other words, when the user clicks this link, the router displays the component associated with the compose route in the popup outlet.

當只需要考慮一個路由和一個無名出口時,外部物件中的這個 outlets 物件是完全不必要的。

This outlets object within an outer object was unnecessary when there was only one route and one unnamed outlet.

路由器假設這個路由指向了無名的主出口,並為你建立這些物件。

The router assumed that your route specification targeted the unnamed primary outlet and created these objects for you.

路由到一個命名出口會揭示一個路由特性: 你可以在同一個 RouterLink 指令中為多個路由出口指定多個路由。

Routing to a named outlet has revealed a router feature: you can target multiple outlets with multiple routes in the same RouterLink directive.

第二路由導航:在導航期間合併路由

Secondary route navigation: merging routes during navigation

導航到危機中心並點選“Contact”,你將會在瀏覽器的位址列看到如下 URL:

Navigate to the Crisis Center and click "Contact". you should see something like the following URL in the browser address bar.

http://.../crisis-center(popup:compose)
      
      http://.../crisis-center(popup:compose)
    

這個 URL 中有意義的部分是 ... 後面的這些:

The relevant part of the URL follows the ...:

  • crisis-center 是主導航。

    The crisis-center is the primary navigation.

  • 圓括號包裹的部分是第二路由。

    Parentheses surround the secondary route.

  • 第二路由包括一個出口名稱(popup)、一個冒號分隔符和第二路由的路徑(compose)。

    The secondary route consists of an outlet name (popup), a colon separator, and the secondary route path (compose).

點選 Heroes 連結,並再次檢視 URL:

Click the Heroes link and look at the URL again.

http://.../heroes(popup:compose)
      
      http://.../heroes(popup:compose)
    

主導航的部分變化了,而第二路由沒有變。

The primary navigation part has changed; the secondary route is the same.

路由器在導航樹中對兩個獨立的分支保持追蹤,並在 URL 中對這棵樹進行表達。

The router is keeping track of two separate branches in a navigation tree and generating a representation of that tree in the URL.

你還可以新增更多出口和更多路由(無論是在最上層還是在巢狀的子層)來建立一個帶有多個分支的導航樹。 路由器將會產生相應的 URL。

You can add many more outlets and routes, at the top level and in nested levels, creating a navigation tree with many branches and the router will generate the URLs to go with it.

透過像前面那樣填充 outlets 物件,你可以告訴路由器立即導航到一棵完整的樹。 然後把這個物件透過一個連結引數陣列傳給 router.navigate 方法。

You can tell the router to navigate an entire tree at once by filling out the outlets object and then pass that object inside a link parameters array to the router.navigate method.

清除第二路由

Clearing secondary routes

像常規出口一樣,二級出口會一直存在,直到你導航到新元件。

Like regular outlets, secondary outlets persists until you navigate away to a new component.

每個第二齣口都有自己獨立的導航,跟主出口的導航彼此獨立。 修改主出口中的當前路由並不會影響到 popup 出口中的。 這就是為什麼在危機中心和英雄管理之間導航時,彈出框始終都是可見的。

Each secondary outlet has its own navigation, independent of the navigation driving the primary outlet. Changing a current route that displays in the primary outlet has no effect on the popup outlet. That's why the popup stays visible as you navigate among the crises and heroes.

再看 closePopup() 方法:

The closePopup() method again:

closePopup() { // Providing a `null` value to the named outlet // clears the contents of the named outlet this.router.navigate([{ outlets: { popup: null }}]); }
src/app/compose-message/compose-message.component.ts (closePopup)
      
      closePopup() {
  // Providing a `null` value to the named outlet
  // clears the contents of the named outlet
  this.router.navigate([{ outlets: { popup: null }}]);
}
    

單擊 “send” 或 “cancel” 按鈕可以清除彈出檢視。closePopup() 函式會使用 Router.navigate() 方法強制導航,並傳入一個連結引數陣列

Clicking the "send" or "cancel" buttons clears the popup view. The closePopup() function navigates imperatively with the Router.navigate() method, passing in a link parameters array.

就像在 AppComponent 中繫結到的 Contact RouterLink 一樣,它也包含了一個帶 outlets 屬性的物件。 outlets 屬性的值是另一個物件,該物件用一些出口名稱作為屬性名。 唯一的命名出口是 'popup'

Like the array bound to the Contact RouterLink in the AppComponent, this one includes an object with an outlets property. The outlets property value is another object with outlet names for keys. The only named outlet is 'popup'.

但這次,'popup' 的值是 nullnull 不是一個路由,但卻是一個合法的值。 把 popup 這個 RouterOutlet 設定為 null 會清除該出口,並且從當前 URL 中移除第二路由 popup

This time, the value of 'popup' is null. That's not a route, but it is a legitimate value. Setting the popup RouterOutlet to null clears the outlet and removes the secondary popup route from the current URL.

里程碑 5:路由守衛

Milestone 5: Route guards

現在,任何使用者都能在任何時候導航到任何地方。但有時候出於種種原因需要控制對該應用的不同部分的訪問。可能包括如下場景:

At the moment, any user can navigate anywhere in the application anytime, but sometimes you need to control access to different parts of your app for various reasons. Some of which may include the following:

  • 該使用者可能無權導航到目標元件。

    Perhaps the user is not authorized to navigate to the target component.

  • 可能使用者得先登入(認證)。

    Maybe the user must login (authenticate) first.

  • 在顯示目標元件前,你可能得先獲取某些資料。

    Maybe you should fetch some data before you display the target component.

  • 在離開元件前,你可能要先儲存修改。

    You might want to save pending changes before leaving a component.

  • 你可能要詢問使用者:你是否要放棄本次更改,而不用儲存它們?

    You might ask the user if it's OK to discard pending changes rather than save them.

你可以往路由配置中新增守衛,來處理這些場景。

You add guards to the route configuration to handle these scenarios.

守衛返回一個值,以控制路由器的行為:

A guard's return value controls the router's behavior:

  • 如果它返回 true,導航過程會繼續

    If it returns true, the navigation process continues.

  • 如果它返回 false,導航過程就會終止,且使用者留在原地。

    If it returns false, the navigation process stops and the user stays put.

  • 如果它返回 UrlTree,則取消當前的導航,並且開始導航到返回的這個 UrlTree.

    If it returns a UrlTree, the current navigation cancels and a new navigation is initiated to the UrlTree returned.

注意:守衛還可以告訴路由器導航到別處,這樣也會取消當前的導航。要想在守衛中這麼做,就要返回 false

Note: The guard can also tell the router to navigate elsewhere, effectively canceling the current navigation. When doing so inside a guard, the guard should return false;

守衛可以用同步的方式返回一個布林值。但在很多情況下,守衛無法用同步的方式給出答案。 守衛可能會向用戶問一個問題、把更改儲存到伺服器,或者獲取新資料,而這些都是非同步操作。

The guard might return its boolean answer synchronously. But in many cases, the guard can't produce an answer synchronously. The guard could ask the user a question, save changes to the server, or fetch fresh data. These are all asynchronous operations.

因此,路由的守衛可以返回一個 Observable<boolean>Promise<boolean>,並且路由器會等待這個可觀察物件被解析為 truefalse

Accordingly, a routing guard can return an Observable<boolean> or a Promise<boolean> and the router will wait for the observable to resolve to true or false.

注意: 提供給 Router 的可觀察物件還必須能結束(complete)。否則,導航就不會繼續。

Note: The observable provided to the Router must also complete. If the observable does not complete, the navigation does not continue.

路由器可以支援多種守衛介面:

The router supports multiple guard interfaces:

  • CanActivate來處理導航某路由的情況。

    CanActivateto mediate navigation to a route.

  • CanActivateChild來處理導航某子路由的情況。

    CanActivateChildto mediate navigation to a child route.

  • CanDeactivate來處理從當前路由離開的情況.

    CanDeactivateto mediate navigation away from the current route.

  • Resolve在路由啟用之前獲取路由資料。

    Resolveto perform route data retrieval before route activation.

  • CanLoad來處理非同步導航到某特性模組的情況。

    CanLoadto mediate navigation to a feature module loaded asynchronously.

在分層路由的每個級別上,你都可以設定多個守衛。 路由器會先按照從最深的子路由由下往上檢查的順序來檢查 CanDeactivate()CanActivateChild() 守衛。 然後它會按照從上到下的順序檢查 CanActivate() 守衛。 如果特性模組是非同步載入的,在載入它之前還會檢查 CanLoad() 守衛。 如果任何一個守衛返回 false,其它尚未完成的守衛會被取消,這樣整個導航就被取消了。

You can have multiple guards at every level of a routing hierarchy. The router checks the CanDeactivate and CanActivateChild guards first, from the deepest child route to the top. Then it checks the CanActivate guards from the top down to the deepest child route. If the feature module is loaded asynchronously, the CanLoad guard is checked before the module is loaded. If any guard returns false, pending guards that have not completed will be canceled, and the entire navigation is canceled.

接下來的小節中有一些例子。

There are several examples over the next few sections.

CanActivate :需要身份驗證

CanActivate: requiring authentication

應用程式通常會根據訪問者來決定是否授予某個特性區的訪問權。 你可以只對已認證過的使用者或具有特定角色的使用者授予訪問權,還可以阻止或限制使用者訪問權,直到使用者賬戶啟用為止。

Applications often restrict access to a feature area based on who the user is. You could permit access only to authenticated users or to users with a specific role. You might block or limit access until the user's account is activated.

CanActivate 守衛是一個管理這些導航類別業務規則的工具。

The CanActivate guard is the tool to manage these navigation business rules.

新增一個“管理”特性模組

Add an admin feature module

本節將指導你使用一些新的管理功能來擴充套件危機中心。首先新增一個名為 AdminModule 的新特性模組。

This section guides you through extending the crisis center with some new administrative features. Start by adding a new feature module named AdminModule.

產生一個帶有特性模組檔案和路由配置檔案的 admin 目錄。

Generate an admin folder with a feature module file and a routing configuration file.

ng generate module admin --routing
      
      ng generate module admin --routing
    

接下來,產生一些支援性元件。

Next, generate the supporting components.

ng generate component admin/admin-dashboard
      
      ng generate component admin/admin-dashboard
    
ng generate component admin/admin
      
      ng generate component admin/admin
    
ng generate component admin/manage-crises
      
      ng generate component admin/manage-crises
    
ng generate component admin/manage-heroes
      
      ng generate component admin/manage-heroes
    

管理特性區的檔案是這樣的:

The admin feature file structure looks like this:

src/app/admin

admin
admin.component.css
admin.component.html
admin.component.ts
admin-dashboard
admin-dashboard.component.css
admin-dashboard.component.html
admin-dashboard.component.ts
manage-crises
manage-crises.component.css
manage-crises.component.html
manage-crises.component.ts
manage-heroes
manage-heroes.component.css
manage-heroes.component.html
manage-heroes.component.ts
admin.module.ts
admin-routing.module.ts

管理特性模組包含 AdminComponent,它用於在特性模組內的儀表盤路由以及兩個尚未完成的用於管理危機和英雄的元件之間進行路由。

The admin feature module contains the AdminComponent used for routing within the feature module, a dashboard route and two unfinished components to manage crises and heroes.

<h3>ADMIN</h3> <nav> <a routerLink="./" routerLinkActive="active" [routerLinkActiveOptions]="{ exact: true }">Dashboard</a> <a routerLink="./crises" routerLinkActive="active">Manage Crises</a> <a routerLink="./heroes" routerLinkActive="active">Manage Heroes</a> </nav> <router-outlet></router-outlet><p>Dashboard</p>import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { AdminComponent } from './admin/admin.component'; import { AdminDashboardComponent } from './admin-dashboard/admin-dashboard.component'; import { ManageCrisesComponent } from './manage-crises/manage-crises.component'; import { ManageHeroesComponent } from './manage-heroes/manage-heroes.component'; import { AdminRoutingModule } from './admin-routing.module'; @NgModule({ imports: [ CommonModule, AdminRoutingModule ], declarations: [ AdminComponent, AdminDashboardComponent, ManageCrisesComponent, ManageHeroesComponent ] }) export class AdminModule {}<p>Manage your crises here</p><p>Manage your heroes here</p>
      
      <h3>ADMIN</h3>
<nav>
  <a routerLink="./" routerLinkActive="active"
    [routerLinkActiveOptions]="{ exact: true }">Dashboard</a>
  <a routerLink="./crises" routerLinkActive="active">Manage Crises</a>
  <a routerLink="./heroes" routerLinkActive="active">Manage Heroes</a>
</nav>
<router-outlet></router-outlet>
    

雖然管理儀表盤中的 RouterLink 只包含一個沒有其它 URL 段的斜槓 /,但它能匹配管理特性區下的任何路由。 但你只希望在訪問 Dashboard 路由時才啟用該連結。 往 Dashboard 這個 routerLink 上新增另一個繫結 [routerLinkActiveOptions]="{ exact: true }", 這樣就只有當用戶導航到 /admin 這個 URL 時才會啟用它,而不會在導航到它的某個子路由時。

Although the admin dashboard RouterLink only contains a relative slash without an additional URL segment, it is a match to any route within the admin feature area. You only want the Dashboard link to be active when the user visits that route. Adding an additional binding to the Dashboard routerLink,[routerLinkActiveOptions]="{ exact: true }", marks the ./ link as active when the user navigates to the /admin URL and not when navigating to any of the child routes.

無元件路由:分組路由,而不需要元件
Component-less route: grouping routes without a component

最初的管理路由配置如下:

The initial admin routing configuration:

const adminRoutes: Routes = [ { path: 'admin', component: AdminComponent, children: [ { path: '', children: [ { path: 'crises', component: ManageCrisesComponent }, { path: 'heroes', component: ManageHeroesComponent }, { path: '', component: AdminDashboardComponent } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(adminRoutes) ], exports: [ RouterModule ] }) export class AdminRoutingModule {}
src/app/admin/admin-routing.module.ts (admin routing)
      
      const adminRoutes: Routes = [
  {
    path: 'admin',
    component: AdminComponent,
    children: [
      {
        path: '',
        children: [
          { path: 'crises', component: ManageCrisesComponent },
          { path: 'heroes', component: ManageHeroesComponent },
          { path: '', component: AdminDashboardComponent }
        ]
      }
    ]
  }
];

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

AdminComponent 下的子路由有一個 path 和一個 children 屬性,但是它沒有使用 component。這就定義了一個無元件路由。

The child route under the AdminComponent has a path and a children property but it's not using a component. This defines a component-less route.

要把 Crisis Center 管理下的路由分組到 admin 路徑下,元件是不必要的。此外,無元件路由可以更容易地保護子路由

To group the Crisis Center management routes under the admin path a component is unnecessary. Additionally, a component-less route makes it easier to guard child routes.

接下來,把 AdminModule 匯入到 app.module.ts 中,並把它加入 imports 陣列中來註冊這些管理類別路由。

Next, import the AdminModule into app.module.ts and add it to the imports array to register the admin routes.

import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; import { ComposeMessageComponent } from './compose-message/compose-message.component'; import { AppRoutingModule } from './app-routing.module'; import { HeroesModule } from './heroes/heroes.module'; import { CrisisCenterModule } from './crisis-center/crisis-center.module'; import { AdminModule } from './admin/admin.module'; @NgModule({ imports: [ CommonModule, FormsModule, HeroesModule, CrisisCenterModule, AdminModule, AppRoutingModule ], declarations: [ AppComponent, ComposeMessageComponent, PageNotFoundComponent ], bootstrap: [ AppComponent ] }) export class AppModule { }
src/app/app.module.ts (admin module)
      
      import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';

import { AppComponent } from './app.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
import { ComposeMessageComponent } from './compose-message/compose-message.component';

import { AppRoutingModule } from './app-routing.module';
import { HeroesModule } from './heroes/heroes.module';
import { CrisisCenterModule } from './crisis-center/crisis-center.module';

import { AdminModule } from './admin/admin.module';

@NgModule({
  imports: [
    CommonModule,
    FormsModule,
    HeroesModule,
    CrisisCenterModule,
    AdminModule,
    AppRoutingModule
  ],
  declarations: [
    AppComponent,
    ComposeMessageComponent,
    PageNotFoundComponent
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }
    

然後往殼元件 AppComponent 中新增一個連結,讓使用者能點選它,以訪問該特性。

Add an "Admin" link to the AppComponent shell so that users can get to this feature.

<h1 class="title">Angular Router</h1> <nav> <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a> <a routerLink="/heroes" routerLinkActive="active">Heroes</a> <a routerLink="/admin" routerLinkActive="active">Admin</a> <a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a> </nav> <div [@routeAnimation]="getAnimationData(routerOutlet)"> <router-outlet #routerOutlet="outlet"></router-outlet> </div> <router-outlet name="popup"></router-outlet>
src/app/app.component.html (template)
      
      <h1 class="title">Angular Router</h1>
<nav>
  <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a>
  <a routerLink="/heroes" routerLinkActive="active">Heroes</a>
  <a routerLink="/admin" routerLinkActive="active">Admin</a>
  <a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a>
</nav>
<div [@routeAnimation]="getAnimationData(routerOutlet)">
  <router-outlet #routerOutlet="outlet"></router-outlet>
</div>
<router-outlet name="popup"></router-outlet>
    

守護“管理特性”區

Guard the admin feature

現在危機中心的每個路由都是對所有人開放的。這些新的管理特性應該只能被已登入使用者訪問。

Currently, every route within the Crisis Center is open to everyone. The new admin feature should be accessible only to authenticated users.

編寫一個 CanActivate() 守衛,將正在嘗試訪問管理元件匿名使用者重新導向到登入頁。

Write a canActivate() guard method to redirect anonymous users to the login page when they try to enter the admin area.

auth 資料夾中產生一個 AuthGuard

Generate an AuthGuard in the auth folder.

ng generate guard auth/auth
      
      ng generate guard auth/auth
    

為了示範這些基礎知識,這個例子只把日誌寫到控制檯中,立即 return true,並允許繼續導航:

To demonstrate the fundamentals, this example only logs to the console, returns true immediately, and allows navigation to proceed:

import { Injectable } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; @Injectable({ providedIn: 'root', }) export class AuthGuard implements CanActivate { canActivate( next: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { console.log('AuthGuard#canActivate called'); return true; } }
src/app/auth/auth.guard.ts (excerpt)
      
      import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';

@Injectable({
  providedIn: 'root',
})
export class AuthGuard implements CanActivate {
  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): boolean {
    console.log('AuthGuard#canActivate called');
    return true;
  }
}
    

接下來,開啟 admin-routing.module.ts,匯入 AuthGuard 類別,修改管理路由並透過 CanActivate() 守衛來參考 AuthGuard

Next, open admin-routing.module.ts, import the AuthGuard class, and update the admin route with a canActivate guard property that references it:

import { AuthGuard } from '../auth/auth.guard'; const adminRoutes: Routes = [ { path: 'admin', component: AdminComponent, canActivate: [AuthGuard], children: [ { path: '', children: [ { path: 'crises', component: ManageCrisesComponent }, { path: 'heroes', component: ManageHeroesComponent }, { path: '', component: AdminDashboardComponent } ], } ] } ]; @NgModule({ imports: [ RouterModule.forChild(adminRoutes) ], exports: [ RouterModule ] }) export class AdminRoutingModule {}
src/app/admin/admin-routing.module.ts (guarded admin route)
      
      import { AuthGuard } from '../auth/auth.guard';

const adminRoutes: Routes = [
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [AuthGuard],
    children: [
      {
        path: '',
        children: [
          { path: 'crises', component: ManageCrisesComponent },
          { path: 'heroes', component: ManageHeroesComponent },
          { path: '', component: AdminDashboardComponent }
        ],
      }
    ]
  }
];

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

管理特性區現在受此守衛保護了,不過該守衛還需要做進一步訂製。

The admin feature is now protected by the guard, but the guard requires more customization to work fully.

透過 AuthGuard 驗證

Authenticate with AuthGuard

AuthGuard 模擬身份驗證。

Make the AuthGuard mimic authentication.

AuthGuard 可以呼叫應用中的一項服務,該服務能讓使用者登入,並且儲存當前使用者的資訊。在 admin 目錄下產生一個新的 AuthService

The AuthGuard should call an application service that can login a user and retain information about the current user. Generate a new AuthService in the auth folder:

ng generate service auth/auth
      
      ng generate service auth/auth
    

修改 AuthService 以登入此使用者:

Update the AuthService to log in the user:

import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; import { tap, delay } from 'rxjs/operators'; @Injectable({ providedIn: 'root', }) export class AuthService { isLoggedIn = false; // store the URL so we can redirect after logging in redirectUrl: string; login(): Observable<boolean> { return of(true).pipe( delay(1000), tap(val => this.isLoggedIn = true) ); } logout(): void { this.isLoggedIn = false; } }
src/app/auth/auth.service.ts (excerpt)
      
      import { Injectable } from '@angular/core';

import { Observable, of } from 'rxjs';
import { tap, delay } from 'rxjs/operators';

@Injectable({
  providedIn: 'root',
})
export class AuthService {
  isLoggedIn = false;

  // store the URL so we can redirect after logging in
  redirectUrl: string;

  login(): Observable<boolean> {
    return of(true).pipe(
      delay(1000),
      tap(val => this.isLoggedIn = true)
    );
  }

  logout(): void {
    this.isLoggedIn = false;
  }
}
    

雖然不會真的進行登入,但它有一個 isLoggedIn 標誌,用來標識是否使用者已經登入過了。 它的 login() 方法會模擬一個對外部服務的 API 呼叫,返回一個可觀察物件(observable)。在短暫的停頓之後,這個可觀察物件就會解析成功。 redirectUrl 屬性將會儲存在使用者要訪問的 URL 中,以便認證完之後導航到它。

Although it doesn't actually log in, it has an isLoggedIn flag to tell you whether the user is authenticated. Its login() method simulates an API call to an external service by returning an observable that resolves successfully after a short pause. The redirectUrl property stores the URL that the user wanted to access so you can navigate to it after authentication.

為了保持最小化,這個例子會將未經身份驗證的使用者重新導向到 /admin

To keep things minimal, this example redirects unauthenticated users to /admin.

修改 AuthGuard 以呼叫 AuthService

Revise the AuthGuard to call the AuthService.

import { Injectable } from '@angular/core'; import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router, UrlTree } from '@angular/router'; import { AuthService } from './auth.service'; @Injectable({ providedIn: 'root', }) export class AuthGuard implements CanActivate { constructor(private authService: AuthService, private router: Router) {} canActivate( next: ActivatedRouteSnapshot, state: RouterStateSnapshot): true|UrlTree { const url: string = state.url; return this.checkLogin(url); } checkLogin(url: string): true|UrlTree { if (this.authService.isLoggedIn) { return true; } // Store the attempted URL for redirecting this.authService.redirectUrl = url; // Redirect to the login page return this.router.parseUrl('/login'); } }
src/app/auth/auth.guard.ts (v2)
      
      import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, Router, UrlTree } from '@angular/router';

import { AuthService } from './auth.service';

@Injectable({
  providedIn: 'root',
})
export class AuthGuard implements CanActivate {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): true|UrlTree {
    const url: string = state.url;

    return this.checkLogin(url);
  }

  checkLogin(url: string): true|UrlTree {
    if (this.authService.isLoggedIn) { return true; }

    // Store the attempted URL for redirecting
    this.authService.redirectUrl = url;

    // Redirect to the login page
    return this.router.parseUrl('/login');
  }
}
    

注意,你把 AuthServiceRouter 服務注入到了建構函式中。 你還沒有提供 AuthService,這裡要說明的是:可以往路由守衛中注入有用的服務。

Notice that you inject the AuthService and the Router in the constructor. You haven't provided the AuthService yet but it's good to know that you can inject helpful services into routing guards.

該守衛返回一個同步的布林值。如果使用者已經登入,它就返回 true,導航會繼續。

This guard returns a synchronous boolean result. If the user is logged in, it returns true and the navigation continues.

這個 ActivatedRouteSnapshot 包含了即將被啟用的路由,而 RouterStateSnapshot 包含了該應用即將到達的狀態。 你應該透過守衛進行檢查。

The ActivatedRouteSnapshot contains the future route that will be activated and the RouterStateSnapshot contains the future RouterState of the application, should you pass through the guard check.

如果使用者還沒有登入,你就會用 RouterStateSnapshot.url 儲存使用者來自的 URL 並讓路由器跳轉到登入頁(你尚未建立該頁)。 這間接導致路由器自動中止了這次導航,checkLogin() 返回 false 並不是必須的,但這樣可以更清楚的表達意圖。

If the user is not logged in, you store the attempted URL the user came from using the RouterStateSnapshot.url and tell the router to redirect to a login page—a page you haven't created yet. Returning a UrlTree tells the Router to cancel the current navigation and schedule a new one to redirect the user.

新增 LoginComponent

Add the LoginComponent

你需要一個 LoginComponent 來讓使用者登入進這個應用。在登入之後,你就會跳轉到前面儲存的 URL,如果沒有,就跳轉到預設 URL。 該元件沒有什麼新內容,你在路由配置中使用它的方式也沒什麼新意。

You need a LoginComponent for the user to log in to the app. After logging in, you'll redirect to the stored URL if available, or use the default URL. There is nothing new about this component or the way you use it in the router configuration.

ng generate component auth/login
      
      ng generate component auth/login
    

auth/auth-routing.module.ts 檔案中註冊一個 /login 路由。在 app.module.ts 中,匯入 AuthModule 並且新增到 AppModuleimports 中。

Register a /login route in the auth/auth-routing.module.ts. In app.module.ts, import and add the AuthModule to the AppModule imports.

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 { PageNotFoundComponent } from './page-not-found/page-not-found.component'; import { ComposeMessageComponent } from './compose-message/compose-message.component'; import { AppRoutingModule } from './app-routing.module'; import { HeroesModule } from './heroes/heroes.module'; import { AuthModule } from './auth/auth.module'; @NgModule({ imports: [ BrowserModule, BrowserAnimationsModule, FormsModule, HeroesModule, AuthModule, AppRoutingModule, ], declarations: [ AppComponent, ComposeMessageComponent, PageNotFoundComponent ], bootstrap: [ AppComponent ] }) export class AppModule { }<h2>LOGIN</h2> <p>{{message}}</p> <p> <button (click)="login()" *ngIf="!authService.isLoggedIn">Login</button> <button (click)="logout()" *ngIf="authService.isLoggedIn">Logout</button> </p>import { Component } from '@angular/core'; import { Router } from '@angular/router'; import { AuthService } from '../auth.service'; @Component({ selector: 'app-login', templateUrl: './login.component.html', styleUrls: ['./login.component.css'] }) export class LoginComponent { message: string; constructor(public authService: AuthService, public router: Router) { this.setMessage(); } setMessage() { this.message = 'Logged ' + (this.authService.isLoggedIn ? 'in' : 'out'); } login() { this.message = 'Trying to log in ...'; this.authService.login().subscribe(() => { this.setMessage(); if (this.authService.isLoggedIn) { // Usually you would use the redirect URL from the auth service. // However to keep the example simple, we will always redirect to `/admin`. const redirectUrl = '/admin'; // Redirect the user this.router.navigate([redirectUrl]); } }); } logout() { this.authService.logout(); this.setMessage(); } }import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; import { LoginComponent } from './login/login.component'; import { AuthRoutingModule } from './auth-routing.module'; @NgModule({ imports: [ CommonModule, FormsModule, AuthRoutingModule ], declarations: [ LoginComponent ] }) export class AuthModule {}
      
      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 { PageNotFoundComponent } from './page-not-found/page-not-found.component';
import { ComposeMessageComponent } from './compose-message/compose-message.component';

import { AppRoutingModule } from './app-routing.module';
import { HeroesModule } from './heroes/heroes.module';
import { AuthModule } from './auth/auth.module';

@NgModule({
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    FormsModule,
    HeroesModule,
    AuthModule,
    AppRoutingModule,
  ],
  declarations: [
    AppComponent,
    ComposeMessageComponent,
    PageNotFoundComponent
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule {
}
    

CanActivateChild:保護子路由

CanActivateChild: guarding child routes

你還可以使用 CanActivateChild 守衛來保護子路由。 CanActivateChild 守衛和 CanActivate 守衛很像。 它們的區別在於,CanActivateChild 會在任何子路由被啟用之前執行。

You can also protect child routes with the CanActivateChild guard. The CanActivateChild guard is similar to the CanActivate guard. The key difference is that it runs before any child route is activated.

你要保護管理特性模組,防止它被非授權訪問,還要保護這個特性模組內部的那些子路由。

You protected the admin feature module from unauthorized access. You should also protect child routes within the feature module.

擴充套件 AuthGuard 以便在 admin 路由之間導航時提供保護。 開啟 auth.guard.ts 並從路由函式庫中匯入 CanActivateChild 介面。

Extend the AuthGuard to protect when navigating between the admin routes. Open auth.guard.ts and add the CanActivateChild interface to the imported tokens from the router package.

接下來,實現 CanActivateChild 方法,它所接收的引數與 CanActivate 方法一樣:一個 ActivatedRouteSnapshot 和一個 RouterStateSnapshotCanActivateChild 方法可以返回 Observable<boolean|UrlTree>Promise<boolean|UrlTree> 來支援非同步檢查,或 booleanUrlTree 來支援同步檢查。 這裡返回的或者是 true 以便允許使用者訪問管理特性模組,或者是 UrlTree 以便把使用者重新導向到登入頁:

Next, implement the canActivateChild() method which takes the same arguments as the canActivate() method: an ActivatedRouteSnapshot and RouterStateSnapshot. The canActivateChild() method can return an Observable<boolean|UrlTree> or Promise<boolean|UrlTree> for async checks and a boolean or UrlTree for sync checks. This one returns either true to allow the user to access the admin feature module or UrlTree to redirect the user to the login page instead:

import { Injectable } from '@angular/core'; import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot, CanActivateChild, UrlTree } from '@angular/router'; import { AuthService } from './auth.service'; @Injectable({ providedIn: 'root', }) export class AuthGuard implements CanActivate, CanActivateChild { constructor(private authService: AuthService, private router: Router) {} canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot): true|UrlTree { const url: string = state.url; return this.checkLogin(url); } canActivateChild( route: ActivatedRouteSnapshot, state: RouterStateSnapshot): true|UrlTree { return this.canActivate(route, state); } /* . . . */ }
src/app/auth/auth.guard.ts (excerpt)
      
      import { Injectable } from '@angular/core';
import {
  CanActivate, Router,
  ActivatedRouteSnapshot,
  RouterStateSnapshot,
  CanActivateChild,
  UrlTree
} from '@angular/router';
import { AuthService } from './auth.service';

@Injectable({
  providedIn: 'root',
})
export class AuthGuard implements CanActivate, CanActivateChild {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): true|UrlTree {
    const url: string = state.url;

    return this.checkLogin(url);
  }

  canActivateChild(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): true|UrlTree {
    return this.canActivate(route, state);
  }

/* . . . */
}
    

同樣把這個 AuthGuard 新增到“無元件的”管理路由,來同時保護它的所有子路由,而不是為每個路由單獨新增這個 AuthGuard

Add the same AuthGuard to the component-less admin route to protect all other child routes at one time instead of adding the AuthGuard to each route individually.

const adminRoutes: Routes = [ { path: 'admin', component: AdminComponent, canActivate: [AuthGuard], children: [ { path: '', canActivateChild: [AuthGuard], children: [ { path: 'crises', component: ManageCrisesComponent }, { path: 'heroes', component: ManageHeroesComponent }, { path: '', component: AdminDashboardComponent } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(adminRoutes) ], exports: [ RouterModule ] }) export class AdminRoutingModule {}
src/app/admin/admin-routing.module.ts (excerpt)
      
      const adminRoutes: Routes = [
  {
    path: 'admin',
    component: AdminComponent,
    canActivate: [AuthGuard],
    children: [
      {
        path: '',
        canActivateChild: [AuthGuard],
        children: [
          { path: 'crises', component: ManageCrisesComponent },
          { path: 'heroes', component: ManageHeroesComponent },
          { path: '', component: AdminDashboardComponent }
        ]
      }
    ]
  }
];

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

CanDeactivate:處理未儲存的更改

CanDeactivate: handling unsaved changes

回到 “Heroes” 工作流,該應用會立即接受對英雄的每次更改,而不進行驗證。

Back in the "Heroes" workflow, the app accepts every change to a hero immediately without validation.

在現實世界,你可能不得不積累來自使用者的更改,跨欄位驗證,在伺服器上驗證,或者把變更保持在待定狀態,直到使用者確認這一組欄位或取消並還原所有變更為止。

In the real world, you might have to accumulate the users changes, validate across fields, validate on the server, or hold changes in a pending state until the user confirms them as a group or cancels and reverts all changes.

當用戶要導航離開時,你可以讓使用者自己決定該怎麼處理這些未儲存的更改。 如果使用者選擇了取消,你就留下來,並允許更多改動。 如果使用者選擇了確認,那就進行儲存。

When the user navigates away, you can let the user decide what to do with unsaved changes. If the user cancels, you'll stay put and allow more changes. If the user approves, the app can save.

在儲存成功之前,你還可以繼續推遲導航。如果你讓使用者立即移到下一個介面,而儲存卻失敗了(可能因為資料不符合有效性規則),你就會丟失該錯誤的上下文環境。

You still might delay navigation until the save succeeds. If you let the user move to the next screen immediately and saving were to fail (perhaps the data is ruled invalid), you would lose the context of the error.

你需要用非同步的方式等待,在伺服器返回答覆之前先停止導航。

You need to stop the navigation while you wait, asynchronously, for the server to return with its answer.

CanDeactivate 守衛能幫助你決定如何處理未儲存的更改,以及如何處理。

The CanDeactivate guard helps you decide what to do with unsaved changes and how to proceed.

取消與儲存

Cancel and save

使用者在 CrisisDetailComponent 中更新危機資訊。 與 HeroDetailComponent 不同,使用者的改動不會立即更新危機的實體物件。當用戶按下了 Save 按鈕時,應用就更新這個實體物件;如果按了 Cancel 按鈕,那就放棄這些更改。

Users update crisis information in the CrisisDetailComponent. Unlike the HeroDetailComponent, the user changes do not update the crisis entity immediately. Instead, the app updates the entity when the user presses the Save button and discards the changes when the user presses the Cancel button.

這兩個按鈕都會在儲存或取消之後導航回危機列表。

Both buttons navigate back to the crisis list after save or cancel.

cancel() { this.gotoCrises(); } save() { this.crisis.name = this.editName; this.gotoCrises(); }
src/app/crisis-center/crisis-detail/crisis-detail.component.ts (cancel and save methods)
      
      cancel() {
  this.gotoCrises();
}

save() {
  this.crisis.name = this.editName;
  this.gotoCrises();
}
    

在這種情況下,使用者可以點選 heroes 連結,取消,按下瀏覽器後退按鈕,或者不儲存就離開。

In this scenario, the user could click the heroes link, cancel, push the browser back button, or navigate away without saving.

這個示例應用會彈出一個確認對話方塊,它會非同步等待使用者的響應,等使用者給出一個明確的答覆。

This example app asks the user to be explicit with a confirmation dialog box that waits asynchronously for the user's response.

你也可以用同步的方式等使用者的答覆,阻塞程式碼。但如果能用非同步的方式等待使用者的答覆,應用就會響應性更好,還能同時做別的事。

You could wait for the user's answer with synchronous, blocking code, however, the app is more responsive—and can do other work—by waiting for the user's answer asynchronously.

產生一個 Dialog 服務,以處理使用者的確認操作。

Generate a Dialog service to handle user confirmation.

ng generate service dialog
      
      ng generate service dialog
    

DialogService 新增一個 confirm() 方法,以提醒使用者確認。window.confirm 是一個阻塞型操作,它會顯示一個模態對話方塊,並等待使用者的互動。

Add a confirm() method to the DialogService to prompt the user to confirm their intent. The window.confirm is a blocking action that displays a modal dialog and waits for user interaction.

import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; /** * Async modal dialog service * DialogService makes this app easier to test by faking this service. * TODO: better modal implementation that doesn't use window.confirm */ @Injectable({ providedIn: 'root', }) export class DialogService { /** * Ask user to confirm an action. `message` explains the action and choices. * Returns observable resolving to `true`=confirm or `false`=cancel */ confirm(message?: string): Observable<boolean> { const confirmation = window.confirm(message || 'Is it OK?'); return of(confirmation); } }
src/app/dialog.service.ts
      
      import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';

/**
 * Async modal dialog service
 * DialogService makes this app easier to test by faking this service.
 * TODO: better modal implementation that doesn't use window.confirm
 */
@Injectable({
  providedIn: 'root',
})
export class DialogService {
  /**
   * Ask user to confirm an action. `message` explains the action and choices.
   * Returns observable resolving to `true`=confirm or `false`=cancel
   */
  confirm(message?: string): Observable<boolean> {
    const confirmation = window.confirm(message || 'Is it OK?');

    return of(confirmation);
  }
}
    

它返回observable,當用戶最終決定了如何去做時,它就會被解析 —— 或者決定放棄更改直接導航離開(true),或者保留未完成的修改,留在危機編輯器中(false)。

It returns an Observable that resolves when the user eventually decides what to do: either to discard changes and navigate away (true) or to preserve the pending changes and stay in the crisis editor (false).

產生一個守衛(guard),以檢查元件(任意元件均可)中是否存在 canDeactivate() 方法。

Generate a guard that checks for the presence of a canDeactivate() method in a component—any component.

ng generate guard can-deactivate
      
      ng generate guard can-deactivate
    

把下面的程式碼貼上到守衛中。

Paste the following code into your guard.

import { Injectable } from '@angular/core'; import { CanDeactivate } from '@angular/router'; import { Observable } from 'rxjs'; export interface CanComponentDeactivate { canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean; } @Injectable({ providedIn: 'root', }) export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> { canDeactivate(component: CanComponentDeactivate) { return component.canDeactivate ? component.canDeactivate() : true; } }
src/app/can-deactivate.guard.ts
      
      import { Injectable } from '@angular/core';
import { CanDeactivate } from '@angular/router';
import { Observable } from 'rxjs';

export interface CanComponentDeactivate {
 canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean;
}

@Injectable({
  providedIn: 'root',
})
export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> {
  canDeactivate(component: CanComponentDeactivate) {
    return component.canDeactivate ? component.canDeactivate() : true;
  }
}
    

守衛不需要知道哪個元件有 deactivate 方法,它可以檢測 CrisisDetailComponent 元件有沒有 canDeactivate() 方法並呼叫它。守衛在不知道任何元件 deactivate 方法細節的情況下,就能讓這個守衛重複使用。

While the guard doesn't have to know which component has a deactivate method, it can detect that the CrisisDetailComponent component has the canDeactivate() method and call it. The guard not knowing the details of any component's deactivation method makes the guard reusable.

另外,你也可以為 CrisisDetailComponent 建立一個特定的 CanDeactivate 守衛。 在需要訪問外部資訊時,canDeactivate() 方法為你提供了元件、ActivatedRouteRouterStateSnapshot 的當前實例。 如果只想為這個元件使用該守衛,並且需要獲取該元件屬性或確認路由器是否允許從該元件導航出去時,這會非常有用。

Alternatively, you could make a component-specific CanDeactivate guard for the CrisisDetailComponent. The canDeactivate() method provides you with the current instance of the component, the current ActivatedRoute, and RouterStateSnapshot in case you needed to access some external information. This would be useful if you only wanted to use this guard for this component and needed to get the component's properties or confirm whether the router should allow navigation away from it.

import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { CanDeactivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; import { CrisisDetailComponent } from './crisis-center/crisis-detail/crisis-detail.component'; @Injectable({ providedIn: 'root', }) export class CanDeactivateGuard implements CanDeactivate<CrisisDetailComponent> { canDeactivate( component: CrisisDetailComponent, route: ActivatedRouteSnapshot, state: RouterStateSnapshot ): Observable<boolean> | boolean { // Get the Crisis Center ID console.log(route.paramMap.get('id')); // Get the current URL console.log(state.url); // Allow synchronous navigation (`true`) if no crisis or the crisis is unchanged if (!component.crisis || component.crisis.name === component.editName) { return true; } // Otherwise ask the user with the dialog service and return its // observable which resolves to true or false when the user decides return component.dialogService.confirm('Discard changes?'); } }
src/app/can-deactivate.guard.ts (component-specific)
      
      import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { CanDeactivate,
         ActivatedRouteSnapshot,
         RouterStateSnapshot } from '@angular/router';

import { CrisisDetailComponent } from './crisis-center/crisis-detail/crisis-detail.component';

@Injectable({
  providedIn: 'root',
})
export class CanDeactivateGuard implements CanDeactivate<CrisisDetailComponent> {

  canDeactivate(
    component: CrisisDetailComponent,
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot
  ): Observable<boolean> | boolean {
    // Get the Crisis Center ID
    console.log(route.paramMap.get('id'));

    // Get the current URL
    console.log(state.url);

    // Allow synchronous navigation (`true`) if no crisis or the crisis is unchanged
    if (!component.crisis || component.crisis.name === component.editName) {
      return true;
    }
    // Otherwise ask the user with the dialog service and return its
    // observable which resolves to true or false when the user decides
    return component.dialogService.confirm('Discard changes?');
  }
}
    

看看 CrisisDetailComponent 元件,它已經實現了對未儲存的更改進行確認的工作流。

Looking back at the CrisisDetailComponent, it implements the confirmation workflow for unsaved changes.

canDeactivate(): Observable<boolean> | boolean { // Allow synchronous navigation (`true`) if no crisis or the crisis is unchanged if (!this.crisis || this.crisis.name === this.editName) { return true; } // Otherwise ask the user with the dialog service and return its // observable which resolves to true or false when the user decides return this.dialogService.confirm('Discard changes?'); }
src/app/crisis-center/crisis-detail/crisis-detail.component.ts (excerpt)
      
      canDeactivate(): Observable<boolean> | boolean {
  // Allow synchronous navigation (`true`) if no crisis or the crisis is unchanged
  if (!this.crisis || this.crisis.name === this.editName) {
    return true;
  }
  // Otherwise ask the user with the dialog service and return its
  // observable which resolves to true or false when the user decides
  return this.dialogService.confirm('Discard changes?');
}
    

注意,canDeactivate() 方法可以同步返回;如果沒有危機,或者沒有待處理的更改,它會立即返回 true。但它也能返回一個 Promise 或一個 Observable,路由器也會等待它解析為真值(導航)或偽造(停留在當前路由上)。

Notice that the canDeactivate() method can return synchronously; it returns true immediately if there is no crisis or there are no pending changes. But it can also return a Promise or an Observable and the router will wait for that to resolve to truthy (navigate) or falsy (stay on the current route).

crisis-center.routing.module.ts 的危機詳情路由中用 canDeactivate 陣列新增一個 Guard(守衛)。

Add the Guard to the crisis detail route in crisis-center-routing.module.ts using the canDeactivate array property.

import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CrisisCenterHomeComponent } from './crisis-center-home/crisis-center-home.component'; import { CrisisListComponent } from './crisis-list/crisis-list.component'; import { CrisisCenterComponent } from './crisis-center/crisis-center.component'; import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component'; import { CanDeactivateGuard } from '../can-deactivate.guard'; const crisisCenterRoutes: Routes = [ { path: 'crisis-center', component: CrisisCenterComponent, children: [ { path: '', component: CrisisListComponent, children: [ { path: ':id', component: CrisisDetailComponent, canDeactivate: [CanDeactivateGuard] }, { path: '', component: CrisisCenterHomeComponent } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(crisisCenterRoutes) ], exports: [ RouterModule ] }) export class CrisisCenterRoutingModule { }
src/app/crisis-center/crisis-center-routing.module.ts (can deactivate guard)
      
      import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { CrisisCenterHomeComponent } from './crisis-center-home/crisis-center-home.component';
import { CrisisListComponent } from './crisis-list/crisis-list.component';
import { CrisisCenterComponent } from './crisis-center/crisis-center.component';
import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component';

import { CanDeactivateGuard } from '../can-deactivate.guard';

const crisisCenterRoutes: Routes = [
  {
    path: 'crisis-center',
    component: CrisisCenterComponent,
    children: [
      {
        path: '',
        component: CrisisListComponent,
        children: [
          {
            path: ':id',
            component: CrisisDetailComponent,
            canDeactivate: [CanDeactivateGuard]
          },
          {
            path: '',
            component: CrisisCenterHomeComponent
          }
        ]
      }
    ]
  }
];

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

現在,你已經給了使用者一個能保護未儲存更改的安全守衛。

Now you have given the user a safeguard against unsaved changes.

Resolve: 預先獲取元件資料

Resolve: pre-fetching component data

Hero DetailCrisis Detail 中,它們等待路由讀取完對應的英雄和危機。

In the Hero Detail and Crisis Detail, the app waited until the route was activated to fetch the respective hero or crisis.

如果你在使用真實 api,很有可能資料返回有延遲,導致無法即時顯示。 在這種情況下,直到資料到達前,顯示一個空的元件不是最好的使用者體驗。

If you were using a real world API, there might be some delay before the data to display is returned from the server. You don't want to display a blank component while waiting for the data.

最好使用解析器預先從伺服器上獲取完資料,這樣在路由啟用的那一刻資料就準備好了。 還要在路由到此元件之前處理好錯誤。 但當某個 id 無法對應到一個危機詳情時,就沒辦法處理它。 這時最好把使用者帶回到“危機列表”中,那裡顯示了所有有效的“危機”。

To improve this behavior, you can pre-fetch data from the server using a resolver so it's ready the moment the route is activated. This also allows you to handle errors before routing to the component. There's no point in navigating to a crisis detail for an id that doesn't have a record. It'd be better to send the user back to the Crisis List that shows only valid crisis centers.

總之,你希望的是只有當所有必要資料都已經拿到之後,才渲染這個路由元件。

In summary, you want to delay rendering the routed component until all necessary data has been fetched.

導航前預先載入路由資訊

Fetch data before navigating

目前,CrisisDetailComponent 會接收選中的危機。 如果該危機沒有找到,路由器就會導航回危機列表檢視。

At the moment, the CrisisDetailComponent retrieves the selected crisis. If the crisis is not found, the router navigates back to the crisis list view.

如果能在該路由將要啟用時提前處理了這個問題,那麼使用者體驗會更好。 CrisisDetailResolver 服務可以接收一個 Crisis,而如果這個 Crisis 不存在,就會在啟用該路由並建立 CrisisDetailComponent 之前先行離開。

The experience might be better if all of this were handled first, before the route is activated. A CrisisDetailResolver service could retrieve a Crisis or navigate away, if the Crisis did not exist, before activating the route and creating the CrisisDetailComponent.

Crisis Center 特性區產生一個 CrisisDetailResolver 服務檔案。

Generate a CrisisDetailResolver service file within the Crisis Center feature area.

ng generate service crisis-center/crisis-detail-resolver
      
      ng generate service crisis-center/crisis-detail-resolver
    
import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root', }) export class CrisisDetailResolverService { constructor() { } }
src/app/crisis-center/crisis-detail-resolver.service.ts (generated)
      
      import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class CrisisDetailResolverService {

  constructor() { }

}
    

CrisisDetailComponent.ngOnInit() 中與危機檢索有關的邏輯移到 CrisisDetailResolverService 中。 匯入 Crisis 模型、CrisisServiceRouter 以便讓你可以在找不到指定的危機時導航到別處。

Move the relevant parts of the crisis retrieval logic in CrisisDetailComponent.ngOnInit() into the CrisisDetailResolverService. Import the Crisis model, CrisisService, and the Router so you can navigate elsewhere if you can't fetch the crisis.

為了更明確一點,可以實現一個帶有 Crisis 型別的 Resolve 介面。

Be explicit and implement the Resolve interface with a type of Crisis.

注入 CrisisServiceRouter,並實現 resolve() 方法。 該方法可以返回一個 Promise、一個 Observable 來支援非同步方式,或者直接返回一個值來支援同步方式。

Inject the CrisisService and Router and implement the resolve() method. That method could return a Promise, an Observable, or a synchronous return value.

CrisisService.getCrisis() 方法返回一個可觀察物件,以防止在資料獲取完之前載入本路由。 Router 守衛要求這個可觀察物件必須可結束(complete),也就是說它已經發出了所有值。 你可以為 take 運算子傳入一個引數 1,以確保這個可觀察物件會在從 getCrisis 方法所返回的可觀察物件中取到第一個值之後就會結束。

The CrisisService.getCrisis() method returns an observable in order to prevent the route from loading until the data is fetched. The Router guards require an observable to complete, which means it has emitted all of its values. You use the take operator with an argument of 1 to ensure that the Observable completes after retrieving the first value from the Observable returned by the getCrisis() method.

如果它沒有返回有效的 Crisis,就會返回一個 Observable,以取消以前到 CrisisDetailComponent 的在途導航,並把使用者導航回 CrisisListComponent。修改後的 resolver 服務是這樣的:

If it doesn't return a valid Crisis, then return an empty Observable, cancel the previous in-progress navigation to the CrisisDetailComponent, and navigate the user back to the CrisisListComponent. The updated resolver service looks like this:

import { Injectable } from '@angular/core'; import { Router, Resolve, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router'; import { Observable, of, EMPTY } from 'rxjs'; import { mergeMap, take } from 'rxjs/operators'; import { CrisisService } from './crisis.service'; import { Crisis } from './crisis'; @Injectable({ providedIn: 'root', }) export class CrisisDetailResolverService implements Resolve<Crisis> { constructor(private cs: CrisisService, private router: Router) {} resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Crisis> | Observable<never> { const id = route.paramMap.get('id'); return this.cs.getCrisis(id).pipe( take(1), mergeMap(crisis => { if (crisis) { return of(crisis); } else { // id not found this.router.navigate(['/crisis-center']); return EMPTY; } }) ); } }
src/app/crisis-center/crisis-detail-resolver.service.ts
      
      import { Injectable } from '@angular/core';
import {
  Router, Resolve,
  RouterStateSnapshot,
  ActivatedRouteSnapshot
} from '@angular/router';
import { Observable, of, EMPTY } from 'rxjs';
import { mergeMap, take } from 'rxjs/operators';

import { CrisisService } from './crisis.service';
import { Crisis } from './crisis';

@Injectable({
  providedIn: 'root',
})
export class CrisisDetailResolverService implements Resolve<Crisis> {
  constructor(private cs: CrisisService, private router: Router) {}

  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Crisis> | Observable<never> {
    const id = route.paramMap.get('id');

    return this.cs.getCrisis(id).pipe(
      take(1),
      mergeMap(crisis => {
        if (crisis) {
          return of(crisis);
        } else { // id not found
          this.router.navigate(['/crisis-center']);
          return EMPTY;
        }
      })
    );
  }
}
    

把這個解析器(resolver)匯入到 crisis-center-routing.module.ts 中,並往 CrisisDetailComponent 的路由配置中新增一個 resolve 物件。

Import this resolver in the crisis-center-routing.module.ts and add a resolve object to the CrisisDetailComponent route configuration.

import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CrisisCenterHomeComponent } from './crisis-center-home/crisis-center-home.component'; import { CrisisListComponent } from './crisis-list/crisis-list.component'; import { CrisisCenterComponent } from './crisis-center/crisis-center.component'; import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component'; import { CanDeactivateGuard } from '../can-deactivate.guard'; import { CrisisDetailResolverService } from './crisis-detail-resolver.service'; const crisisCenterRoutes: Routes = [ { path: 'crisis-center', component: CrisisCenterComponent, children: [ { path: '', component: CrisisListComponent, children: [ { path: ':id', component: CrisisDetailComponent, canDeactivate: [CanDeactivateGuard], resolve: { crisis: CrisisDetailResolverService } }, { path: '', component: CrisisCenterHomeComponent } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(crisisCenterRoutes) ], exports: [ RouterModule ] }) export class CrisisCenterRoutingModule { }
src/app/crisis-center/crisis-center-routing.module.ts (resolver)
      
      import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { CrisisCenterHomeComponent } from './crisis-center-home/crisis-center-home.component';
import { CrisisListComponent } from './crisis-list/crisis-list.component';
import { CrisisCenterComponent } from './crisis-center/crisis-center.component';
import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component';

import { CanDeactivateGuard } from '../can-deactivate.guard';
import { CrisisDetailResolverService } from './crisis-detail-resolver.service';

const crisisCenterRoutes: Routes = [
  {
    path: 'crisis-center',
    component: CrisisCenterComponent,
    children: [
      {
        path: '',
        component: CrisisListComponent,
        children: [
          {
            path: ':id',
            component: CrisisDetailComponent,
            canDeactivate: [CanDeactivateGuard],
            resolve: {
              crisis: CrisisDetailResolverService
            }
          },
          {
            path: '',
            component: CrisisCenterHomeComponent
          }
        ]
      }
    ]
  }
];

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

CrisisDetailComponent 不應該再去獲取這個危機的詳情。 你只要重新配置路由,就可以修改從哪裡獲取危機的詳情。 把 CrisisDetailComponent 改成從 ActivatedRoute.data.crisis 屬性中獲取危機詳情,這正是你重新配置路由的恰當時機。

The CrisisDetailComponent should no longer fetch the crisis. When you re-configured the route, you changed where the crisis is. Update the CrisisDetailComponent to get the crisis from the ActivatedRoute.data.crisis property instead;

ngOnInit() { this.route.data .subscribe((data: { crisis: Crisis }) => { this.editName = data.crisis.name; this.crisis = data.crisis; }); }
src/app/crisis-center/crisis-detail/crisis-detail.component.ts (ngOnInit v2)
      
      ngOnInit() {
  this.route.data
    .subscribe((data: { crisis: Crisis }) => {
      this.editName = data.crisis.name;
      this.crisis = data.crisis;
    });
}
    

注意以下三個要點:

Note the following three important points:

  1. 路由器的這個 Resolve 介面是可選的。CrisisDetailResolverService 沒有繼承自某個基底類別。路由器只要找到了這個方法,就會呼叫它。

    The router's Resolve interface is optional. The CrisisDetailResolverService doesn't inherit from a base class. The router looks for that method and calls it if found.

  2. 路由器會在使用者可以導航的任何情況下呼叫該解析器,這樣你就不用針對每個用例都編寫程式碼了。

    The router calls the resolver in any case where the the user could navigate away so you don't have to code for each use case.

  3. 在任何一個解析器中返回空的 Observable 就會取消導航。

    Returning an empty Observable in at least one resolver will cancel navigation.

與里程碑相關的危機中心程式碼如下。

The relevant Crisis Center code for this milestone follows.

<h1 class="title">Angular Router</h1> <nav> <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a> <a routerLink="/superheroes" routerLinkActive="active">Heroes</a> <a routerLink="/admin" routerLinkActive="active">Admin</a> <a routerLink="/login" routerLinkActive="active">Login</a> <a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a> </nav> <div [@routeAnimation]="getAnimationData(routerOutlet)"> <router-outlet #routerOutlet="outlet"></router-outlet> </div> <router-outlet name="popup"></router-outlet><p>Welcome to the Crisis Center</p><h2>CRISIS CENTER</h2> <router-outlet></router-outlet>import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CrisisCenterHomeComponent } from './crisis-center-home/crisis-center-home.component'; import { CrisisListComponent } from './crisis-list/crisis-list.component'; import { CrisisCenterComponent } from './crisis-center/crisis-center.component'; import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component'; import { CanDeactivateGuard } from '../can-deactivate.guard'; import { CrisisDetailResolverService } from './crisis-detail-resolver.service'; const crisisCenterRoutes: Routes = [ { path: 'crisis-center', component: CrisisCenterComponent, children: [ { path: '', component: CrisisListComponent, children: [ { path: ':id', component: CrisisDetailComponent, canDeactivate: [CanDeactivateGuard], resolve: { crisis: CrisisDetailResolverService } }, { path: '', component: CrisisCenterHomeComponent } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(crisisCenterRoutes) ], exports: [ RouterModule ] }) export class CrisisCenterRoutingModule { }<ul class="crises"> <li *ngFor="let crisis of crises$ | async" [class.selected]="crisis.id === selectedId"> <a [routerLink]="[crisis.id]"> <span class="badge">{{ crisis.id }}</span>{{ crisis.name }} </a> </li> </ul> <router-outlet></router-outlet>import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { CrisisService } from '../crisis.service'; import { Crisis } from '../crisis'; import { Observable } from 'rxjs'; import { switchMap } from 'rxjs/operators'; @Component({ selector: 'app-crisis-list', templateUrl: './crisis-list.component.html', styleUrls: ['./crisis-list.component.css'] }) export class CrisisListComponent implements OnInit { crises$: Observable<Crisis[]>; selectedId: number; constructor( private service: CrisisService, private route: ActivatedRoute ) {} ngOnInit() { this.crises$ = this.route.paramMap.pipe( switchMap(params => { this.selectedId = +params.get('id'); return this.service.getCrises(); }) ); } }<div *ngIf="crisis"> <h3>"{{ editName }}"</h3> <div> <label>Id: </label>{{ crisis.id }}</div> <div> <label>Name: </label> <input [(ngModel)]="editName" placeholder="name"/> </div> <p> <button (click)="save()">Save</button> <button (click)="cancel()">Cancel</button> </p> </div>import { Component, OnInit, HostBinding } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { Observable } from 'rxjs'; import { Crisis } from '../crisis'; import { DialogService } from '../../dialog.service'; @Component({ selector: 'app-crisis-detail', templateUrl: './crisis-detail.component.html', styleUrls: ['./crisis-detail.component.css'] }) export class CrisisDetailComponent implements OnInit { crisis: Crisis; editName: string; constructor( private route: ActivatedRoute, private router: Router, public dialogService: DialogService ) {} ngOnInit() { this.route.data .subscribe((data: { crisis: Crisis }) => { this.editName = data.crisis.name; this.crisis = data.crisis; }); } cancel() { this.gotoCrises(); } save() { this.crisis.name = this.editName; this.gotoCrises(); } canDeactivate(): Observable<boolean> | boolean { // Allow synchronous navigation (`true`) if no crisis or the crisis is unchanged if (!this.crisis || this.crisis.name === this.editName) { return true; } // Otherwise ask the user with the dialog service and return its // observable which resolves to true or false when the user decides return this.dialogService.confirm('Discard changes?'); } gotoCrises() { const crisisId = this.crisis ? this.crisis.id : null; // Pass along the crisis id if available // so that the CrisisListComponent can select that crisis. // Add a totally useless `foo` parameter for kicks. // Relative navigation back to the crises this.router.navigate(['../', { id: crisisId, foo: 'foo' }], { relativeTo: this.route }); } }import { Injectable } from '@angular/core'; import { Router, Resolve, RouterStateSnapshot, ActivatedRouteSnapshot } from '@angular/router'; import { Observable, of, EMPTY } from 'rxjs'; import { mergeMap, take } from 'rxjs/operators'; import { CrisisService } from './crisis.service'; import { Crisis } from './crisis'; @Injectable({ providedIn: 'root', }) export class CrisisDetailResolverService implements Resolve<Crisis> { constructor(private cs: CrisisService, private router: Router) {} resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<Crisis> | Observable<never> { const id = route.paramMap.get('id'); return this.cs.getCrisis(id).pipe( take(1), mergeMap(crisis => { if (crisis) { return of(crisis); } else { // id not found this.router.navigate(['/crisis-center']); return EMPTY; } }) ); } }import { BehaviorSubject } from 'rxjs'; import { map } from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { MessageService } from '../message.service'; import { Crisis } from './crisis'; import { CRISES } from './mock-crises'; @Injectable({ providedIn: 'root', }) export class CrisisService { static nextCrisisId = 100; private crises$: BehaviorSubject<Crisis[]> = new BehaviorSubject<Crisis[]>(CRISES); constructor(private messageService: MessageService) { } getCrises() { return this.crises$; } getCrisis(id: number | string) { return this.getCrises().pipe( map(crises => crises.find(crisis => crisis.id === +id)) ); } }import { Injectable } from '@angular/core'; import { Observable, of } from 'rxjs'; /** * Async modal dialog service * DialogService makes this app easier to test by faking this service. * TODO: better modal implementation that doesn't use window.confirm */ @Injectable({ providedIn: 'root', }) export class DialogService { /** * Ask user to confirm an action. `message` explains the action and choices. * Returns observable resolving to `true`=confirm or `false`=cancel */ confirm(message?: string): Observable<boolean> { const confirmation = window.confirm(message || 'Is it OK?'); return of(confirmation); } }
      
      <h1 class="title">Angular Router</h1>
<nav>
  <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a>
  <a routerLink="/superheroes" routerLinkActive="active">Heroes</a>
  <a routerLink="/admin" routerLinkActive="active">Admin</a>
  <a routerLink="/login" routerLinkActive="active">Login</a>
  <a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a>
</nav>
<div [@routeAnimation]="getAnimationData(routerOutlet)">
  <router-outlet #routerOutlet="outlet"></router-outlet>
</div>
<router-outlet name="popup"></router-outlet>
    

路由守衛

Guards

import { Injectable } from '@angular/core'; import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot, CanActivateChild, UrlTree } from '@angular/router'; import { AuthService } from './auth.service'; @Injectable({ providedIn: 'root', }) export class AuthGuard implements CanActivate, CanActivateChild { constructor(private authService: AuthService, private router: Router) {} canActivate( route: ActivatedRouteSnapshot, state: RouterStateSnapshot): true|UrlTree { const url: string = state.url; return this.checkLogin(url); } canActivateChild( route: ActivatedRouteSnapshot, state: RouterStateSnapshot): true|UrlTree { return this.canActivate(route, state); } checkLogin(url: string): true|UrlTree { if (this.authService.isLoggedIn) { return true; } // Store the attempted URL for redirecting this.authService.redirectUrl = url; // Redirect to the login page return this.router.parseUrl('/login'); } }import { Injectable } from '@angular/core'; import { CanDeactivate } from '@angular/router'; import { Observable } from 'rxjs'; export interface CanComponentDeactivate { canDeactivate: () => Observable<boolean> | Promise<boolean> | boolean; } @Injectable({ providedIn: 'root', }) export class CanDeactivateGuard implements CanDeactivate<CanComponentDeactivate> { canDeactivate(component: CanComponentDeactivate) { return component.canDeactivate ? component.canDeactivate() : true; } }
      
      import { Injectable } from '@angular/core';
import {
  CanActivate, Router,
  ActivatedRouteSnapshot,
  RouterStateSnapshot,
  CanActivateChild,
  UrlTree
} from '@angular/router';
import { AuthService } from './auth.service';

@Injectable({
  providedIn: 'root',
})
export class AuthGuard implements CanActivate, CanActivateChild {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): true|UrlTree {
    const url: string = state.url;

    return this.checkLogin(url);
  }

  canActivateChild(
    route: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): true|UrlTree {
    return this.canActivate(route, state);
  }

  checkLogin(url: string): true|UrlTree {
    if (this.authService.isLoggedIn) { return true; }

    // Store the attempted URL for redirecting
    this.authService.redirectUrl = url;

    // Redirect to the login page
    return this.router.parseUrl('/login');
  }
}
    

查詢引數及片段

Query parameters and fragments

路由引數部分,你只需要處理該路由的專屬引數。但是,你也可以用查詢引數來獲取對所有路由都可用的可選引數。

In the route parameters section, you only dealt with parameters specific to the route. However, you can use query parameters to get optional parameters available to all routes.

片段可以參考頁面中帶有特定 id 屬性的元素.

Fragments refer to certain elements on the page identified with an id attribute.

修改 AuthGuard 以提供 session_id 查詢引數,在導航到其它路由後,它還會存在。

Update the AuthGuard to provide a session_id query that will remain after navigating to another route.

再新增一個錨點(A)元素,來讓你能跳轉到頁面中的正確位置。

Add an anchor element so you can jump to a certain point on the page.

router.navigate() 方法新增一個 NavigationExtras 物件,用來導航到 /login 路由。

Add the NavigationExtras object to the router.navigate() method that navigates you to the /login route.

import { Injectable } from '@angular/core'; import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot, CanActivateChild, NavigationExtras, UrlTree } from '@angular/router'; import { AuthService } from './auth.service'; @Injectable({ providedIn: 'root', }) export class AuthGuard implements CanActivate, CanActivateChild { constructor(private authService: AuthService, private router: Router) {} canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): true|UrlTree { const url: string = state.url; return this.checkLogin(url); } canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): true|UrlTree { return this.canActivate(route, state); } checkLogin(url: string): true|UrlTree { if (this.authService.isLoggedIn) { return true; } // Store the attempted URL for redirecting this.authService.redirectUrl = url; // Create a dummy session id const sessionId = 123456789; // Set our navigation extras object // that contains our global query params and fragment const navigationExtras: NavigationExtras = { queryParams: { session_id: sessionId }, fragment: 'anchor' }; // Redirect to the login page with extras return this.router.createUrlTree(['/login'], navigationExtras); } }
src/app/auth/auth.guard.ts (v3)
      
      import { Injectable } from '@angular/core';
import {
  CanActivate, Router,
  ActivatedRouteSnapshot,
  RouterStateSnapshot,
  CanActivateChild,
  NavigationExtras,
  UrlTree
} from '@angular/router';
import { AuthService } from './auth.service';

@Injectable({
  providedIn: 'root',
})
export class AuthGuard implements CanActivate, CanActivateChild {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): true|UrlTree {
    const url: string = state.url;

    return this.checkLogin(url);
  }

  canActivateChild(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): true|UrlTree {
    return this.canActivate(route, state);
  }

  checkLogin(url: string): true|UrlTree {
    if (this.authService.isLoggedIn) { return true; }

    // Store the attempted URL for redirecting
    this.authService.redirectUrl = url;

    // Create a dummy session id
    const sessionId = 123456789;

    // Set our navigation extras object
    // that contains our global query params and fragment
    const navigationExtras: NavigationExtras = {
      queryParams: { session_id: sessionId },
      fragment: 'anchor'
    };

    // Redirect to the login page with extras
    return this.router.createUrlTree(['/login'], navigationExtras);
  }
}
    

還可以在導航之間保留查詢引數和片段,而無需再次在導航中提供。在 LoginComponent 中的 router.navigateUrl() 方法中,新增一個物件作為第二個引數,該物件提供了 queryParamsHandlingpreserveFragment,用於傳遞當前的查詢引數和片段到下一個路由。

You can also preserve query parameters and fragments across navigations without having to provide them again when navigating. In the LoginComponent, you'll add an object as the second argument in the router.navigateUrl() function and provide the queryParamsHandling and preserveFragment to pass along the current query parameters and fragment to the next route.

// Set our navigation extras object // that passes on our global query params and fragment const navigationExtras: NavigationExtras = { queryParamsHandling: 'preserve', preserveFragment: true }; // Redirect the user this.router.navigate([redirectUrl], navigationExtras);
src/app/auth/login/login.component.ts (preserve)
      
      // Set our navigation extras object
// that passes on our global query params and fragment
const navigationExtras: NavigationExtras = {
  queryParamsHandling: 'preserve',
  preserveFragment: true
};

// Redirect the user
this.router.navigate([redirectUrl], navigationExtras);
    

queryParamsHandling 特性還提供了 merge 選項,它將會在導航時保留當前的查詢引數,並與其它查詢引數合併。

The queryParamsHandling feature also provides a merge option, which preserves and combines the current query parameters with any provided query parameters when navigating.

要在登入後導航到 Admin Dashboard 路由,請更新 admin-dashboard.component.ts 以處理這些查詢引數和片段。

To navigate to the Admin Dashboard route after logging in, update admin-dashboard.component.ts to handle the query parameters and fragment.

import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @Component({ selector: 'app-admin-dashboard', templateUrl: './admin-dashboard.component.html', styleUrls: ['./admin-dashboard.component.css'] }) export class AdminDashboardComponent implements OnInit { sessionId: Observable<string>; token: Observable<string>; constructor(private route: ActivatedRoute) {} ngOnInit() { // Capture the session ID if available this.sessionId = this.route .queryParamMap .pipe(map(params => params.get('session_id') || 'None')); // Capture the fragment if available this.token = this.route .fragment .pipe(map(fragment => fragment || 'None')); } }
src/app/admin/admin-dashboard/admin-dashboard.component.ts (v2)
      
      import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

@Component({
  selector: 'app-admin-dashboard',
  templateUrl: './admin-dashboard.component.html',
  styleUrls: ['./admin-dashboard.component.css']
})
export class AdminDashboardComponent implements OnInit {
  sessionId: Observable<string>;
  token: Observable<string>;

  constructor(private route: ActivatedRoute) {}

  ngOnInit() {
    // Capture the session ID if available
    this.sessionId = this.route
      .queryParamMap
      .pipe(map(params => params.get('session_id') || 'None'));

    // Capture the fragment if available
    this.token = this.route
      .fragment
      .pipe(map(fragment => fragment || 'None'));
  }
}
    

查詢引數和片段可透過 Router 服務的 routerState 屬性使用。和路由引數類似,全域性查詢引數和片段也是 Observable 物件。 在修改過的英雄管理元件中,你將藉助 AsyncPipe 直接把 Observable 傳給範本。

Query parameters and fragments are also available through the ActivatedRoute service. Just like route parameters, the query parameters and fragments are provided as an Observable. The updated Crisis Admin component feeds the Observable directly into the template using the AsyncPipe.

按照下列步驟試驗下:點選 Admin 按鈕,它會帶著你提供的 queryParamMapfragment 跳轉到登入頁。 點選 Login 按鈕,你就會被重新導向到 Admin Dashboard 頁。 注意,它仍然帶著上一步提供的 queryParamMapfragment

Now, you can click on the Admin button, which takes you to the Login page with the provided queryParamMap and fragment. After you click the login button, notice that you have been redirected to the Admin Dashboard page with the query parameters and fragment still intact in the address bar.

你可以用這些持久化資訊來攜帶需要為每個頁面都提供的資訊,如認證令牌或會話的 ID 等。

You can use these persistent bits of information for things that need to be provided across pages like authentication tokens or session ids.

“查詢引數”和“片段”也可以分別用 RouterLink 中的 queryParamsHandlingpreserveFragment 儲存。

The query params and fragment can also be preserved using a RouterLink with the queryParamsHandling and preserveFragment bindings respectively.

里程碑 6:非同步路由

Milestone 6: Asynchronous routing

完成上面的里程碑後,應用程式很自然地長大了。在某一個時間點,你將達到一個頂點,應用將會需要過多的時間來載入。

As you've worked through the milestones, the application has naturally gotten larger. At some point you'll reach a point where the application takes a long time to load.

為了解決這個問題,請使用非同步路由,它會根據請求來延遲載入某些特性模組。延遲載入有很多好處。

To remedy this issue, use asynchronous routing, which loads feature modules lazily, on request. Lazy loading has multiple benefits.

  • 你可以只在使用者請求時才載入某些特性區。

    You can load feature areas only when requested by the user.

  • 對於那些只訪問應用程式某些區域的使用者,這樣能加快載入速度。

    You can speed up load time for users that only visit certain areas of the application.

  • 你可以持續擴充延遲載入特性區的功能,而不用增加初始載入的包體積。

    You can continue expanding lazy loaded feature areas without increasing the size of the initial load bundle.

你已經完成了一部分。透過把應用組織成一些模組:AppModuleHeroesModuleAdminModuleCrisisCenterModule, 你已經有了可用於實現延遲載入的候選者。

You're already part of the way there. By organizing the application into modules—AppModule, HeroesModule, AdminModule and CrisisCenterModule—you have natural candidates for lazy loading.

有些模組(比如 AppModule)必須在啟動時載入,但其它的都可以而且應該延遲載入。 比如 AdminModule 就只有少數已認證的使用者才需要它,所以你應該只有在正確的人請求它時才載入。

Some modules, like AppModule, must be loaded from the start. But others can and should be lazy loaded. The AdminModule, for example, is needed by a few authorized users, so you should only load it when requested by the right people.

延遲載入路由配置

Lazy Loading route configuration

admin-routing.module.ts 中的 admin 路徑從 'admin' 改為空路徑 ''

Change the admin path in the admin-routing.module.ts from 'admin' to an empty string, '', the empty path.

可以用空路徑路由來對路由進行分組,而不用往 URL 中新增額外的路徑片段。 使用者仍舊訪問 /admin,並且 AdminComponent 仍然作為用來包含子路由的路由元件。

Use empty path routes to group routes together without adding any additional path segments to the URL. Users will still visit /admin and the AdminComponent still serves as the Routing Component containing child routes.

開啟 AppRoutingModule,並把一個新的 admin 路由新增到它的 appRoutes 陣列中。

Open the AppRoutingModule and add a new admin route to its appRoutes array.

給它一個 loadChildren 屬性(替換掉 children 屬性)。 loadChildren 屬性接收一個函式,該函式使用瀏覽器內建的動態匯入語法 import('...') 來延遲載入程式碼,並返回一個承諾(Promise)。 其路徑是 AdminModule 的位置(相對於應用的根目錄)。 當代碼請求並載入完畢後,這個 Promise 就會解析成一個包含 NgModule 的物件,也就是 AdminModule

Give it a loadChildren property instead of a children property. The loadChildren property takes a function that returns a promise using the browser's built-in syntax for lazy loading code using dynamic imports import('...'). The path is the location of the AdminModule (relative to the app root). After the code is requested and loaded, the Promise resolves an object that contains the NgModule, in this case the AdminModule.

{ path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule), },
app-routing.module.ts (load children)
      
      {
  path: 'admin',
  loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
},
    

注意: 當使用絕對路徑時,NgModule 的檔案位置必須以 src/app 開頭,以便正確解析。對於自訂的 使用絕對路徑的路徑對映表,你必須在專案的 tsconfig.json 中必須配置好 baseUrlpaths 屬性。

Note: When using absolute paths, the NgModule file location must begin with src/app in order to resolve correctly. For custom path mapping with absolute paths, you must configure the baseUrl and paths properties in the project tsconfig.json.

當路由器導航到這個路由時,它會用 loadChildren 字串來動態載入 AdminModule,然後把 AdminModule 新增到當前的路由配置中, 最後,它把所請求的路由載入到目標 admin 元件中。

When the router navigates to this route, it uses the loadChildren string to dynamically load the AdminModule. Then it adds the AdminModule routes to its current route configuration. Finally, it loads the requested route to the destination admin component.

延遲載入和重新配置工作只會發生一次,也就是在該路由首次被請求時。在後續的請求中,該模組和路由都是立即可用的。

The lazy loading and re-configuration happen just once, when the route is first requested; the module and routes are available immediately for subsequent requests.

Angular 提供一個內建模組載入器,支援SystemJS來非同步載入模組。如果你使用其它捆綁工具比如 Webpack,則使用 Webpack 的機制來非同步載入模組。

Angular provides a built-in module loader that supports SystemJS to load modules asynchronously. If you were using another bundling tool, such as Webpack, you would use the Webpack mechanism for asynchronously loading modules.

最後一步是把管理特性區從主應用中完全分離開。 根模組 AppModule 既不能載入也不能參考 AdminModule 及其檔案。

Take the final step and detach the admin feature set from the main application. The root AppModule must neither load nor reference the AdminModule or its files.

app.module.ts 中,從頂部移除 AdminModule 的匯入語句,並且從 NgModule 的 imports 陣列中移除 AdminModule

In app.module.ts, remove the AdminModule import statement from the top of the file and remove the AdminModule from the NgModule's imports array.

CanLoad:保護對特性模組的未授權載入

CanLoad: guarding unauthorized loading of feature modules

你已經使用 CanActivate 保護 AdminModule 了,它會阻止未授權使用者訪問管理特性區。如果使用者未登入,它就會跳轉到登入頁。

You're already protecting the AdminModule with a CanActivate guard that prevents unauthorized users from accessing the admin feature area. It redirects to the login page if the user is not authorized.

但是路由器仍然會載入 AdminModule —— 即使使用者無法訪問它的任何一個元件。 理想的方式是,只有在使用者已登入的情況下你才載入 AdminModule

But the router is still loading the AdminModule even if the user can't visit any of its components. Ideally, you'd only load the AdminModule if the user is logged in.

新增一個 CanLoad 守衛,它只在使用者已登入並且嘗試訪問管理特性區的時候,才載入 AdminModule 一次。

Add a CanLoad guard that only loads the AdminModule once the user is logged in and attempts to access the admin feature area.

現有的 AuthGuardcheckLogin() 方法中已經有了支援 CanLoad 守衛的基礎邏輯。

The existing AuthGuard already has the essential logic in its checkLogin() method to support the CanLoad guard.

開啟 auth.guard.ts,從 @angular/router 中匯入 CanLoad 介面。 把它新增到 AuthGuard 類別的 implements 列表中。 然後實現 canLoad,程式碼如下:

Open auth.guard.ts. Import the CanLoad interface from @angular/router. Add it to the AuthGuard class's implements list. Then implement canLoad() as follows:

canLoad(route: Route): boolean { const url = `/${route.path}`; return this.checkLogin(url); }
src/app/auth/auth.guard.ts (CanLoad guard)
      
      canLoad(route: Route): boolean {
  const url = `/${route.path}`;

  return this.checkLogin(url);
}
    

路由器會把 canLoad() 方法的 route 引數設定為準備訪問的目標 URL。 如果使用者已經登入了,checkLogin() 方法就會重新導向到那個 URL。

The router sets the canLoad() method's route parameter to the intended destination URL. The checkLogin() method redirects to that URL once the user has logged in.

現在,把 AuthGuard 匯入到 AppRoutingModule 中,並把 AuthGuard 新增到 admin 路由的 canLoad 陣列中。 完整的 admin 路由是這樣的:

Now import the AuthGuard into the AppRoutingModule and add the AuthGuard to the canLoad array property for the admin route. The completed admin route looks like this:

{ path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule), canLoad: [AuthGuard] },
app-routing.module.ts (lazy admin route)
      
      {
  path: 'admin',
  loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
  canLoad: [AuthGuard]
},
    

預載入:特性區的後臺載入

Preloading: background loading of feature areas

除了按需載入模組外,還可以透過預載入方式非同步載入模組。

In addition to loading modules on-demand, you can load modules asynchronously with preloading.

當應用啟動時,AppModule 被急性載入,這意味著它會立即載入。而 AdminModule 只在使用者點選連結時載入,這叫做延遲載入。

The AppModule is eagerly loaded when the application starts, meaning that it loads right away. Now the AdminModule loads only when the user clicks on a link, which is called lazy loading.

預載入允許你在後臺載入模組,以便當使用者啟用某個特定的路由時,就可以渲染這些資料了。 考慮一下危機中心。 它不是使用者看到的第一個檢視。 預設情況下,英雄列表才是第一個檢視。為了獲得最小的初始有效負載和最快的啟動時間,你應該急性載入 AppModuleHeroesModule

Preloading allows you to load modules in the background so that the data is ready to render when the user activates a particular route. Consider the Crisis Center. It isn't the first view that a user sees. By default, the Heroes are the first view. For the smallest initial payload and fastest launch time, you should eagerly load the AppModule and the HeroesModule.

你可以延遲載入危機中心。 但是,你幾乎可以肯定使用者會在啟動應用之後的幾分鐘內訪問危機中心。 理想情況下,應用啟動時應該只載入 AppModuleHeroesModule,然後幾乎立即開始後臺載入 CrisisCenterModule。 在使用者瀏覽到危機中心之前,該模組應該已經載入完畢,可供訪問了。

You could lazy load the Crisis Center. But you're almost certain that the user will visit the Crisis Center within minutes of launching the app. Ideally, the app would launch with just the AppModule and the HeroesModule loaded and then, almost immediately, load the CrisisCenterModule in the background. By the time the user navigates to the Crisis Center, its module will have been loaded and ready.

預載入的工作原理

How preloading works

在每次成功的導航後,路由器會在自己的配置中查詢尚未載入並且可以預載入的模組。 是否載入某個模組,以及要載入哪些模組,取決於預載入策略

After each successful navigation, the router looks in its configuration for an unloaded module that it can preload. Whether it preloads a module, and which modules it preloads, depends upon the preload strategy.

Router 提供了兩種預載入策略:

The Router offers two preloading strategies:

  • 完全不預載入,這是預設值。延遲載入的特性區仍然會按需載入。

    No preloading, which is the default. Lazy loaded feature areas are still loaded on-demand.

  • 預載入所有延遲載入的特性區。

    Preloading of all lazy loaded feature areas.

路由器或者完全不預載入或者預載入每個延遲載入模組。 路由器還支援自訂預載入策略,以便完全控制要預載入哪些模組以及何時載入。

The router either never preloads, or preloads every lazy loaded module. The Router also supports custom preloading strategies for fine control over which modules to preload and when.

本節將指導你把 CrisisCenterModule 改成延遲載入的,並使用 PreloadAllModules 策略來預載入所有延遲載入模組。

This section guides you through updating the CrisisCenterModule to load lazily by default and use the PreloadAllModules strategy to load all lazy loaded modules.

延遲載入危機中心

Lazy load the crisis center

修改路由配置,來延遲載入 CrisisCenterModule。修改的步驟和配置延遲載入 AdminModule 時一樣。

Update the route configuration to lazy load the CrisisCenterModule. Take the same steps you used to configure AdminModule for lazy loading.

  1. CrisisCenterRoutingModule 中的路徑從 crisis-center 改為空字串。

    Change the crisis-center path in the CrisisCenterRoutingModule to an empty string.

  2. AppRoutingModule 中新增一個 crisis-center 路由。

    Add a crisis-center route to the AppRoutingModule.

  3. 設定 loadChildren 字串來載入 CrisisCenterModule

    Set the loadChildren string to load the CrisisCenterModule.

  4. app.module.ts 中移除所有對 CrisisCenterModule 的參考。

    Remove all mention of the CrisisCenterModule from app.module.ts.

下面是開啟預載入之前的模組修改版:

Here are the updated modules before enabling preload:

import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { Router } from '@angular/router'; import { AppComponent } from './app.component'; import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; import { ComposeMessageComponent } from './compose-message/compose-message.component'; import { AppRoutingModule } from './app-routing.module'; import { HeroesModule } from './heroes/heroes.module'; import { AuthModule } from './auth/auth.module'; @NgModule({ imports: [ BrowserModule, BrowserAnimationsModule, FormsModule, HeroesModule, AuthModule, AppRoutingModule, ], declarations: [ AppComponent, ComposeMessageComponent, PageNotFoundComponent ], bootstrap: [ AppComponent ] }) export class AppModule { }import { NgModule } from '@angular/core'; import { RouterModule, Routes, } from '@angular/router'; import { ComposeMessageComponent } from './compose-message/compose-message.component'; import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; import { AuthGuard } from './auth/auth.guard'; const appRoutes: Routes = [ { path: 'compose', component: ComposeMessageComponent, outlet: 'popup' }, { path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule), canLoad: [AuthGuard] }, { path: 'crisis-center', loadChildren: () => import('./crisis-center/crisis-center.module').then(m => m.CrisisCenterModule) }, { path: '', redirectTo: '/heroes', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ]; @NgModule({ imports: [ RouterModule.forRoot( appRoutes, ) ], exports: [ RouterModule ] }) export class AppRoutingModule {}import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { CrisisCenterHomeComponent } from './crisis-center-home/crisis-center-home.component'; import { CrisisListComponent } from './crisis-list/crisis-list.component'; import { CrisisCenterComponent } from './crisis-center/crisis-center.component'; import { CrisisDetailComponent } from './crisis-detail/crisis-detail.component'; import { CanDeactivateGuard } from '../can-deactivate.guard'; import { CrisisDetailResolverService } from './crisis-detail-resolver.service'; const crisisCenterRoutes: Routes = [ { path: '', component: CrisisCenterComponent, children: [ { path: '', component: CrisisListComponent, children: [ { path: ':id', component: CrisisDetailComponent, canDeactivate: [CanDeactivateGuard], resolve: { crisis: CrisisDetailResolverService } }, { path: '', component: CrisisCenterHomeComponent } ] } ] } ]; @NgModule({ imports: [ RouterModule.forChild(crisisCenterRoutes) ], exports: [ RouterModule ] }) export class CrisisCenterRoutingModule { }
      
      import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

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

import { AppComponent } from './app.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
import { ComposeMessageComponent } from './compose-message/compose-message.component';

import { AppRoutingModule } from './app-routing.module';
import { HeroesModule } from './heroes/heroes.module';
import { AuthModule } from './auth/auth.module';

@NgModule({
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    FormsModule,
    HeroesModule,
    AuthModule,
    AppRoutingModule,
  ],
  declarations: [
    AppComponent,
    ComposeMessageComponent,
    PageNotFoundComponent
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule {
}
    

你可以現在嘗試它,並確認在點選了“Crisis Center”按鈕之後載入了 CrisisCenterModule

You could try this now and confirm that the CrisisCenterModule loads after you click the "Crisis Center" button.

要為所有延遲載入模組啟用預載入功能,請從 Angular 的路由模組中匯入 PreloadAllModules

To enable preloading of all lazy loaded modules, import the PreloadAllModules token from the Angular router package.

RouterModule.forRoot() 方法的第二個引數接受一個附加配置選項物件。 preloadingStrategy 就是其中之一。 把 PreloadAllModules 新增到 forRoot() 呼叫中:

The second argument in the RouterModule.forRoot() method takes an object for additional configuration options. The preloadingStrategy is one of those options. Add the PreloadAllModules token to the forRoot() call:

RouterModule.forRoot( appRoutes, { enableTracing: true, // <-- debugging purposes only preloadingStrategy: PreloadAllModules } )
src/app/app-routing.module.ts (preload all)
      
      RouterModule.forRoot(
  appRoutes,
  {
    enableTracing: true, // <-- debugging purposes only
    preloadingStrategy: PreloadAllModules
  }
)
    

這項配置會讓 Router 預載入器立即載入所有延遲載入路由(帶 loadChildren 屬性的路由)。

This configures the Router preloader to immediately load all lazy loaded routes (routes with a loadChildren property).

當訪問 http://localhost:4200 時,/heroes 路由立即隨之啟動,並且路由器在載入了 HeroesModule 之後立即開始載入 CrisisCenterModule

When you visit http://localhost:4200, the /heroes route loads immediately upon launch and the router starts loading the CrisisCenterModule right after the HeroesModule loads.

目前,AdminModule 並沒有預載入,因為 CanLoad 阻塞了它。

Currently, the AdminModule does not preload because CanLoad is blocking it.

CanLoad 會阻塞預載入

CanLoad blocks preload

PreloadAllModules 策略不會載入被CanLoad守衛所保護的特性區。

The PreloadAllModules strategy does not load feature areas protected by a CanLoad guard.

幾步之前,你剛剛給 AdminModule 中的路由添加了 CanLoad 守衛,以阻塞載入那個模組,直到使用者認證結束。 CanLoad 守衛的優先順序高於預載入策略。

You added a CanLoad guard to the route in the AdminModule a few steps back to block loading of that module until the user is authorized. That CanLoad guard takes precedence over the preload strategy.

如果你要載入一個模組並且保護它防止未授權訪問,請移除 canLoad 守衛,只單獨依賴CanActivate守衛。

If you want to preload a module as well as guard against unauthorized access, remove the canLoad() guard method and rely on the canActivate() guard alone.

自訂預載入策略

Custom Preloading Strategy

在很多場景下,預載入的每個延遲載入模組都能正常工作。但是,考慮到低頻寬和使用者指標等因素,可以為特定的特性模組使用自訂預載入策略。

Preloading every lazy loaded module works well in many situations. However, in consideration of things such as low bandwidth and user metrics, you can use a custom preloading strategy for specific feature modules.

本節將指導你新增一個自訂策略,它只預載入 data.preload 標誌為 true 路由。回想一下,你可以在路由的 data 屬性中新增任何東西。

This section guides you through adding a custom strategy that only preloads routes whose data.preload flag is set to true. Recall that you can add anything to the data property of a route.

AppRoutingModulecrisis-center 路由中設定 data.preload 標誌。

Set the data.preload flag in the crisis-center route in the AppRoutingModule.

{ path: 'crisis-center', loadChildren: () => import('./crisis-center/crisis-center.module').then(m => m.CrisisCenterModule), data: { preload: true } },
src/app/app-routing.module.ts (route data preload)
      
      {
  path: 'crisis-center',
  loadChildren: () => import('./crisis-center/crisis-center.module').then(m => m.CrisisCenterModule),
  data: { preload: true }
},
    

產生一個新的 SelectivePreloadingStrategy 服務。

Generate a new SelectivePreloadingStrategy service.

ng generate service selective-preloading-strategy
      
      ng generate service selective-preloading-strategy
    

使用下列內容替換 selective-preloading-strategy.service.ts

Replace the contents of selective-preloading-strategy.service.ts with the following:

import { Injectable } from '@angular/core'; import { PreloadingStrategy, Route } from '@angular/router'; import { Observable, of } from 'rxjs'; @Injectable({ providedIn: 'root', }) export class SelectivePreloadingStrategyService implements PreloadingStrategy { preloadedModules: string[] = []; preload(route: Route, load: () => Observable<any>): Observable<any> { if (route.data && route.data.preload) { // add the route path to the preloaded module array this.preloadedModules.push(route.path); // log the route path to the console console.log('Preloaded: ' + route.path); return load(); } else { return of(null); } } }
src/app/selective-preloading-strategy.service.ts
      
      import { Injectable } from '@angular/core';
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';

@Injectable({
  providedIn: 'root',
})
export class SelectivePreloadingStrategyService implements PreloadingStrategy {
  preloadedModules: string[] = [];

  preload(route: Route, load: () => Observable<any>): Observable<any> {
    if (route.data && route.data.preload) {
      // add the route path to the preloaded module array
      this.preloadedModules.push(route.path);

      // log the route path to the console
      console.log('Preloaded: ' + route.path);

      return load();
    } else {
      return of(null);
    }
  }
}
    

SelectivePreloadingStrategyService 實現了 PreloadingStrategy,它有一個方法 preload()

SelectivePreloadingStrategyService implements the PreloadingStrategy, which has one method, preload().

路由器會用兩個引數來呼叫 preload() 方法:

The router calls the preload() method with two arguments:

  1. 要載入的路由。

    The route to consider.

  2. 一個載入器(loader)函式,它能非同步載入帶路由的模組。

    A loader function that can load the routed module asynchronously.

preload 的實現要返回一個 Observable。 如果該路由應該預載入,它就會返回呼叫載入器函式所返回的 Observable。 如果該路由應該預載入,它就返回一個 null 值的 Observable 物件。

An implementation of preload must return an Observable. If the route does preload, it returns the observable returned by calling the loader function. If the route does not preload, it returns an Observable of null.

在這個例子中,如果路由的 data.preload 標誌是真值,則 preload() 方法會載入該路由。

In this sample, the preload() method loads the route if the route's data.preload flag is truthy.

它的副作用是 SelectivePreloadingStrategyService 會把所選路由的 path 記錄在它的公共陣列 preloadedModules 中。

As a side-effect, SelectivePreloadingStrategyService logs the path of a selected route in its public preloadedModules array.

很快,你就會擴充套件 AdminDashboardComponent 來注入該服務,並且顯示它的 preloadedModules 陣列。

Shortly, you'll extend the AdminDashboardComponent to inject this service and display its preloadedModules array.

但是首先,要對 AppRoutingModule 做少量修改。

But first, make a few changes to the AppRoutingModule.

  1. SelectivePreloadingStrategyService 匯入到 AppRoutingModule 中。

    Import SelectivePreloadingStrategyService into AppRoutingModule.

  2. PreloadAllModules 策略替換成對 forRoot() 的呼叫,並且傳入這個 SelectivePreloadingStrategyService

    Replace the PreloadAllModules strategy in the call to forRoot() with this SelectivePreloadingStrategyService.

  3. SelectivePreloadingStrategyService 策略新增到 AppRoutingModuleproviders 陣列中,以便它可以注入到應用中的任何地方。

    Add the SelectivePreloadingStrategyService strategy to the AppRoutingModule providers array so you can inject it elsewhere in the app.

現在,編輯 AdminDashboardComponent 以顯示這些預載入路由的日誌。

Now edit the AdminDashboardComponent to display the log of preloaded routes.

  1. 匯入 SelectivePreloadingStrategyService(它是一個服務)。

    Import the SelectivePreloadingStrategyService.

  2. 把它注入到儀表盤的建構函式中。

    Inject it into the dashboard's constructor.

  3. 修改範本來顯示這個策略服務的 preloadedModules 陣列。

    Update the template to display the strategy service's preloadedModules array.

現在檔案如下:

Now the file is as follows:

import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { SelectivePreloadingStrategyService } from '../../selective-preloading-strategy.service'; @Component({ selector: 'app-admin-dashboard', templateUrl: './admin-dashboard.component.html', styleUrls: ['./admin-dashboard.component.css'] }) export class AdminDashboardComponent implements OnInit { sessionId: Observable<string>; token: Observable<string>; modules: string[]; constructor( private route: ActivatedRoute, preloadStrategy: SelectivePreloadingStrategyService ) { this.modules = preloadStrategy.preloadedModules; } ngOnInit() { // Capture the session ID if available this.sessionId = this.route .queryParamMap .pipe(map(params => params.get('session_id') || 'None')); // Capture the fragment if available this.token = this.route .fragment .pipe(map(fragment => fragment || 'None')); } }
src/app/admin/admin-dashboard/admin-dashboard.component.ts (preloaded modules)
      
      import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

import { SelectivePreloadingStrategyService } from '../../selective-preloading-strategy.service';

@Component({
  selector: 'app-admin-dashboard',
  templateUrl: './admin-dashboard.component.html',
  styleUrls: ['./admin-dashboard.component.css']
})
export class AdminDashboardComponent implements OnInit {
  sessionId: Observable<string>;
  token: Observable<string>;
  modules: string[];

  constructor(
    private route: ActivatedRoute,
    preloadStrategy: SelectivePreloadingStrategyService
  ) {
    this.modules = preloadStrategy.preloadedModules;
  }

  ngOnInit() {
    // Capture the session ID if available
    this.sessionId = this.route
      .queryParamMap
      .pipe(map(params => params.get('session_id') || 'None'));

    // Capture the fragment if available
    this.token = this.route
      .fragment
      .pipe(map(fragment => fragment || 'None'));
  }
}
    

一旦應用載入完了初始路由,CrisisCenterModule 也被預載入了。 透過 Admin 特性區中的記錄就可以驗證它,“Preloaded Modules”中列出了 crisis-center。 它也被記錄到了瀏覽器的控制檯。

Once the application loads the initial route, the CrisisCenterModule is preloaded. Verify this by logging in to the Admin feature area and noting that the crisis-center is listed in the Preloaded Modules. It also logs to the browser's console.

使用重新導向遷移 URL

Migrating URLs with redirects

你已經設定好了路由,並且用命令式和宣告式的方式導航到了很多不同的路由。但是,任何應用的需求都會隨著時間而改變。 你把連結 /heroeshero/:id 指向了 HeroListComponentHeroDetailComponent 元件。 如果有這樣一個需求,要把連結 heroes 變成 superheroes,你可能仍然希望以前的 URL 能正常導航。 但你也不想在應用中找到並修改每一個連結,這時候,重新導向就可以省去這些瑣碎的重構工作。

You've setup the routes for navigating around your application and used navigation imperatively and declaratively. But like any application, requirements change over time. You've setup links and navigation to /heroes and /hero/:id from the HeroListComponent and HeroDetailComponent components. If there were a requirement that links to heroes become superheroes, you would still want the previous URLs to navigate correctly. You also don't want to update every link in your application, so redirects makes refactoring routes trivial.

/heroes 改為 /superheroes

Changing /heroes to /superheroes

本節將指導你將 Hero 路由遷移到新的 URL。在導航之前,Router 會檢查路由配置中的重新導向語句,以便將來按需觸發重新導向。要支援這種修改,你就要在 heroes-routing.module 檔案中把老的路由重新導向到新的路由。

This section guides you through migrating the Hero routes to new URLs. The Router checks for redirects in your configuration before navigating, so each redirect is triggered when needed. To support this change, add redirects from the old routes to the new routes in the heroes-routing.module.

import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { HeroListComponent } from './hero-list/hero-list.component'; import { HeroDetailComponent } from './hero-detail/hero-detail.component'; const heroesRoutes: Routes = [ { path: 'heroes', redirectTo: '/superheroes' }, { path: 'hero/:id', redirectTo: '/superhero/:id' }, { path: 'superheroes', component: HeroListComponent, data: { animation: 'heroes' } }, { path: 'superhero/:id', component: HeroDetailComponent, data: { animation: 'hero' } } ]; @NgModule({ imports: [ RouterModule.forChild(heroesRoutes) ], exports: [ RouterModule ] }) export class HeroesRoutingModule { }
src/app/heroes/heroes-routing.module.ts (heroes redirects)
      
      import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

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

const heroesRoutes: Routes = [
  { path: 'heroes', redirectTo: '/superheroes' },
  { path: 'hero/:id', redirectTo: '/superhero/:id' },
  { path: 'superheroes',  component: HeroListComponent, data: { animation: 'heroes' } },
  { path: 'superhero/:id', component: HeroDetailComponent, data: { animation: 'hero' } }
];

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

注意,這裡有兩種型別的重新導向。第一種是不帶引數的從 /heroes 重新導向到 /superheroes。這是一種非常直觀的重新導向。第二種是從 /hero/:id 重新導向到 /superhero/:id,它還要包含一個 :id 路由引數。 路由器重新導向時使用強大的模式匹配功能,這樣,路由器就會檢查 URL,並且把 path 中帶的路由引數替換成相應的目標形式。以前,你導航到形如 /hero/15 的 URL 時,帶了一個路由引數 id,它的值是 15

Notice two different types of redirects. The first change is from /heroes to /superheroes without any parameters. The second change is from /hero/:id to /superhero/:id, which includes the :id route parameter. Router redirects also use powerful pattern-matching, so the Router inspects the URL and replaces route parameters in the path with their appropriate destination. Previously, you navigated to a URL such as /hero/15 with a route parameter id of 15.

在重新導向的時候,路由器還支援查詢引數片段(fragment)

The Router also supports query parameters and the fragment when using redirects.

  • 當使用絕對地址重新導向時,路由器將會使用路由配置的 redirectTo 屬性中規定的查詢引數和片段。

    When using absolute redirects, the Router will use the query parameters and the fragment from the redirectTo in the route config.

  • 當使用相對地址重新導向時,路由器將會使用源地址(跳轉前的地址)中的查詢引數和片段。

    When using relative redirects, the Router use the query params and the fragment from the source URL.

目前,空路徑被重新導向到了 /heroes,它又被重新導向到了 /superheroes。這樣不行,因為 Router 在每一層的路由配置中只會處理一次重新導向。這樣可以防止出現無限迴圈的重新導向。

Currently, the empty path route redirects to /heroes, which redirects to /superheroes. This won't work because the Router handles redirects once at each level of routing configuration. This prevents chaining of redirects, which can lead to endless redirect loops.

所以,你要在 app-routing.module.ts 中修改空路徑路由,讓它重新導向到 /superheroes

Instead, update the empty path route in app-routing.module.ts to redirect to /superheroes.

import { NgModule } from '@angular/core'; import { RouterModule, Routes } from '@angular/router'; import { ComposeMessageComponent } from './compose-message/compose-message.component'; import { PageNotFoundComponent } from './page-not-found/page-not-found.component'; import { AuthGuard } from './auth/auth.guard'; import { SelectivePreloadingStrategyService } from './selective-preloading-strategy.service'; const appRoutes: Routes = [ { path: 'compose', component: ComposeMessageComponent, outlet: 'popup' }, { path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule), canLoad: [AuthGuard] }, { path: 'crisis-center', loadChildren: () => import('./crisis-center/crisis-center.module').then(m => m.CrisisCenterModule), data: { preload: true } }, { path: '', redirectTo: '/superheroes', pathMatch: 'full' }, { path: '**', component: PageNotFoundComponent } ]; @NgModule({ imports: [ RouterModule.forRoot( appRoutes, { enableTracing: false, // <-- debugging purposes only preloadingStrategy: SelectivePreloadingStrategyService, } ) ], exports: [ RouterModule ] }) export class AppRoutingModule { }
src/app/app-routing.module.ts (superheroes redirect)
      
      import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

import { ComposeMessageComponent } from './compose-message/compose-message.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';

import { AuthGuard } from './auth/auth.guard';
import { SelectivePreloadingStrategyService } from './selective-preloading-strategy.service';

const appRoutes: Routes = [
  {
    path: 'compose',
    component: ComposeMessageComponent,
    outlet: 'popup'
  },
  {
    path: 'admin',
    loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
    canLoad: [AuthGuard]
  },
  {
    path: 'crisis-center',
    loadChildren: () => import('./crisis-center/crisis-center.module').then(m => m.CrisisCenterModule),
    data: { preload: true }
  },
  { path: '',   redirectTo: '/superheroes', pathMatch: 'full' },
  { path: '**', component: PageNotFoundComponent }
];

@NgModule({
  imports: [
    RouterModule.forRoot(
      appRoutes,
      {
        enableTracing: false, // <-- debugging purposes only
        preloadingStrategy: SelectivePreloadingStrategyService,
      }
    )
  ],
  exports: [
    RouterModule
  ]
})
export class AppRoutingModule { }
    

由於 routerLink 與路由配置無關,所以你要修改相關的路由連結,以便在新的路由啟用時,它們也能保持啟用狀態。還要修改 app.component.ts 範本中的 /heroes 這個 routerLink

A routerLink isn't tied to route configuration, so update the associated router links to remain active when the new route is active. Update the app.component.ts template for the /heroes routerLink.

<h1 class="title">Angular Router</h1> <nav> <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a> <a routerLink="/superheroes" routerLinkActive="active">Heroes</a> <a routerLink="/admin" routerLinkActive="active">Admin</a> <a routerLink="/login" routerLinkActive="active">Login</a> <a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a> </nav> <div [@routeAnimation]="getAnimationData(routerOutlet)"> <router-outlet #routerOutlet="outlet"></router-outlet> </div> <router-outlet name="popup"></router-outlet>
src/app/app.component.html (superheroes active routerLink)
      
      <h1 class="title">Angular Router</h1>
<nav>
  <a routerLink="/crisis-center" routerLinkActive="active">Crisis Center</a>
  <a routerLink="/superheroes" routerLinkActive="active">Heroes</a>
  <a routerLink="/admin" routerLinkActive="active">Admin</a>
  <a routerLink="/login" routerLinkActive="active">Login</a>
  <a [routerLink]="[{ outlets: { popup: ['compose'] } }]">Contact</a>
</nav>
<div [@routeAnimation]="getAnimationData(routerOutlet)">
  <router-outlet #routerOutlet="outlet"></router-outlet>
</div>
<router-outlet name="popup"></router-outlet>
    

修改 hero-detail.component.ts 中的 goToHeroes() 方法,使用可選的路由引數導航回 /superheroes

Update the goToHeroes() method in the hero-detail.component.ts to navigate back to /superheroes with the optional route parameters.

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(['/superheroes', { id: heroId, foo: 'foo' }]); }
src/app/heroes/hero-detail/hero-detail.component.ts (goToHeroes)
      
      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(['/superheroes', { id: heroId, foo: 'foo' }]);
}
    

當這些重新導向設定好之後,所有以前的路由都指向了它們的新目標,並且每個 URL 也仍然能正常工作。

With the redirects setup, all previous routes now point to their new destinations and both URLs still function as intended.

審查路由器配置

Inspect the router's configuration

要確定你的路由是否真的按照正確的順序執行的,你可以審查路由器的配置。

To determine if your routes are actually evaluated in the proper order, you can inspect the router's configuration.

可以透過注入路由器並在控制檯中記錄其 config 屬性來實現。 例如,把 AppModule 修改為這樣,並在瀏覽器的控制檯視窗中檢視最終的路由配置。

Do this by injecting the router and logging to the console its config property. For example, update the AppModule as follows and look in the browser console window to see the finished route configuration.

export class AppModule { // Diagnostic only: inspect router configuration constructor(router: Router) { // Use a custom replacer to display function names in the route configs const replacer = (key, value) => (typeof value === 'function') ? value.name : value; console.log('Routes: ', JSON.stringify(router.config, replacer, 2)); } }
src/app/app.module.ts (inspect the router config)
      
      export class AppModule {
  // Diagnostic only: inspect router configuration
  constructor(router: Router) {
    // Use a custom replacer to display function names in the route configs
    const replacer = (key, value) => (typeof value === 'function') ? value.name : value;

    console.log('Routes: ', JSON.stringify(router.config, replacer, 2));
  }
}
    

最終的應用

Final app

對這個已完成的路由器應用,參見現場演練 / 下載範例的最終程式碼。

For the completed router app, see the現場演練 / 下載範例for the final source code.