路由

我们收到了《英雄指南》的一些新需求:

完成时,用户就能像这样在应用中导航:

查看导航

我们将把 Angular 路由器加入应用中,以满足这些需求。 (译注:硬件领域中的路由器是用来帮你找到另一台网络设备的,而这里的路由器用于帮你找到一个组件)

更多信息,见路由和导航

完成本章之后,应用会变成这样:在线例子 / 可下载的例子

要看到这个在线例子中浏览器地址栏的URL变化情况,请点击右上角的图标,在Plunker编辑器中打开它,接下来在弹出的预览窗口中点击右上角的蓝色'X'按钮就可以了。

pop out the window
pop out the window

延续上一步教程

在继续《英雄指南》之前,我们先验证一下目录结构:

angular-tour-of-heroes
src
app
app.component.ts
app.module.ts
hero.service.ts
hero.ts
hero-detail.component.ts
mock-heroes.ts
main.ts
index.html
styles.css
systemjs.config.js
tsconfig.json
node_modules ...
package.json

让应用代码保持转译和运行

打开终端/控制台窗口,运行如下命令:

npm start

这个命令会在“监听”模式下运行TypeScript编译器,当代码变化时,它会自动重新编译。 同时,该命令还会在浏览器中启动该应用,并且当代码变化时刷新浏览器。

在后续构建《英雄指南》过程中,应用能持续运行,而不用中断服务来编译或刷新浏览器。

行动计划

下面是我们的计划:

路由是导航的另一个名字。路由器就是从一个视图导航到另一个视图的机制。

拆分 AppComponent

现在的应用会加载AppComponent组件,并且立刻显示出英雄列表。

我们修改后的应用将提供一个壳,它会选择仪表盘英雄列表视图之一,然后默认显示它。

AppComponent组件应该只处理导航。 我们来把英雄列表的显示职责,从AppComponent移到HeroesComponent组件中。

HeroesComponent

AppComponent的职责已经被移交给HeroesComponent了。 与其把AppComponent中所有的东西都搬过去,不如索性把它改名为HeroesComponent,然后单独创建一个新的AppComponent壳。

步骤如下:

src/app/heroes.component.ts (showing renamings only)

  1. @Component({
  2. selector: 'my-heroes',
  3. })
  4. export class HeroesComponent implements OnInit {
  5. }

创建 AppComponent

新的AppComponent将成为应用的“壳”。 它将在顶部放一些导航链接,并且把我们要导航到的页面放在下面的显示区中。

执行下列步骤:

我们的第一个草稿版就像这样:

  1. import { Component } from '@angular/core';
  2. @Component({
  3. selector: 'my-app',
  4. template: `
  5. <h1>{{title}}</h1>
  6. <my-heroes></my-heroes>
  7. `
  8. })
  9. export class AppComponent {
  10. title = 'Tour of Heroes';
  11. }
HeroesComponentproviders中移除HeroService

回到HeroesComponent,并从providers数组中移除HeroService。 把它从HeroesComponent提升到根NgModule中。 我们不希望在应用的两个不同层次上存在它的两个副本

应用仍然在运行,并显示着英雄列表。

添加路由

我们希望在用户点击按钮之后才显示英雄列表,而不是自动显示。 换句话说,我们希望用户能“导航”到英雄列表。

我们要使用 Angular 路由器进行导航。

Angular 路由器是一个可选的外部 Angular NgModule,名叫RouterModule。 路由器包含了多种服务(RouterModule)、多种指令(RouterOutlet、RouterLink、RouterLinkActive)、 和一套配置(Routes)。我们将先配置路由。

<base href>

打开index.html,确保它的<head>区顶部有一个<base href="...">元素(或动态生成该元素的脚本)。

src/index.html (base-href)

  1. <head>
  2. <base href="/">
基地址(base href)是必须的

要了解更多信息,请参见路由与导航章的设置基地址(base href)部分。

配置路由

本应用还没有路由。我们来为应用的路由新建一个配置。

路由告诉路由器,当用户点击链接或者把 URL 粘贴到浏览器地址栏时,应该显示哪个视图。

我们的第一个路由是指向英雄列表组件的。

src/app/app.module.ts (heroes route)

  1. import { RouterModule } from '@angular/router';
  2. RouterModule.forRoot([
  3. {
  4. path: 'heroes',
  5. component: HeroesComponent
  6. }
  7. ])

这个Routes是一个路由定义的数组。 此时,我们只有一个路由定义,但别急,后面还会添加更多。

