阅读(638) (0)

Angular 英雄之旅-添加导航

2022-07-12 17:38:38 更新

用路由添加导航支持

有一些《英雄之旅》的新需求:

  • 添加一个仪表盘视图
  • 添加在英雄列表仪表盘视图之间导航的能力
  • 无论在哪个视图中点击一个英雄,都会导航到该英雄的详情页
  • 在邮件中点击一个深链接,会直接打开一个特定英雄的详情视图

要查看本页所讲的范例程序,参阅现场演练 / 下载范例

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


添加 AppRoutingModule

在 Angular 中,最好在一个独立的顶层模块中加载和配置路由器,它专注于路由功能,然后由根模块 ​AppModule ​导入它。

按照惯例,这个模块类的名字叫做 ​AppRoutingModule​,并且位于 ​src/app​ 下的 ​app-routing.module.ts​ 文件中。

使用 CLI 生成它。

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

参数

详情

--flat

把这个文件放进了 src/app 中,而不是单独的目录中。

--module=app

告诉 CLI 把它注册到 AppModule 的 imports 数组中。

生成的文件是这样的:

import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

@NgModule({
  imports: [
    CommonModule
  ],
  declarations: []
})
export class AppRoutingModule { }

把它替换为如下代码:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { HeroesComponent } from './heroes/heroes.component';

