# 路由器教程:英雄之旅

本教程提供了关于 Angular 路由器的概要性概述。在本教程中,你将基于基本的路由器配置来探索诸如子路由、路由参数、惰性加载 NgModule、路由守卫和预加载数据等功能,以改善用户体验。

有关该应用最终版本的有效示例,请参阅现场演练/ 下载范例 。

# 目标

本指南描述了一个多页路由示例应用程序的开发过程。在此过程中,它重点介绍了路由器的关键特性,比如:

  • 将应用程序功能组织到模块中

  • 导航到组件(从 Heroes链接导航到“英雄列表”)

  • 包括一个路由参数(在路由到“英雄详细信息”时传入英雄的 id

  • 子路由(危机中心特性区有自己的路由)

  • canActivate守卫(检查路由访问)

  • canActivateChild守卫(检查子路由访问)

  • canDeactivate守卫(在放弃未保存的更改之前请求许可)

  • resolve守卫(预先获取路由数据)。

  • 惰性加载 NgModule

  • canLoad守卫(在加载功能模块的文件之前检查)

本指南按照里程碑的顺序进行,就像你逐步构建应用程序一样,但这里假定你已经熟悉 Angular 的 基本概念。关于 Angular 的一般性介绍,请参见《入门指南》。关于更深入的概述,请参见《英雄之旅》教程。

# 前提条件

要完成本教程,你应该对以下概念有基本的了解:

  • JavaScript

  • HTML

  • CSS

  • Angular CLI

你可能会发现《英雄之旅》教程很有用,但这不是必需的。

# 范例应用实战

本教程的示例应用会帮助“英雄雇佣管理局”找到需要各位英雄去解决的危机。

本应用有三个主要的特性区:

  • 危机中心,用于维护要指派给英雄的危机列表。

  • 英雄特性区,用于维护管理局雇佣的英雄列表。

  • 管理特性区会管理危机和英雄的列表。

点击到在线例子的链接到在线例子的链接/ 下载范例 试用一下。

该应用会渲染出一排导航按钮和和一个英雄列表视图。

Example application with a row of navigation buttons and a list of heroes  选择其中之一,该应用就会把你带到此英雄的编辑页面。

Detail view of hero with additional information, input, and back button  修改完名字,再点击“后退”按钮,应用又回到了英雄列表页,其中显示的英雄名已经变了。注意,对名字的修改会立即生效。

另外你也可以点击浏览器本身的后退按钮(而不是应用中的 “Back” 按钮),这也同样会回到英雄列表页。在 Angular 应用中导航也会和标准的 Web 导航一样更新浏览器中的历史。

现在,点击危机中心链接,前往危机列表页。

Crisis Center list of crises  选择其中之一,该应用就会把你带到此危机的编辑页面。危机详情是当前页的子组件,就在列表的紧下方。

修改危机的名称。注意,危机列表中的相应名称并没有修改。

Crisis Center detail of a crisis with data, an input, and save and cancel buttons.  这和英雄详情页略有不同。英雄详情会立即保存你所做的更改。而危机详情页中,你的更改都是临时的 —— 除非按“保存”按钮保存它们,或者按“取消”按钮放弃它们。这两个按钮都会导航回危机中心,显示危机列表。

单击浏览器后退按钮或 “Heroes” 链接,可以激活一个对话框。

Alert that asks user to confirm discarding changes  你可以回答“确定”以放弃这些更改,或者回答“取消”来继续编辑。

这种行为的幕后是路由器的 canDeactivate守卫。该守卫让你有机会进行清理工作或在离开当前视图之前请求用户的许可。

AdminLogin按钮用于演示路由器的其它能力,本章稍后的部分会讲解它们。

# 里程碑 1:起步

开始本应用的一个简版,它在两个空路由之间导航。

Animated image of application with a Crisis Center button and a Heroes button. The pointer clicks each button to show a view for each.

# 创建一个范例应用

  • 创建一个新的 Angular 项目 angular-router-tour-of-heroes。
ng new angular-router-tour-of-heroes

当系统提示 Would you like to add Angular routing?时,选择 N

当系统提示 Which stylesheet format would you like to use?时,选择 CSS

片刻之后,一个新项目 angular-router-tour-of-heroes已准备就绪。

  • 从你的终端,导航到 angular-router-tour-of-heroes目录。

  • 运行 ng serve来验证新应用是否正常运行。

ng serve
  • 打开浏览器访问 http://localhost:4200

你会发现本应用正运行在浏览器中。

# 定义路由

路由器必须用“路由定义”的列表进行配置。

每个定义都被翻译成了一个Route对象。该对象有一个 path字段,表示该路由中的 URL 路径部分,和一个 component字段,表示与该路由相关联的组件。

当浏览器的 URL 变化时或在代码中告诉路由器导航到一个路径时,路由器就会翻出它用来保存这些路由定义的注册表。

第一个路由执行以下操作:

  • 当浏览器地址栏的 URL 变化时,如果它匹配上了路径部分 /crisis-center,路由器就会激活一个 CrisisListComponent的实例,并显示它的视图。

  • 当应用程序请求导航到路径 /crisis-center时,路由器激活一个 CrisisListComponent的实例,显示它的视图,并将该路径更新到浏览器地址栏和历史。

第一个配置定义了由两个路由构成的数组,它们用最短路径指向了 CrisisListComponentHeroListComponent

生成 CrisisListHeroList组件,以便路由器能够渲染它们。

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

把每个组件的内容都替换成下列范例 HTML。

src/app/crisis-list/crisis-list.component.html

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

src/app/hero-list/hero-list.component.html

<h2>HEROES</h2>
<p>Get your heroes here</p>

# 注册 RouterRoutes

为了使用 Router,你必须注册来自 @angular/router包中的 RouterModule。定义一个路由数组 appRoutes,并把它传给 RouterModule.forRoot()方法。RouterModule.forRoot()方法会返回一个模块,其中包含配置好的 Router服务提供者,以及路由库所需的其它提供者。一旦启动了应用,Router就会根据当前的浏览器 URL 进行首次导航。

注意RouterModule.forRoot()方法是用于注册全应用级提供者的编码模式。要详细了解全应用级提供者,参见单例服务一章。

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中就足够了。但是,随着应用的成长,你将需要将路由配置重构到单独的文件中,并创建路由模块,路由模块是一种特殊的、专做路由的服务模块

RouterModule.forRoot()注册到 AppModuleimports数组中,能让该 Router服务在应用的任何地方都能使用。

# 添加路由出口

根组件 AppComponent是本应用的壳。它在顶部有一个标题、一个带两个链接的导航条,在底部有一个路由器出口,路由器会在它所指定的位置上渲染各个组件。

A nav, made of two navigation buttons, with the first button active and its associated view displayed  路由出口扮演一个占位符的角色,表示路由组件将会渲染到哪里。

该组件所对应的模板是这样的:

src/app/app.component.html

<h1>Angular Router</h1>
<nav>
  <a routerLink="/crisis-center" routerLinkActive="active" ariaCurrentWhenActive="page">Crisis Center</a>
  <a routerLink="/heroes" routerLinkActive="active" ariaCurrentWhenActive="page">Heroes</a>
</nav>
<router-outlet></router-outlet>

# 定义通配符路由

你以前在应用中创建过两个路由,一个是 /crisis-center,另一个是 /heroes。所有其它 URL 都会导致路由器抛出错误,并让应用崩溃。

可以添加一个通配符路由来拦截所有无效的 URL,并优雅的处理它们。 通配符路由的 path是两个星号(**),它会匹配任何 URL。 而当路由器匹配不上以前定义的那些路由时,它就会选择这个通配符路由。 通配符路由可以导航到自定义的“404 Not Found”组件,也可以重定向到一个现有路由。

路由器会使用先到先得的策略来选择路由。由于通配符路由是最不具体的那个,因此务必确保它是路由配置中的最后一个路由。

要测试本特性,请往 HeroListComponent的模板中添加一个带 RouterLink的按钮,并且把它的链接设置为一个不存在的路由 "/sidekicks"

src/app/hero-list/hero-list.component.html (excerpt)

<h2>HEROES</h2>
<p>Get your heroes here</p>

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

当用户点击该按钮时,应用就会失败,因为你尚未定义过 "/sidekicks"路由。

不要添加 "/sidekicks"路由,而是定义一个“通配符”路由,让它导航到 PageNotFoundComponent组件。

src/app/app.module.ts (wildcard)

{ path: '**', component: PageNotFoundComponent }

创建 PageNotFoundComponent,以便在用户访问无效网址时显示它。

ng generate component page-not-found

src/app/page-not-found.component.html (404 component)

<h2>Page not found</h2>

现在,当用户访问 /sidekicks或任何无效的 URL 时,浏览器就会显示“Page not found”。浏览器的地址栏仍指向无效的 URL。

# 设置跳转

应用启动时,浏览器地址栏中的初始 URL 默认是这样的:

localhost:4200

它不能匹配上任何硬编码进来的路由,于是就会走到通配符路由中去,并且显示 PageNotFoundComponent

这个应用需要一个有效的默认路由,在这里应该用英雄列表作为默认页。当用户点击"Heroes"链接或把 localhost:4200/heroes粘贴到地址栏时,它应该导航到列表页。

添加一个 redirect路由,把最初的相对 URL('')转换成所需的默认路径(/heroes)。

在通配符路由上方添加一个默认路由。在下方的代码片段中,它出现在通配符路由的紧上方,展示了这个里程碑的完整 appRoutes

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,好像你直接在那里导航一样。

重定向路由需要一个 pathMatch属性,来告诉路由器如何用 URL 去匹配路由的路径。在本应用中,路由器应该只有在完整的 URL等于 ''时才选择 HeroListComponent组件,因此要把 pathMatch设置为 'full'

聚焦 pathMatchSpotlight on pathMatch从技术角度看,pathMatch = 'full'会导致 URL 中剩下的、未匹配的部分必须等于 ''。在这个例子中,跳转路由在一个顶层路由中,因此剩下的URL 和完整的URL 是一样的。

pathMatch的另一个可能的值是 'prefix',它会告诉路由器:当剩下的URL 以这个跳转路由中的 prefix值开头时,就会匹配上这个跳转路由。但这不适用于此示例应用,因为如果 pathMatch值是 'prefix',那么每个 URL 都会匹配 ''

尝试把它设置为 'prefix',并点击 Go to sidekicks按钮。这是因为它是一个无效 URL,本应显示“Page not found”页。但是,你仍然在“英雄列表”页中。在地址栏中输入一个无效的 URL,你又被路由到了 /heroes。每一个URL,无论有效与否,都会匹配上这个路由定义。

默认路由应该只有在整个 URL 等于 ''时才重定向到 HeroListComponent,别忘了把重定向路由设置为 pathMatch = 'full'

要了解更多,参见 Victor Savkin 的帖子关于重定向。

# 里程碑 1 小结

当用户单击某个链接时,该示例应用可以在两个视图之间切换。

里程碑 1 涵盖了以下几点的做法:

  • 加载路由库

  • 往壳组件的模板中添加一个导航条,导航条中有一些 A 标签、routerLink指令和 routerLinkActive指令

  • 往壳组件的模板中添加一个 router-outlet指令,视图将会被显示在那里

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

  • 设置路由器,使其合成 HTML5 模式的浏览器 URL

  • 使用通配符路由来处理无效路由

  • 当应用在空路径下启动时,导航到默认路由

这个初学者应用的结构是这样的:


angular-router-tour-of-heroes

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

下面是本里程碑中的文件列表。

app.component.html

<h1>Angular Router</h1>
<nav>
  <a routerLink="/crisis-center" routerLinkActive="active" ariaCurrentWhenActive="page">Crisis Center</a>
  <a routerLink="/heroes" routerLinkActive="active" ariaCurrentWhenActive="page">Heroes</a>
</nav>
<router-outlet></router-outlet>

# 里程碑 2:路由模块

这个里程碑会向你展示如何配置一个名叫路由模块的专用模块,它会保存你应用的路由配置。

路由模块有以下几个特点:

  • 把路由这个关注点从其它应用类关注点中分离出去。

  • 测试特性模块时,可以替换或移除路由模块。

  • 为路由服务提供者(如守卫和解析器等)提供一个众所周知的位置。

  • 不要声明组件。

# 把路由集成到应用中

路由应用范例中默认不包含路由。要想在使用 Angular CLI 创建项目时支持路由,请为项目或应用的每个 NgModule 设置 --routing选项。当你用 CLI 命令 ng new 创建新项目或用 ng generate app 命令创建新应用,请指定 --routing选项。这会告诉 CLI 包含上 @angular/router包,并创建一个名叫 app-routing.module.ts的文件。然后你就可以在添加到项目或应用中的任何 NgModule 中使用路由功能了。

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

ng generate module my-module --routing

这将创建一个名叫 my-module-routing.module.ts的独立文件,来保存这个 NgModule 的路由信息。该文件包含一个空的 Routes对象,你可以使用一些指向各个组件和 NgModule 的路由来填充该对象。

# 将路由配置重构为路由模块

/app目录下创建一个 AppRouting模块,以包含路由配置。

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

导入 CrisisListComponentHeroListComponentPageNotFoundComponent组件,就像 app.module.ts中那样。然后把 Router的导入语句和路由配置以及 RouterModule.forRoot()移入这个路由模块中。

把 Angular 的 RouterModule添加到该模块的 exports数组中,以便再次导出它。通过再次导出 RouterModule,那些声明在 AppModule中的组件就可以访问路由指令了,比如 RouterLinkRouterOutlet

做完这些之后,该文件变成了这样。

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

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

稍后,本指南将向你展示如何创建多个路由模块,并以正确的顺序导入这些路由模块。

应用继续照常运行,你可以把路由模块作为将来每个模块维护路由配置的中心位置。

# 路由模块的优点

路由模块(通常称为 AppRoutingModule)代替了根模板或特性模块中的路由模块。

这种路由模块在你的应用不断增长,以及配置中包括了专门的守卫和解析器函数时会非常有用。

在配置很简单时,一些开发者会跳过路由模块,并将路由配置直接混合在关联模块中(比如 AppModule)。

大多数应用都应该采用路由模块,以保持一致性。它在配置复杂时,能确保代码干净。它让测试特性模块更加容易。它的存在让人一眼就能看出这个模块是带路由的。开发者可以很自然的从路由模块中查找和扩展路由配置。

# 里程碑 3: 英雄特征区

本里程碑涵盖了以下内容:

  • 用模块把应用和路由组织为一些特性区。

  • 命令式的从一个组件导航到另一个

  • 通过路由传递必要信息和可选信息

这个示例应用在“英雄指南”教程的“服务”部分重新创建了英雄特性区,并复用了Tour of Heroes: Services example code/ 下载范例 中的大部分代码。

典型的应用具有多个特性区,每个特性区都专注于特定的业务用途并拥有自己的文件夹。

该部分将向你展示如何将应用重构为不同的特性模块、将它们导入到主模块中,并在它们之间导航。

# 添加英雄管理功能

遵循下列步骤:

  • 为了管理这些英雄,在 heroes目录下创建一个带路由的 HeroesModule,并把它注册到根模块 AppModule中。
ng generate module heroes/heroes --module app --flat --routing
  • app下占位用的 hero-list目录移到 heroes目录中。

  • 从 教程的 "服务" 部分教程的 "服务" 部分/ 下载范例 把 heroes/heroes.component.html的内容复制到 hero-list.component.html模板中。

  • &lt;h2&gt;加文字,改成 &lt;h2>HEROES</h2&gt;

  • 删除模板底部的 &lt;app-hero-detail&gt;组件。

  • 把现场演练中 heroes/heroes.component.css文件的内容复制到 hero-list.component.css文件中。

  • 把现场演练中 heroes/heroes.component.ts文件的内容复制到 hero-list.component.ts文件中。

  • 把组件类名改为 HeroListComponent

  • selector改为 app-hero-list

对于路由组件来说,这些选择器不是必须的,因为这些组件是在渲染页面时动态插入的,不过选择器对于在 HTML 元素树中标记和选中它们是很有用的。

  • hero-detail目录中的 hero.tshero.service.tsmock-heroes.ts文件复制到 heroes子目录下。

  • message.service.ts文件复制到 src/app目录下。

  • hero.service.ts文件中修改导入 message.service的相对路径。

接下来,更新 HeroesModule的元数据。

  • 导入 HeroDetailComponentHeroListComponent,并添加到 HeroesModule模块的 declarations数组中。

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

英雄管理部分的文件结构如下:


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
Last Updated: 5/13/2023, 10:57:08 AM