路由定义包括以下部分:

关于Routes定义的更多信息,见路由与导航一章。

让路由器可用

导入RouterModule并添加到AppModuleimports数组中。

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

  1. import { NgModule } from '@angular/core';
  2. import { BrowserModule } from '@angular/platform-browser';
  3. import { FormsModule } from '@angular/forms';
  4. import { RouterModule } from '@angular/router';
  5. import { AppComponent } from './app.component';
  6. import { HeroDetailComponent } from './hero-detail.component';
  7. import { HeroesComponent } from './heroes.component';
  8. import { HeroService } from './hero.service';
  9. @NgModule({
  10. imports: [
  11. BrowserModule,
  12. FormsModule,
  13. RouterModule.forRoot([
  14. {
  15. path: 'heroes',
  16. component: HeroesComponent
  17. }
  18. ])
  19. ],
  20. declarations: [
  21. AppComponent,
  22. HeroDetailComponent,
  23. HeroesComponent
  24. ],
  25. providers: [
  26. HeroService
  27. ],
  28. bootstrap: [ AppComponent ]
  29. })
  30. export class AppModule {
  31. }

这里使用了forRoot()方法,因为我们是在应用根部提供配置好的路由器。 forRoot()方法提供了路由需要的路由服务提供商和指令,并基于当前浏览器 URL 初始化导航。

路由出口(Outlet)

如果我们把路径/heroes粘贴到浏览器的地址栏,路由器会匹配到'Heroes'路由,并显示HeroesComponent组件。 我们必须告诉路由器它位置,所以我们把<router-outlet>标签添加到模板的底部。 RouterOutlet是由RouterModule提供的指令之一。 当我们在应用中导航时,路由器就把激活的组件显示在<router-outlet>里面。

我们当然不会真让用户往地址栏中粘贴路由的 URL, 而应该在模板中的什么地方添加一个锚标签。点击时,就会导航到HeroesComponent组件。

修改过的模板是这样的:

src/app/app.component.ts (template-v2)

  1. template: `
  2. <h1>{{title}}</h1>
  3. <a routerLink="/heroes">Heroes</a>
  4. <router-outlet></router-outlet>
  5. `

注意,锚标签中的[routerLink]绑定。 我们把RouterLink指令(ROUTER_DIRECTIVES中的另一个指令)绑定到一个字符串。 它将告诉路由器,当用户点击这个链接时,应该导航到哪里。

由于这个链接不是动态的,我们只要用一次性绑定的方式绑定到路由的路径 (path) 就行了。 回来看路由配置表,我们清楚的看到,这个路径 —— '/heroes'就是指向HeroesComponent的那个路由的路径。

关于动态路由器链接和*链接参数数组更多信息,见路由与导航中的附录:链接参数数组部分。

刷新浏览器。我们只看到了应用标题和英雄链接。英雄列表到哪里去了?

浏览器的地址栏显示的是/。而到HeroesComponent的路由中的路径是/heroes,不是/。 我们没有任何路由能匹配当前的路径/,所以,自然没啥可显示的。 接下来,我们就修复这个问题。

我们点击“Heroes(英雄列表)”导航链接,浏览器地址栏更新为/heroes,并且看到了英雄列表。我们终于导航过去了!

现在,AppComponent是这样的:

src/app/app.component.ts (v2)

  1. import { Component } from '@angular/core';
  2. @Component({
  3. selector: 'my-app',
  4. template: `
  5. <h1>{{title}}</h1>
  6. <a routerLink="/heroes">Heroes</a>
  7. <router-outlet></router-outlet>
  8. `
  9. })
  10. export class AppComponent {
  11. title = 'Tour of Heroes';
  12. }

AppComponent现在加上了路由器,并能显示路由到的视图了。 因此,为了把它从其它种类的组件中区分出来,我们称这类组件为路由器组件

添加一个仪表盘

当我们有多个视图的时候,路由才有意义。所以我们需要另一个视图。先创建一个DashboardComponent的占位符,让用户可以导航到它或从它导航出来。

src/app/dashboard.component.ts (v1)

  1. import { Component } from '@angular/core';
  2. @Component({
  3. selector: 'my-dashboard',
  4. template: '<h3>My Dashboard</h3>'
  5. })
  6. export class DashboardComponent { }

我们先不实现它,稍后,我们再回来,给这个组件一些实际用途。

配置仪表盘路由

要让app.module.ts能导航到仪表盘,就要先导入仪表盘组件,然后把下列路由定义添加到Routes数组中。