const routes: Routes = [
  { path: 'heroes', component: HeroesComponent }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

首先,​app-routing.module.ts​ 会导入 ​RouterModule ​和 ​Routes​,以便该应用具有路由功能。配置好路由后,接着导入 ​HeroesComponent​,它将告诉路由器要去什么地方。

注意,对 ​CommonModule ​的引用和 ​declarations ​数组不是必要的,因此它们不再是 ​AppRoutingModule ​的一部分。以下各节将详细介绍 ​AppRoutingModule ​的其余部分。

路由

该文件的下一部分是你的路由配置。Routes 告诉路由器,当用户单击链接或将 URL 粘贴进浏览器地址栏时要显示哪个视图。

由于 ​app-routing.module.ts​ 已经导入了 ​HeroesComponent​,因此你可以直接在 ​routes ​数组中使用它:

const routes: Routes = [
  { path: 'heroes', component: HeroesComponent }
];

典型的 Angular ​Route ​具有两个属性:

属性

详情

path

用来匹配浏览器地址栏中 URL 的字符串。

component

导航到该路由时,路由器应该创建的组件。

这会告诉路由器把该 URL 与 ​path:'heroes'​ 匹配。如果网址类似于 ​localhost:4200/heroes​ 就显示 ​HeroesComponent​。

RouterModule.forRoot()

@NgModule​ 元数据会初始化路由器,并开始监听浏览器地址的变化。

下面的代码行将 ​RouterModule ​添加到 ​AppRoutingModule ​的 ​imports ​数组中,同时通过调用 ​RouterModule.forRoot()​ 来用这些 ​routes ​配置它:

imports: [ RouterModule.forRoot(routes) ],

这个方法之所以叫 ​forRoot()​,是因为你要在应用的顶层配置这个路由器。​forRoot()​ 方法会提供路由所需的服务提供者和指令,还会基于浏览器的当前 URL 执行首次导航。

接下来,​AppRoutingModule ​导出 ​RouterModule​,以便它在整个应用程序中生效。

exports: [ RouterModule ]

添加 RouterOutlet

打开 ​AppComponent ​的模板,把 ​<app-heroes>​ 元素替换为 ​<router-outlet>​ 元素。

<h1>{{title}}</h1>
<router-outlet></router-outlet>
<app-messages></app-messages>

AppComponent ​的模板不再需要 ​<app-heroes>​,因为只有当用户导航到这里时,才需要显示 ​HeroesComponent​。

<router-outlet>​ 会告诉路由器要在哪里显示路由的视图。

能在 ​AppComponent ​中使用 ​RouterOutlet​,是因为 ​AppModule ​导入了 ​AppRoutingModule​,而 ​AppRoutingModule ​中导出了 ​RouterModule​。在本教程开始时你运行的那个 ​ng generate​ 命令添加了这个导入,是因为 ​--module=app​ 标志。如果你手动创建 ​app-routing.module.ts​ 或使用了 CLI 之外的工具,你就要把 ​AppRoutingModule ​导入到 ​app.module.ts​ 中,并且把它添加到 ​NgModule ​的 ​imports ​数组中

试试看

你的 CLI 命令应该仍在运行吧。

ng serve

浏览器应该刷新,并显示着应用的标题,但是没有显示英雄列表。

看看浏览器的地址栏。URL 是以 ​/​ 结尾的。而到 ​HeroesComponent ​的路由路径是 ​/heroes​。

在地址栏中把 ​/heroes​ 追加到 URL 后面。你应该能看到熟悉的主从结构的英雄显示界面。

从浏览器地址栏中的 URL 中移除 ​/heroes​。浏览器就会刷新,并且显示本应用的标题,而不显示英雄列表。

添加路由链接 (routerLink)

理想情况下,用户应该能通过点击链接进行导航,而不用被迫把路由的 URL 粘贴到地址栏。

添加一个 ​<nav>​ 元素,并在其中放一个链接 ​<a>​ 元素,当点击它时,就会触发一个到 ​HeroesComponent ​的导航。修改过的 ​AppComponent ​模板如下:

<h1>{{title}}</h1>
<nav>
  <a routerLink="/heroes">Heroes</a>
</nav>
<router-outlet></router-outlet>
<app-messages></app-messages>

routerLink 属性的值为 ​"/heroes"​,路由器会用它来匹配出指向 ​HeroesComponent ​的路由。 ​routerLink ​是 RouterLink 指令的选择器,它会把用户的点击转换为路由器的导航操作。 它是 ​RouterModule ​中的另一个公共指令。

刷新浏览器,显示出了应用的标题和指向英雄列表的链接,但并没有显示英雄列表。

点击这个链接。地址栏变成了 ​/heroes​,并且显示出了英雄列表。

从下面的 最终代码中把私有 CSS 样式添加到 ​app.component.css​ 中,可以让导航链接变得更好看一点。

添加仪表盘视图

当有多个视图时,路由会更有价值。不过目前还只有一个英雄列表视图。

使用 CLI 添加一个 ​DashboardComponent​:

ng generate component dashboard

CLI 生成了 ​DashboardComponent ​的相关文件,并把它声明到 ​AppModule ​中。

把这三个文件中的内容改成这样:

  • src/app/dashboard/dashboard.component.html
  • <h2>Top Heroes</h2>
    <div class="heroes-menu">
      <a *ngFor="let hero of heroes">
        {{hero.name}}
      </a>
    </div>
  • src/app/dashboard/dashboard.component.ts
  • import { Component, OnInit } from '@angular/core';
    import { Hero } from '../hero';
    import { HeroService } from '../hero.service';
    
    @Component({
      selector: 'app-dashboard',
      templateUrl: './dashboard.component.html',
      styleUrls: [ './dashboard.component.css' ]
    })
    export class DashboardComponent implements OnInit {
      heroes: Hero[] = [];
    
      constructor(private heroService: HeroService) { }
    
      ngOnInit(): void {
        this.getHeroes();
      }
    
      getHeroes(): void {
        this.heroService.getHeroes()
          .subscribe(heroes => this.heroes = heroes.slice(1, 5));
      }
    }
  • src/app/dashboard/dashboard.component.css
  • /* DashboardComponent's private CSS styles */
    
    h2 {
      text-align: center;
    }
    
    .heroes-menu {
      padding: 0;
      margin: auto;
      max-width: 1000px;
    
      /* flexbox */
      display: flex;
      flex-direction: row;
      flex-wrap: wrap;
      justify-content: space-around;
      align-content: flex-start;
      align-items: flex-start;
    }
    
    a {
      background-color: #3f525c;
      border-radius: 2px;
      padding: 1rem;
      font-size: 1.2rem;
      text-decoration: none;
      display: inline-block;
      color: #fff;
      text-align: center;
      width: 100%;
      min-width: 70px;
      margin: .5rem auto;
      box-sizing: border-box;
    
      /* flexbox */
      order: 0;
      flex: 0 1 auto;
      align-self: auto;
    }
    
    @media (min-width: 600px) {
      a {
        width: 18%;
        box-sizing: content-box;
      }
    }
    
    a:hover {
      background-color: #000;
    }

这个模板用来表示由英雄名字链接组成的一个阵列。

  • *ngFor​ 复写器为组件的 ​heroes ​数组中的每个条目创建了一个链接。
  • 这些链接被 ​dashboard.component.css​ 中的样式格式化成了一些色块。
  • 这些链接还没有指向任何地方,但很快就会了

这个和 ​HeroesComponent ​类很像。

  • 它定义了一个 ​heroes ​数组属性。
  • 它的构造函数希望 Angular 把 ​HeroService ​注入到私有的 ​heroService ​属性中。
  • 在 ​ngOnInit()​ 生命周期钩子中调用 ​getHeroes()​。

这个 ​getHeroes()​ 函数会截取第 2 到 第 5 位英雄,也就是说只返回四个顶层英雄(第二,第三,第四和第五)。

getHeroes(): void {
  this.heroService.getHeroes()
    .subscribe(heroes => this.heroes = heroes.slice(1, 5));
}

添加仪表盘路由

要导航到仪表盘,路由器中就需要一个相应的路由。

把 ​DashboardComponent ​导入到 ​app-routing-module.ts​ 中。

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

把一个指向 ​DashboardComponent ​的路由添加到 ​routes ​数组中。

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

添加默认路由

当应用启动时,浏览器的地址栏指向了网站的根路径。它没有匹配到任何现存路由,因此路由器也不会导航到任何地方。​<router-outlet>​ 下方是空白的。

要让应用自动导航到这个仪表盘,请把下列路由添加到 ​routes ​数组中。

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

这个路由会把一个与空路径“完全匹配”的 URL 重定向到路径为 ​'/dashboard'​ 的路由。

浏览器刷新之后,路由器加载了 ​DashboardComponent​,并且浏览器的地址栏会显示出 ​/dashboard​ 这个 URL。

把仪表盘链接添加到壳组件中

应该允许用户通过点击页面顶部导航区的各个链接在 ​DashboardComponent ​和 ​HeroesComponent ​之间来回导航。

把仪表盘的导航链接添加到壳组件 ​AppComponent ​的模板中,就放在 Heroes 链接的前面。

<h1>{{title}}</h1>
<nav>
  <a routerLink="/dashboard">Dashboard</a>
  <a routerLink="/heroes">Heroes</a>
</nav>
<router-outlet></router-outlet>
<app-messages></app-messages>

刷新浏览器,你就能通过点击这些链接在这两个视图之间自由导航了。

导航到英雄详情

HeroDetailComponent ​可以显示所选英雄的详情。此刻,​HeroDetailComponent ​只能在 ​HeroesComponent ​的底部看到。

用户应该能通过三种途径看到这些详情。

  1. 通过在仪表盘中点击某个英雄。
  2. 通过在英雄列表中点击某个英雄。
  3. 通过把一个“深链接” URL 粘贴到浏览器的地址栏中来指定要显示的英雄。

在这一节,你将能导航到 ​HeroDetailComponent​,并把它从 ​HeroesComponent ​中解放出来。

从 HeroesComponent 中删除英雄详情

当用户在 ​HeroesComponent ​中点击某个英雄条目时,应用应该能导航到 ​HeroDetailComponent​,从英雄列表视图切换到英雄详情视图。英雄列表视图将不再显示,而英雄详情视图要显示出来。

打开 ​HeroesComponent ​的模板文件(​heroes/heroes.component.html​),并从底部删除 ​<app-hero-detail>​ 元素。

目前,点击某个英雄条目还没有反应。不过当你启用了到 ​HeroDetailComponent ​的路由之后,很快就能修复它。

添加英雄详情视图

要导航到 ​id ​为 ​11​ 的英雄的详情视图,类似于 ​~/detail/11​ 的 URL 将是一个不错的 URL。

打开 ​app-routing.module.ts​ 并导入 ​HeroDetailComponent​。

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

然后把一个参数化路由添加到 ​routes ​数组中,它要匹配指向英雄详情视图的路径。

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

path ​中的冒号(​:​)表示 ​:id​ 是一个占位符,它表示某个特定英雄的 ​id​。

此刻,应用中的所有路由都就绪了。

const routes: Routes = [
  { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
  { path: 'dashboard', component: DashboardComponent },
  { path: 'detail/:id', component: HeroDetailComponent },
  { path: 'heroes', component: HeroesComponent }
];

DashboardComponent 中的英雄链接

此刻,​DashboardComponent ​中的英雄连接还没有反应。

路由器已经有一个指向 ​HeroDetailComponent ​的路由了,修改仪表盘中的英雄连接,让它们通过参数化的英雄详情路由进行导航。

<a *ngFor="let hero of heroes"
  routerLink="/detail/{{hero.id}}">
  {{hero.name}}
</a>

你正在 ​*ngFor​ 复写器中使用 Angular 的插值绑定来把当前迭代的 ​hero.id​ 插入到每个 ​routerLink ​中。

HeroesComponent 中的英雄链接

HeroesComponent ​中的这些英雄条目都是 ​<li>​ 元素,它们的点击事件都绑定到了组件的 ​onSelect()​ 方法中。

<ul class="heroes">
  <li *ngFor="let hero of heroes">
    <button type="button" (click)="onSelect(hero)" [class.selected]="hero === selectedHero">
      <span class="badge">{{hero.id}}</span>
      <span class="name">{{hero.name}}</span>
    </button>
  </li>
</ul>

清理 ​<li>​,只保留它的 ​*ngFor​,把徽章(​<badge>​)和名字包裹进一个 ​<a>​ 元素中, 并且像仪表盘的模板中那样为这个 ​<a>​ 元素添加一个 ​routerLink ​属性。

<ul class="heroes">
  <li *ngFor="let hero of heroes">
    <a routerLink="/detail/{{hero.id}}">
      <span class="badge">{{hero.id}}</span> {{hero.name}}
    </a>
  </li>
</ul>

你还要修改私有样式表(​heroes.component.css​),让列表恢复到以前的外观。

移除死代码(可选)

虽然 ​HeroesComponent ​类仍然能正常工作,但 ​onSelect()​ 方法和 ​selectedHero ​属性已经没用了。

最好清理掉它们,将来你会体会到这么做的好处。下面是删除了死代码之后的类。

export class HeroesComponent implements OnInit {
  heroes: Hero[] = [];

  constructor(private heroService: HeroService) { }

  ngOnInit(): void {
    this.getHeroes();
  }

  getHeroes(): void {
    this.heroService.getHeroes()
    .subscribe(heroes => this.heroes = heroes);
  }
}

支持路由的 HeroDetailComponent

以前,父组件 ​HeroesComponent ​会设置 ​HeroDetailComponent.hero​ 属性,然后 ​HeroDetailComponent ​就会显示这个英雄。

HeroesComponent ​已经不会再那么做了。现在,当路由器会在响应形如 ​~/detail/11​ 的 URL 时创建 ​HeroDetailComponent​。

HeroDetailComponent ​需要从一种新的途径获取要显示的英雄。本节会讲解如下操作:

  • 获取创建本组件的路由
  • 从这个路由中提取出 ​id
  • 通过 ​HeroService ​从服务器上获取具有这个 ​id ​的英雄数据。

先添加下列导入语句:

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

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

然后把 ​ActivatedRoute​、​HeroService ​和 ​Location ​服务注入到构造函数中,将它们的值保存到私有变量里:

constructor(
  private route: ActivatedRoute,
  private heroService: HeroService,
  private location: Location
) {}

ActivatedRoute ​保存着到这个 ​HeroDetailComponent ​实例的路由信息。这个组件对从 URL 中提取的路由参数感兴趣。其中的 ​id ​参数就是要显示的英雄的 ​id​。

HeroService ​从远端服务器获取英雄数据,本组件将使用它来获取要显示的英雄。

location ​是一个 Angular 的服务,用来与浏览器打交道。 稍后,你就会使用它来导航回上一个视图。

从路由参数中提取 id

在 ​ngOnInit()​ 生命周期钩子 中调用 ​getHero()​,代码如下。

ngOnInit(): void {
  this.getHero();
}

getHero(): void {
  const id = Number(this.route.snapshot.paramMap.get('id'));
  this.heroService.getHero(id)
    .subscribe(hero => this.hero = hero);
}

route.snapshot​ 是一个路由信息的静态快照,抓取自组件刚刚创建完毕之后。

paramMap ​是一个从 URL 中提取的路由参数值的字典。​"id"​ 对应的值就是要获取的英雄的 ​id​。

路由参数总会是字符串。JavaScript 的 ​Number ​函数会把字符串转换成数字,英雄的 ​id ​就是数字类型。

刷新浏览器,应用挂了。出现一个编译错误,因为 ​HeroService ​没有一个名叫 ​getHero()​ 的方法。这就添加它。

添加 HeroService.getHero()

添加 ​HeroService​,并在 ​getHeroes()​ 后面添加如下的 ​getHero()​ 方法,它接收 ​id ​参数:

getHero(id: number): Observable<Hero> {
  // For now, assume that a hero with the specified `id` always exists.
  // Error handling will be added in the next step of the tutorial.
  const hero = HEROES.find(h => h.id === id)!;
  this.messageService.add(`HeroService: fetched hero id=${id}`);
  return of(hero);
}
重要:
反引号 ( ​`​ ) 用于定义 JavaScript 的 模板字符串字面量,以便嵌入 ​id​。

像 ​getHeroes()​ 一样,​getHero()​ 也有一个异步函数签名。它用 RxJS 的 ​of()​ 函数返回一个 ​Observable ​形式的模拟英雄数据

你将来可以用一个真实的 ​Http ​请求来重新实现 ​getHero()​,而不用修改调用了它的 ​HeroDetailComponent​。

试试看

刷新浏览器,应用又恢复正常了。你可以在仪表盘或英雄列表中点击一个英雄来导航到该英雄的详情视图。

如果你在浏览器的地址栏中粘贴了 ​localhost:4200/detail/11​,路由器也会导航到 ​id: 11​ 的英雄("Dr. Nice")的详情视图。

回到原路

通过点击浏览器的后退按钮,你可以回到英雄列表或仪表盘视图,这取决于你从哪里进入的详情视图。

如果能在 ​HeroDetail ​视图中也有这么一个按钮就更好了。

把一个后退按钮添加到组件模板的底部,并且把它绑定到组件的 ​goBack()​ 方法。

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

在组件类中添加一个 ​goBack()​ 方法,利用你以前注入的 ​Location ​服务在浏览器的历史栈中后退一步。

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

刷新浏览器,并开始点击。用户能在应用中导航:从仪表盘到英雄详情再回来,从英雄列表到 mini 版英雄详情到英雄详情,再回到英雄列表。

当你将一些私有 CSS 样式添加到 ​hero-detail.component.css​ 里之后,其细节看起来会更好,如下面的“查看最终代码”标签页中所示。

查看最终代码

下面是本页所提到的源代码。

AppRoutingModule、AppModule 和 HeroService

  • 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 { DashboardComponent } from './dashboard/dashboard.component';
    import { HeroDetailComponent } from './hero-detail/hero-detail.component';
    import { HeroesComponent } from './heroes/heroes.component';
    import { MessagesComponent } from './messages/messages.component';
    
    import { AppRoutingModule } from './app-routing.module';
    
    @NgModule({
      imports: [
        BrowserModule,
        FormsModule,
        AppRoutingModule
      ],
      declarations: [
        AppComponent,
        DashboardComponent,
        HeroesComponent,
        HeroDetailComponent,
        MessagesComponent
      ],
      bootstrap: [ AppComponent ]
    })
    export class AppModule { }
  • src/app/app-routing.module.ts
  • import { NgModule } from '@angular/core';
    import { RouterModule, Routes } from '@angular/router';
    
    import { DashboardComponent } from './dashboard/dashboard.component';
    import { HeroesComponent } from './heroes/heroes.component';
    import { HeroDetailComponent } from './hero-detail/hero-detail.component';
    
    const routes: Routes = [
      { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
      { path: 'dashboard', component: DashboardComponent },
      { path: 'detail/:id', component: HeroDetailComponent },
      { path: 'heroes', component: HeroesComponent }
    ];
    
    @NgModule({
      imports: [ RouterModule.forRoot(routes) ],
      exports: [ RouterModule ]
    })
    export class AppRoutingModule {}
  • src/app/hero.service.ts
  • import { Injectable } from '@angular/core';
    
    import { Observable, of } from 'rxjs';
    
    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[]> {
        const heroes = of(HEROES);
        this.messageService.add('HeroService: fetched heroes');
        return heroes;
      }
    
      getHero(id: number): Observable<Hero> {
        // For now, assume that a hero with the specified `id` always exists.
        // Error handling will be added in the next step of the tutorial.
        const hero = HEROES.find(h => h.id === id)!;
        this.messageService.add(`HeroService: fetched hero id=${id}`);
        return of(hero);
      }
    }

AppComponent

  • src/app/app.component.html
  • <h1>{{title}}</h1>
    <nav>
      <a routerLink="/dashboard">Dashboard</a>
      <a routerLink="/heroes">Heroes</a>
    </nav>
    <router-outlet></router-outlet>
    <app-messages></app-messages>
  • src/app/app.component.ts
  • import { Component } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.css']
    })
    export class AppComponent {
      title = 'Tour of Heroes';
    }
  • src/app/app.component.css
  • /* AppComponent's private CSS styles */
    h1 {
      margin-bottom: 0;
    }
    nav a {
      padding: 1rem;
      text-decoration: none;
      margin-top: 10px;
      display: inline-block;
      background-color: #e8e8e8;
      color: #3d3d3d;
      border-radius: 4px;
    }
    
    nav a:hover {
      color: white;
      background-color: #42545C;
    }
    nav a.active {
      background-color: black;
    }

DashboardComponent

  • src/app/dashboard/dashboard.component.html
  • <h2>Top Heroes</h2>
    <div class="heroes-menu">
      <a *ngFor="let hero of heroes"
        routerLink="/detail/{{hero.id}}">
        {{hero.name}}
      </a>
    </div>
  • src/app/dashboard/dashboard.component.ts
  • import { Component, OnInit } from '@angular/core';
    import { Hero } from '../hero';
    import { HeroService } from '../hero.service';
    
    @Component({
      selector: 'app-dashboard',
      templateUrl: './dashboard.component.html',
      styleUrls: [ './dashboard.component.css' ]
    })
    export class DashboardComponent implements OnInit {
      heroes: Hero[] = [];
    
      constructor(private heroService: HeroService) { }
    
      ngOnInit(): void {
        this.getHeroes();
      }
    
      getHeroes(): void {
        this.heroService.getHeroes()
          .subscribe(heroes => this.heroes = heroes.slice(1, 5));
      }
    }
  • src/app/dashboard/dashboard.component.css
  • /* DashboardComponent's private CSS styles */
    
    h2 {
      text-align: center;
    }
    
    .heroes-menu {
      padding: 0;
      margin: auto;
      max-width: 1000px;
    
      /* flexbox */
      display: flex;
      flex-direction: row;
      flex-wrap: wrap;
      justify-content: space-around;
      align-content: flex-start;
      align-items: flex-start;
    }
    
    a {
      background-color: #3f525c;
      border-radius: 2px;
      padding: 1rem;
      font-size: 1.2rem;
      text-decoration: none;
      display: inline-block;
      color: #fff;
      text-align: center;
      width: 100%;
      min-width: 70px;
      margin: .5rem auto;
      box-sizing: border-box;
    
      /* flexbox */
      order: 0;
      flex: 0 1 auto;
      align-self: auto;
    }
    
    @media (min-width: 600px) {
      a {
        width: 18%;
        box-sizing: content-box;
      }
    }
    
    a:hover {
      background-color: #000;
    }

HeroesComponent

  • src/app/heroes/heroes.component.html
  • <h2>My Heroes</h2>
    <ul class="heroes">
      <li *ngFor="let hero of heroes">
        <a routerLink="/detail/{{hero.id}}">
          <span class="badge">{{hero.id}}</span> {{hero.name}}
        </a>
      </li>
    </ul>
  • src/app/heroes/heroes.component.ts
  • import { Component, OnInit } from '@angular/core';
    
    import { Hero } from '../hero';
    import { HeroService } from '../hero.service';
    
    @Component({
      selector: 'app-heroes',
      templateUrl: './heroes.component.html',
      styleUrls: ['./heroes.component.css']
    })
    export class HeroesComponent implements OnInit {
      heroes: Hero[] = [];
    
      constructor(private heroService: HeroService) { }
    
      ngOnInit(): void {
        this.getHeroes();
      }
    
      getHeroes(): void {
        this.heroService.getHeroes()
        .subscribe(heroes => this.heroes = heroes);
      }
    }
  • src/app/heroes/heroes.component.css
  • /* HeroesComponent's private CSS styles */
    .heroes {
      margin: 0 0 2em 0;
      list-style-type: none;
      padding: 0;
      width: 15em;
    }
    .heroes li {
      position: relative;
      cursor: pointer;
    }
    
    .heroes li:hover {
      left: .1em;
    }
    
    .heroes a {
      color: #333;
      text-decoration: none;
      background-color: #EEE;
      margin: .5em;
      padding: .3em 0;
      height: 1.6em;
      border-radius: 4px;
      display: block;
      width: 100%;
    }
    
    .heroes a:hover {
      color: #2c3a41;
      background-color: #e6e6e6;
    }
    
    .heroes a:active {
      background-color: #525252;
      color: #fafafa;
    }
    
    .heroes .badge {
      display: inline-block;
      font-size: small;
      color: white;
      padding: 0.8em 0.7em 0 0.7em;
      background-color: #405061;
      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;
    }

HeroDetailComponent

  • src/app/hero-detail/hero-detail.component.html
  • <div *ngIf="hero">
      <h2>{{hero.name | uppercase}} Details</h2>
      <div><span>id: </span>{{hero.id}}</div>
      <div>
        <label for="hero-name">Hero name: </label>
        <input id="hero-name" [(ngModel)]="hero.name" placeholder="Hero name"/>
      </div>
      <button type="button" (click)="goBack()">go back</button>
    </div>
  • src/app/hero-detail/hero-detail.component.ts
  • import { Component, OnInit } from '@angular/core';
    import { ActivatedRoute } from '@angular/router';
    import { Location } from '@angular/common';
    
    import { Hero } from '../hero';
    import { HeroService } from '../hero.service';
    
    @Component({
      selector: 'app-hero-detail',
      templateUrl: './hero-detail.component.html',
      styleUrls: [ './hero-detail.component.css' ]
    })
    export class HeroDetailComponent implements OnInit {
      hero: Hero | undefined;
    
      constructor(
        private route: ActivatedRoute,
        private heroService: HeroService,
        private location: Location
      ) {}
    
      ngOnInit(): void {
        this.getHero();
      }
    
      getHero(): void {
        const id = Number(this.route.snapshot.paramMap.get('id'));
        this.heroService.getHero(id)
          .subscribe(hero => this.hero = hero);
      }
    
      goBack(): void {
        this.location.back();
      }
    }
  • src/app/hero-detail/hero-detail.component.css
  • /* HeroDetailComponent's private CSS styles */
    label {
      color: #435960;
      font-weight: bold;
    }
    input {
      font-size: 1em;
      padding: .5rem;
    }
    button {
      margin-top: 20px;
      background-color: #eee;
      padding: 1rem;
      border-radius: 4px;
      font-size: 1rem;
    }
    button:hover {
      background-color: #cfd8dc;
    }
    button:disabled {
      background-color: #eee;
      color: #ccc;
      cursor: auto;
    }

小结

  • 添加了 Angular 路由器在各个不同组件之间导航
  • 你使用一些 ​<a>​ 链接和一个 ​<router-outlet>​ 把 ​AppComponent ​转换成了一个导航用的壳组件
  • 你在 ​AppRoutingModule ​中配置了路由器
  • 你定义了一些简单路由、一个重定向路由和一个参数化路由
  • 你在 ​<a>​ 元素中使用了 ​routerLink ​指令
  • 你把一个紧耦合的主从视图重构成了带路由的详情视图
  • 你使用路由链接参数来导航到所选英雄的详情视图
  • 在多个组件之间共享了 ​HeroService ​服务