src/app/app.module.ts (Dashboard route)

  1. {
  2. path: 'dashboard',
  3. component: DashboardComponent
  4. },

然后还得把DashboardComponent添加到AppModuledeclarations数组中。

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

  1. declarations: [
  2. AppComponent,
  3. DashboardComponent,
  4. HeroDetailComponent,
  5. HeroesComponent
  6. ],

添加重定向路由

浏览器启动时地址栏中的地址是/。 当应用启动时,它应该显示仪表盘,并且在浏览器的地址栏显示URL:/dashboard

我们可以使用重定向路由来实现它。把下面的内容添加到路由定义数组中:

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

  1. {
  2. path: '',
  3. redirectTo: '/dashboard',
  4. pathMatch: 'full'
  5. },

关于重定向的更多信息,见路由

添加导航到模版中

在模板上添加一个到仪表盘的导航链接,就放在Heroes(英雄列表)链接的上方。

src/app/app.component.ts (template-v3)

  1. template: `
  2. <h1>{{title}}</h1>
  3. <nav>
  4. <a routerLink="/dashboard">Dashboard</a>
  5. <a routerLink="/heroes">Heroes</a>
  6. </nav>
  7. <router-outlet></router-outlet>
  8. `

我们在<nav>标签中放了两个链接。 它们现在还没有作用,但稍后,当我们对这些链接添加样式时,会显得比较方便。

刷新浏览器。应用显示出了仪表盘,并可以在仪表盘和英雄列表之间导航了。

把英雄添加到仪表盘

我们想让仪表盘更有趣,比如:让用户一眼就能看到四个顶级英雄。

把元数据中的template属性替换为templateUrl属性,它将指向一个新的模板文件。

src/app/dashboard.component.ts (metadata)

  1. @Component({
  2. selector: 'my-dashboard',
  3. templateUrl: './dashboard.component.html',
  4. })

使用下列内容创建文件:

src/app/dashboard.component.html

  1. <h3>Top Heroes</h3>
  2. <div class="grid grid-pad">
  3. <div *ngFor="let hero of heroes" class="col-1-4">
  4. <div class="module hero">
  5. <h4>{{hero.name}}</h4>
  6. </div>
  7. </div>
  8. </div>

我们再次使用*ngFor来在英雄列表上迭代,并显示它们的名字。 还添加了一个额外的<div>元素,来帮助稍后的美化工作。

共享 HeroService

要想管理该组件的heroes数组,我们可以复用HeroService

在前面的章节中,我们从HeroesComponentproviders数组中移除了HeroService服务, 并把它添加到AppModuleproviders数组中。 这个改动创建了一个HeroService的单例对象,应用中的所有组件都可以使用它。 Angular 会把HeroService注入到DashboardComponent,我们就能在DashboardComponent中使用它了。

获取英雄数据

打开dashboard.component.ts文件,并添加下列import语句。

src/app/dashboard.component.ts (imports)

  1. import { Component, OnInit } from '@angular/core';
  2. import { Hero } from './hero';
  3. import { HeroService } from './hero.service';

我们需要实现OnInit接口,因为我们将在ngOnInit方法中初始化英雄数组 —— 就像上次一样。 我们需要导入Hero类和HeroService类来引用它们的数据类型。

我们现在就实现DashboardComponent类,像这样:

src/app/dashboard.component.ts (class)

  1. export class DashboardComponent implements OnInit {
  2. heroes: Hero[] = [];
  3. constructor(private heroService: HeroService) { }
  4. ngOnInit(): void {
  5. this.heroService.getHeroes()
  6. .then(heroes => this.heroes = heroes.slice(1, 5));
  7. }
  8. }

我们在之前的HeroesComponent中也看到过类似的逻辑:

在仪表盘中我们用Array.slice方法提取了四个英雄(第2、3、4、5个)。

刷新浏览器,在这个新的仪表盘中就看到了四个英雄。

虽然我们在HeroesComponent组件的底部显示了所选英雄的详情, 但用户还没法导航HeroDetailComponent组件。我们可以用下列方式导航到HeroDetailComponent

路由到一个英雄详情

我们将在app.module.ts中添加一个到HeroDetailComponent的路由,也就是配置其它路由的地方。

这个新路由的不寻常之处在于,我们必须告诉HeroDetailComponent该显示哪个英雄。 之前,我们不需要告诉HeroesComponent组件和DashboardComponent组件任何东西。

现在,父组件HeroesComponent通过数据绑定来把一个英雄对象设置为组件的hero属性。就像这样:

<hero-detail [hero]="selectedHero"></hero-detail>

显然,在我们的任何一个路由场景中它都无法工作。 最后一种场景肯定不行,我们无法将一个完整的 hero 对象嵌入到 URL 中!不过我们本来也不想这样。

参数化路由

我们可以把英雄的id添加到 URL 中。当导航到一个id为 11 的英雄时,我们期望的 URL 是这样的:

/detail/11

URL中的/detail/部分是固定不变的,但结尾的数字id部分会随着英雄的不同而变化。 我们要把路由中可变的那部分表示成一个参数 (parameter) 令牌 (token) ,代表英雄的id

配置带参数的路由

我们将使用下列路由定义

src/app/app.module.ts (hero detail)

  1. {
  2. path: 'detail/:id',
  3. component: HeroDetailComponent
  4. },

路径中的冒号 (:) 表示:id是一个占位符,当导航到这个HeroDetailComponent组件时,它将被填入一个特定英雄的id

别忘了在创建这个路由前导入英雄详情组件。

我们已经完成了本应用的路由的配置。

我们没有往模板中添加一个'英雄详情',这是因为用户不会直接点击导航栏中的链接去查看一个特定的英雄。 它们只会通过在英雄列表或者仪表盘中点击来显示一个英雄。

要想支持“点击英雄”,就得先对HeroDetailComponent进行修改,好让我们能导航到它。

修改HeroDetailComponent

在重写HeroDetailComponent之前,我们先看看它现在的样子:

src/app/hero-detail.component.ts (current)

  1. import { Component, Input } from '@angular/core';
  2. import { Hero } from './hero';
  3. @Component({
  4. selector: 'hero-detail',
  5. template: `
  6. <div *ngIf="hero">
  7. <h2>{{hero.name}} details!</h2>
  8. <div>
  9. <label>id: </label>{{hero.id}}
  10. </div>
  11. <div>
  12. <label>name: </label>
  13. <input [(ngModel)]="hero.name" placeholder="name"/>
  14. </div>
  15. </div>
  16. `
  17. })
  18. export class HeroDetailComponent {
  19. @Input() hero: Hero;
  20. }

模板不用修改,我们会用原来的方式显示英雄。导致这次大修的原因是如何获得这个英雄的数据。

我们不会再从父组件的属性绑定中接收英雄数据。 新的HeroDetailComponent应该从ActivatedRoute服务的可观察对象params中取得id参数, 并通过HeroService服务获取具有这个指定id的英雄数据。

先添加下列导入语句:

src/app/hero-detail.component.ts

  1. // Keep the Input import for now, you'll remove it later:
  2. import { Component, Input, OnInit } from '@angular/core';
  3. import { ActivatedRoute, Params } from '@angular/router';
  4. import { Location } from '@angular/common';
  5. import { HeroService } from './hero.service';

然后注入ActivatedRouteHeroService服务到构造函数中,将它们的值保存到私有变量中:

src/app/hero-detail.component.ts (constructor)

  1. constructor(
  2. private heroService: HeroService,
  3. private route: ActivatedRoute,
  4. private location: Location
  5. ) {}

src/app/hero-detail.component.ts (switchMap import)

import 'rxjs/add/operator/switchMap';

我们告诉这个类,我们要实现OnInit接口。

src/app/hero-detail.component.ts

export class HeroDetailComponent implements OnInit {

ngOnInit()生命周期钩子中,我们从ActivatedRoute服务的可观察对象params中提取id参数, 并且使用HeroService来获取具有这个id的英雄数据。。

src/app/hero-detail.component.ts

  1. ngOnInit(): void {
  2. this.route.params
  3. .switchMap((params: Params) => this.heroService.getHero(+params['id']))
  4. .subscribe(hero => this.hero = hero);
  5. }

注意switchMap运算符如何将可观察的路由参数中的 id 映射到一个新的Observable, 即HeroService.getHero()方法的结果。

如果用户在 getHero 请求执行的过程中再次导航这个组件,switchMap 再次调用HeroService.getHero()之前, 会取消之前的请求。

英雄的id是数字,而路由参数的值总是字符串。 所以我们需要通过 JavaScript 的 (+) 操作符把路由参数的值转成数字。

我需要取消订阅吗?

正如以前在路由与导航章的ActivatedRoute:一站式获取路由信息部分讲过的,Router管理它提供的可观察对象,并使订阅局部化。当组件被销毁时,会清除 订阅,防止内存泄漏,所以我们不需要从路由参数Observable取消订阅

添加 HeroService.getHero()

在前面的代码片段中HeroService没有getHero()方法。要解决这个问题,请打开HeroService并添加一个getHero()方法,它会根据idgetHeroes()中过滤英雄列表。

src/app/hero.service.ts (getHero)

  1. getHero(id: number): Promise<Hero> {
  2. return this.getHeroes()
  3. .then(heroes => heroes.find(hero => hero.id === id));
  4. }

回到原路

用户有多种方式导航到HeroDetailComponent

要导航到别处,用户可以点击AppComponent中的两个链接之一,也可以点击浏览器的后退按钮。 我们来添加第三个选项:一个goBack()方法,它使用之前注入的Location服务, 利用浏览器的历史堆栈,导航到上一步。

src/app/hero-detail.component.ts (goBack)

  1. goBack(): void {
  2. this.location.back();
  3. }

回退太多步会跑出我们的应用。 在真实的应用中,我们需要使用CanDeactivate守卫对此进行防范。 要了解更多,参见CanDeactivate

然后,我们通过一个事件绑定把此方法绑定到模板底部的 Back(后退)按钮上。

<button (click)="goBack()">Back</button>

修改模板,添加这个按钮以提醒我们还要做更多的改进, 并把模板移到独立的hero-detail.component.html文件中去。

src/app/hero-detail.component.html

  1. <div *ngIf="hero">
  2. <h2>{{hero.name}} details!</h2>
  3. <div>
  4. <label>id: </label>{{hero.id}}</div>
  5. <div>
  6. <label>name: </label>
  7. <input [(ngModel)]="hero.name" placeholder="name" />
  8. </div>
  9. <button (click)="goBack()">Back</button>
  10. </div>

然后修改组件元数据的templateUrl属性,让它指向我们刚刚创建的模板文件。

src/app/hero-detail.component.ts (metadata)

  1. @Component({
  2. selector: 'hero-detail',
  3. templateUrl: './hero-detail.component.html',
  4. })

刷新浏览器,查看结果。

选择一个仪表盘中的英雄

当用户从仪表盘中选择了一位英雄时,本应用要导航到HeroDetailComponent以查看和编辑所选的英雄。

虽然仪表盘英雄被显示为像按钮一样的方块,但是它们的行为应该像锚标签一样。 当鼠标移动到一个英雄方块上时,目标 URL 应该显示在浏览器的状态条上,用户应该能拷贝链接或者在新的浏览器标签页中打开英雄详情视图。

要达到这个效果,再次打开dashboard.component.html,将用来迭代的<div *ngFor...>替换为<a>,就像这样:

src/app/dashboard.component.html (repeated <a> tag)

<a *ngFor="let hero of heroes"  [routerLink]="['/detail', hero.id]"  class="col-1-4">

注意[routerLink]绑定。正如本章的部分所说, AppComponent模板中的顶级导航有一些路由器链接被设置固定的路径,例如“/dashboard”和“/heroes”。

这次,我们绑定了一个包含链接参数数组的表达式。 该数组有两个元素,目标路由和一个用来设置当前英雄的 id 值的路由参数

这两个数组项与之前在app.module.ts添加的参数化的英雄详情路由定义中的 path:id 对应。

src/app/app.module.ts (hero detail)

  1. {
  2. path: 'detail/:id',
  3. component: HeroDetailComponent
  4. },

刷新浏览器,并从仪表盘中选择一位英雄,应用就会直接导航到英雄的详情。

重构路由为一个路由模块

AppModule中有将近 20 行代码是用来配置四个路由的。 绝大多数应用有更多路由,并且它们还有守卫服务来保护不希望或未授权的导航。 (要了解守卫服务的更多知识,参见路由与导航页的路由守卫) 路由的配置可能迅速占领这个模块,并掩盖其主要目的,即为 Angular 编译器设置整个应用的关键配置。

我们应该重构路由配置到它自己的类。 什么样的类呢? 当前的RouterModule.forRoot()产生一个Angular ModuleWithProviders,所以这个路由类应该是一种模块类。 它应该是一个路由模块。要想了解更多,请参阅路由与导航一章的里程碑2:路由模块部分。

按约定,路由模块的名字应该包含 “Routing”,并与导航到的组件所在的模块的名称看齐。

app.module.ts所在目录创建app-routing.module.ts文件。将下面从AppModule类提取出来的代码拷贝进去:

src/app/app-routing.module.ts

  1. import { NgModule } from '@angular/core';
  2. import { RouterModule, Routes } from '@angular/router';
  3. import { DashboardComponent } from './dashboard.component';
  4. import { HeroesComponent } from './heroes.component';
  5. import { HeroDetailComponent } from './hero-detail.component';
  6. const routes: Routes = [
  7. { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
  8. { path: 'dashboard', component: DashboardComponent },
  9. { path: 'detail/:id', component: HeroDetailComponent },
  10. { path: 'heroes', component: HeroesComponent }
  11. ];
  12. @NgModule({
  13. imports: [ RouterModule.forRoot(routes) ],
  14. exports: [ RouterModule ]
  15. })
  16. export class AppRoutingModule {}

典型路由模块需要注意的有:

修改 AppModule

删除AppModule中的路由配置,并导入AppRoutingModule (使用 ES import语句导入,将它添加到NgModule.imports列表)。

下面是修改后的AppModule,与重构前的对比:

  1. import { NgModule } from '@angular/core';
  2. import { BrowserModule } from '@angular/platform-browser';
  3. import { FormsModule } from '@angular/forms';
  4. import { AppComponent } from './app.component';
  5. import { DashboardComponent } from './dashboard.component';
  6. import { HeroDetailComponent } from './hero-detail.component';
  7. import { HeroesComponent } from './heroes.component';
  8. import { HeroService } from './hero.service';
  9. import { AppRoutingModule } from './app-routing.module';
  10. @NgModule({
  11. imports: [
  12. BrowserModule,
  13. FormsModule,
  14. AppRoutingModule
  15. ],
  16. declarations: [
  17. AppComponent,
  18. DashboardComponent,
  19. HeroDetailComponent,
  20. HeroesComponent
  21. ],
  22. providers: [ HeroService ],
  23. bootstrap: [ AppComponent ]
  24. })
  25. export class AppModule { }

HeroesComponent 中选择一位英雄

HeroesComponent中,当前模板展示了一个主从风格的界面:上方是英雄列表,底下是所选英雄的详情。

src/app/heroes.component.ts (current template)

template: `
  <h1>{{title}}</h1>
  <h2>My Heroes</h2>
  <ul class="heroes">
    <li *ngFor="let hero of heroes"
      [class.selected]="hero === selectedHero"
      (click)="onSelect(hero)">
      <span class="badge">{{hero.id}}</span> {{hero.name}}
    </li>
  </ul>
  <hero-detail [hero]="selectedHero"></hero-detail>
`,

删除顶部的<h1>

删除模板最后带有<hero-detail>标签的那一行。

我们不在这里展示完整的HeroDetailComponent了。 而是在独立的路由和页面中显示英雄详情,就像我们在仪表盘中所做的那样。

但是,当用户从列表中选择一个英雄时,他们不会在导航到详情页, 而是在当前页显示一个Mini版详情视图,当用户点击一个按钮时,才导航到完整的详情页面。

添加 mini 版英雄详情

在模板底部原来放<hero-detail>的地方添加下列 HTML 片段:

src/app/heroes.component.ts

  1. <div *ngIf="selectedHero">
  2. <h2>
  3. {{selectedHero.name | uppercase}} is my hero
  4. </h2>
  5. <button (click)="gotoDetail()">View Details</button>
  6. </div>

点击一个英雄,用户将会在英雄列表的下方看到这些:

Mini版英雄

使用uppercase管道格式化

注意,英雄的名字全被显示成大写字母。那是uppercase管道的效果,借助它,我们能干预插值表达式绑定的过程。可以管道操作符 ( | ) 后面看到它。

{{selectedHero.name | uppercase}} is my hero

管道擅长做下列工作:格式化字符串、金额、日期和其它显示数据。 Angular 自带了一些管道,我们也可以写自己的管道。

关于管道的更多信息,参见管道

把内容移出组件文件

当用户点击查看详情按钮时,要让它能导航到HeroDetailComponent,我们还需要修改它。

这个组件文件太大了。要想在 HTML 和 CSS 的噪音中看清组件的工作逻辑太难了。

在做更多修改之前,我们先把模板和样式移到它们自己的文件中去:

首先,从heroes.component.ts中把模板内容移到新的heroes.component.html文件中,但不要把反引号也拷贝过去。就像heroes.component.ts一样,我们很快就能做完。接着,把样式的内容移到新的heroes.component.css文件中。

这两个新文件是这样的:

  1. <h2>My Heroes</h2>
  2. <ul class="heroes">
  3. <li *ngFor="let hero of heroes"
  4. [class.selected]="hero === selectedHero"
  5. (click)="onSelect(hero)">
  6. <span class="badge">{{hero.id}}</span> {{hero.name}}
  7. </li>
  8. </ul>
  9. <div *ngIf="selectedHero">
  10. <h2>
  11. {{selectedHero.name | uppercase}} is my hero
  12. </h2>
  13. <button (click)="gotoDetail()">View Details</button>
  14. </div>

现在,回来编辑heroes.component.ts中的组件元数据,删除templatestyles属性,代之以相应的templateUrlstyleUrls属性。让它们指向这些新文件。

src/app/heroes.component.ts (revised metadata)

  1. @Component({
  2. selector: 'my-heroes',
  3. templateUrl: './heroes.component.html',
  4. styleUrls: [ './heroes.component.css' ]
  5. })

styleUrls属性是一个由样式文件的文件名(包括路径)组成的数组。我们还可以列出来自多个不同位置的样式文件。

更新 HeroesComponent

点击按钮时,HeroesComponent导航到HeroDetailComponent。 该按钮的点击事件绑定到了gotoDetail()方法,它使用命令式的导航,告诉路由器去哪儿。

该方法需要对组件类做一些修改:

  1. 从 Angular 路由器库导入Router

  2. 在构造函数中注入Router(与HeroService一起)

  3. 实现gotoDetail(),调用路由器的navigate()方法

src/app/heroes.component.ts (gotoDetail)

  1. gotoDetail(): void {
  2. this.router.navigate(['/detail', this.selectedHero.id]);
  3. }

注意,我们将一个包含两个元素的链接参数数组 — 路径和路由参数 — 传递到路由的navigate(), 与之前在DashboardComponent中使用[routerLink]绑定一样。 修改完成的HeroesComponent类如下所示:

src/app/heroes.component.ts (class)

  1. export class HeroesComponent implements OnInit {
  2. heroes: Hero[];
  3. selectedHero: Hero;
  4. constructor(
  5. private router: Router,
  6. private heroService: HeroService) { }
  7. getHeroes(): void {
  8. this.heroService.getHeroes().then(heroes => this.heroes = heroes);
  9. }
  10. ngOnInit(): void {
  11. this.getHeroes();
  12. }
  13. onSelect(hero: Hero): void {
  14. this.selectedHero = hero;
  15. }
  16. gotoDetail(): void {
  17. this.router.navigate(['/detail', this.selectedHero.id]);
  18. }
  19. }

刷新浏览器,并开始点击。 我们能在应用中导航:从仪表盘到英雄详情再回来,从英雄列表到 mini 版英雄详情到英雄详情,再回到英雄列表。 我们可以在仪表盘和英雄列表之间跳来跳去。

我们已经满足了在本章开头设定的所有导航需求。

美化本应用

应用在功能上已经正常了,但还需要美化。 仪表盘上的英雄应该显示在一行上的几个方块中。 我们拿到了大约60行CSS来完成这件事,包括一些简单的媒体查询代码以实现响应式设计。

我们不能把这 60 来行 CSS 粘贴到组件元数据的styles中,否则它会淹没组件的工作逻辑。反之,我们应该在独立的*.css文件中编辑这些CSS。

dashboard.component.css文件添加到app目录下,并在组件元数据的styleUrls数组属性中引用它。就像这样:

src/app/dashboard.component.ts (styleUrls)

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

美化英雄详情

我们还拿到了一些HeroDetailComponent特有的 CSS 风格。

app目录下添加hero-detail.component.css文件, 并且在styleUrls数组中引用它 —— 就像之前在DashboardComponent中做过的那样。 同时删除hero``@Input装饰器属性和它的导入语句。

上述组件的 CSS 文件内容如下:

  1. label {
  2. display: inline-block;
  3. width: 3em;
  4. margin: .5em 0;
  5. color: #607D8B;
  6. font-weight: bold;
  7. }
  8. input {
  9. height: 2em;
  10. font-size: 1em;
  11. padding-left: .4em;
  12. }
  13. button {
  14. margin-top: 20px;
  15. font-family: Arial;
  16. background-color: #eee;
  17. border: none;
  18. padding: 5px 10px;
  19. border-radius: 4px;
  20. cursor: pointer; cursor: hand;
  21. }
  22. button:hover {
  23. background-color: #cfd8dc;
  24. }
  25. button:disabled {
  26. background-color: #eee;
  27. color: #ccc;
  28. cursor: auto;
  29. }

设计师还给了我们一些 CSS,用于让AppComponent中的导航链接看起来更像可被选择的按钮。 要让它们协同工作,我们得把那些链接包含在<nav>标签中。

app目录下添加一个app.component.css文件,内容如下:

src/app/app.component.css (navigation styles)

  1. h1 {
  2. font-size: 1.2em;
  3. color: #999;
  4. margin-bottom: 0;
  5. }
  6. h2 {
  7. font-size: 2em;
  8. margin-top: 0;
  9. padding-top: 0;
  10. }
  11. nav a {
  12. padding: 5px 10px;
  13. text-decoration: none;
  14. margin-top: 10px;
  15. display: inline-block;
  16. background-color: #eee;
  17. border-radius: 4px;
  18. }
  19. nav a:visited, a:link {
  20. color: #607D8B;
  21. }
  22. nav a:hover {
  23. color: #039be5;
  24. background-color: #CFD8DC;
  25. }
  26. nav a.active {
  27. color: #039be5;
  28. }

routerLinkActive指令

Angular路由器提供了routerLinkActive指令,我们可以用它来为匹配了活动路由的 HTML 导航元素自动添加一个 CSS 类。 我们唯一要做的就是为它定义样式。真好!

src/app/app.component.ts (active router links)

  1. template: `
  2. <h1>{{title}}</h1>
  3. <nav>
  4. <a routerLink="/dashboard" routerLinkActive="active">Dashboard</a>
  5. <a routerLink="/heroes" routerLinkActive="active">Heroes</a>
  6. </nav>
  7. <router-outlet></router-outlet>
  8. `,

首先把moduleId: module.id添加到AppComponent组件的@Component元数据中以启用相对于模块的文件URL。 然后添加styleUrls属性,使其指向这个CSS文件,代码如下:

styleUrls: ['./app.component.css'],

应用的全局样式

当我们把样式添加到组件中时,我们假定组件所需的一切 — HTML、CSS、程序代码 — 都在紧邻的地方。 这样,无论是把它们打包在一起还是在别的组件中复用它都会很容易。

我们也可以在所有组件之外创建应用级样式。

我们的设计师提供了一组基础样式,这些样式应用到的元素横跨整个应用。 它们与我们之前在开发环境时安装的整套样式对应。 下面是摘录:

src/styles.css (excerpt)

  1. /* Master Styles */
  2. h1 {
  3. color: #369;
  4. font-family: Arial, Helvetica, sans-serif;
  5. font-size: 250%;
  6. }
  7. h2, h3 {
  8. color: #444;
  9. font-family: Arial, Helvetica, sans-serif;
  10. font-weight: lighter;
  11. }
  12. body {
  13. margin: 2em;
  14. }
  15. body, input[text], button {
  16. color: #888;
  17. font-family: Cambria, Georgia;
  18. }
  19. /* . . . */
  20. /* everywhere else */
  21. * {
  22. font-family: Arial, Helvetica, sans-serif;
  23. }

如果在根目录下没有一个名叫styles.css的文件,就添加它。 确保它包含这里给出的主样式。 并编辑index.html来引用这个样式表。

src/index.html (link ref)

<link rel="stylesheet" href="styles.css">

看看现在的应用!我们的仪表盘、英雄列表和导航链接都漂亮多了!

查看导航栏

应用结构和代码

回顾一下本章在线例子 / 可下载的例子中范例代码。 验证我们是否已经得到了如下结构:

angular-tour-of-heroes
src
app
app.component.css
app.component.ts
app.module.ts
app-routing.module.ts
dashboard.component.css
dashboard.component.html
dashboard.component.ts
hero.service.ts
hero.ts
hero-detail.component.css
hero-detail.component.html
hero-detail.component.ts
heroes.component.css
heroes.component.html
heroes.component.ts
mock-heroes.ts
main.ts
index.html
styles.css
systemjs.config.js
tsconfig.json
node_modules ...
package.json

走过的路

本章中我们完成了这些:

我们的应用现在变成了这样:在线例子 / 可下载的例子

前方的路

我们有了很多用于构建应用的基石。 但我们仍然缺少很关键的一块:远程数据存取。

在下一章,我们将从硬编码模拟数据改为使用 http 服务从服务器获取数据。

下一步

HTTP