填写这份《一分钟调查》,帮我们(开发组)做得更好!去填写Home

路由

Add in-app navigation (routing)

有一些《英雄指南》的新需求:

There are new requirements for the Tour of Heroes app:

  • 添加一个仪表盘视图。

    Add a Dashboard view.

  • 添加在英雄列表仪表盘视图之间导航的能力。

    Add the ability to navigate between the Heroes and Dashboard views.

  • 无论在哪个视图中点击一个英雄,都会导航到该英雄的详情页。

    When users click a hero name in either view, navigate to a detail view of the selected hero.

  • 在邮件中点击一个深链接,会直接打开一个特定英雄的详情视图。

    When users click a deep link in an email, open the detail view for a particular hero.

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

When you’re done, users will be able to navigate the app like this:

添加 AppRoutingModule

Add the AppRoutingModule

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

In Angular, the best practice is to load and configure the router in a separate, top-level module that is dedicated to routing and imported by the root AppModule.

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

By convention, the module class name is AppRoutingModule and it belongs in the app-routing.module.ts in the src/app folder.

使用 CLI 生成它。

Use the CLI to generate it.

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

--flat 把这个文件放进了 src/app 中,而不是单独的目录中。
--module=app 告诉 CLI 把它注册到 AppModuleimports 数组中。

--flat puts the file in src/app instead of its own folder.
--module=app tells the CLI to register it in the imports array of the AppModule.

生成的文件是这样的:

The generated file looks like this:

import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; @NgModule({ imports: [ CommonModule ], declarations: [] }) export class AppRoutingModule { }
src/app/app-routing.module.ts (generated)
      
      import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';

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

把它替换为如下代码:

Replace it with the following:

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 { }
src/app/app-routing.module.ts (updated)
      
      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 { }
    

首先,AppRoutingModule 会导入RouterModuleRoutes,以便该应用具有路由功能。配置好路由后,接着导入 HeroesComponent,它将告诉路由器要去什么地方。

First, AppRoutingModule imports RouterModule and Routes so the app can have routing functionality. The next import, HeroesComponent, will give the Router somewhere to go once you configure the routes.

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

Notice that the CommonModule references and declarations array are unnecessary, so are no longer part of AppRoutingModule. The following sections explain the rest of the AppRoutingModule in more detail.

路由

Routes

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

The next part of the file is where you configure your routes. Routes tell the Router which view to display when a user clicks a link or pastes a URL into the browser address bar.

由于 AppRoutingModule 已经导入了 HeroesComponent,因此你可以直接在 routes 数组中使用它:

Since AppRoutingModule already imports HeroesComponent, you can use it in the routes array:

const routes: Routes = [ { path: 'heroes', component: HeroesComponent } ];
src/app/app-routing.module.ts
      
      const routes: Routes = [
  { path: 'heroes', component: HeroesComponent }
];
    

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

A typical Angular Route has two properties:

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

    path: a string that matches the URL in the browser address bar.

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

    component: the component that the router should create when navigating to this route.

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

This tells the router to match that URL to path: 'heroes' and display the HeroesComponent when the URL is something like localhost:4200/heroes.

RouterModule.forRoot()

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

The @NgModule metadata initializes the router and starts it listening for browser location changes.

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

The following line adds the RouterModule to the AppRoutingModule imports array and configures it with the routes in one step by calling RouterModule.forRoot():

imports: [ RouterModule.forRoot(routes) ],
src/app/app-routing.module.ts
      
      imports: [ RouterModule.forRoot(routes) ],
    

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

The method is called forRoot() because you configure the router at the application's root level. The forRoot() method supplies the service providers and directives needed for routing, and performs the initial navigation based on the current browser URL.

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

Next, AppRoutingModule exports RouterModule so it will be available throughout the app.

exports: [ RouterModule ]
src/app/app-routing.module.ts (exports array)
      
      exports: [ RouterModule ]
    

添加路由出口 RouterOutlet

Add RouterOutlet

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

Open the AppComponent template and replace the <app-heroes> element with a <router-outlet> element.

<h1>{{title}}</h1> <router-outlet></router-outlet> <app-messages></app-messages>
src/app/app.component.html (router-outlet)
      
      <h1>{{title}}</h1>
<router-outlet></router-outlet>
<app-messages></app-messages>
    

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

The AppComponent template no longer needs <app-heroes> because the app will only display the HeroesComponent when the user navigates to it.

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

The <router-outlet> tells the router where to display routed views.

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

The RouterOutlet is one of the router directives that became available to the AppComponent because AppModule imports AppRoutingModule which exported RouterModule. The ng generate command you ran at the start of this tutorial added this import because of the --module=app flag. If you manually created app-routing.module.ts or used a tool other than the CLI to do so, you'll need to import AppRoutingModule into app.module.ts and add it to the imports array of the NgModule.

试试看

Try it

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

You should still be running with this CLI command.

ng serve
      
      ng serve
    

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

The browser should refresh and display the app title but not the list of heroes.

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

Look at the browser's address bar. The URL ends in /. The route path to HeroesComponent is /heroes.

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

Append /heroes to the URL in the browser address bar. You should see the familiar heroes master/detail view.

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

Ideally, users should be able to click a link to navigate rather than pasting a route URL into the address bar.

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

Add a <nav> element and, within that, an anchor element that, when clicked, triggers navigation to the HeroesComponent. The revised AppComponent template looks like this:

<h1>{{title}}</h1> <nav> <a routerLink="/heroes">Heroes</a> </nav> <router-outlet></router-outlet> <app-messages></app-messages>
src/app/app.component.html (heroes RouterLink)
      
      <h1>{{title}}</h1>
<nav>
  <a routerLink="/heroes">Heroes</a>
</nav>
<router-outlet></router-outlet>
<app-messages></app-messages>
    

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

A routerLink attribute is set to "/heroes", the string that the router matches to the route to HeroesComponent. The routerLink is the selector for the RouterLink directive that turns user clicks into router navigations. It's another of the public directives in the RouterModule.

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

The browser refreshes and displays the app title and heroes link, but not the heroes list.

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

Click the link. The address bar updates to /heroes and the list of heroes appears.

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

Make this and future navigation links look better by adding private CSS styles to app.component.css as listed in the final code review below.

添加仪表盘视图

Add a dashboard view

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

Routing makes more sense when there are multiple views. So far there's only the heroes view.

使用 CLI 添加一个 DashboardComponent

Add a DashboardComponent using the CLI:

ng generate component dashboard
      
      ng generate component dashboard
    

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

The CLI generates the files for the DashboardComponent and declares it in AppModule.

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

Replace the default file content in these three files as follows:

<h3>Top Heroes</h3> <div class="grid grid-pad"> <a *ngFor="let hero of heroes" class="col-1-4"> <div class="module hero"> <h4>{{hero.name}}</h4> </div> </a> </div>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() { this.getHeroes(); } getHeroes(): void { this.heroService.getHeroes() .subscribe(heroes => this.heroes = heroes.slice(1, 5)); } }/* DashboardComponent's private CSS styles */ [class*='col-'] { float: left; padding-right: 20px; padding-bottom: 20px; } [class*='col-']:last-of-type { padding-right: 0; } a { text-decoration: none; } *, *:after, *:before { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } h3 { text-align: center; margin-bottom: 0; } h4 { position: relative; } .grid { margin: 0; } .col-1-4 { width: 25%; } .module { padding: 20px; text-align: center; color: #eee; max-height: 120px; min-width: 120px; background-color: #3f525c; border-radius: 2px; } .module:hover { background-color: #eee; cursor: pointer; color: #607d8b; } .grid-pad { padding: 10px 0; } .grid-pad > [class*='col-']:last-of-type { padding-right: 20px; } @media (max-width: 600px) { .module { font-size: 10px; max-height: 75px; } } @media (max-width: 1024px) { .grid { margin: 0; } .module { min-width: 60px; } }
      
      <h3>Top Heroes</h3>
<div class="grid grid-pad">
  <a *ngFor="let hero of heroes" class="col-1-4">
    <div class="module hero">
      <h4>{{hero.name}}</h4>
    </div>
  </a>
</div>
    

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

The template presents a grid of hero name links.

  • *ngFor 复写器为组件的 heroes 数组中的每个条目创建了一个链接。

    The *ngFor repeater creates as many links as are in the component's heroes array.

  • 这些链接被 dashboard.component.css 中的样式格式化成了一些色块。

    The links are styled as colored blocks by the dashboard.component.css.

  • 这些链接还没有指向任何地方,但很快就会了

    The links don't go anywhere yet but they will shortly.

这个HeroesComponent 类很像。

The class is similar to the HeroesComponent class.

  • 它定义了一个 heroes 数组属性。

    It defines a heroes array property.

  • 它的构造函数希望 Angular 把 HeroService 注入到私有的 heroService 属性中。

    The constructor expects Angular to inject the HeroService into a private heroService property.

  • ngOnInit() 生命周期钩子中调用 getHeroes()

    The ngOnInit() lifecycle hook calls getHeroes().

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

This getHeroes() returns the sliced list of heroes at positions 1 and 5, returning only four of the Top Heroes (2nd, 3rd, 4th, and 5th).

getHeroes(): void { this.heroService.getHeroes() .subscribe(heroes => this.heroes = heroes.slice(1, 5)); }
src/app/dashboard/dashboard.component.ts
      
      getHeroes(): void {
  this.heroService.getHeroes()
    .subscribe(heroes => this.heroes = heroes.slice(1, 5));
}
    

添加仪表盘路由

Add the dashboard route

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

To navigate to the dashboard, the router needs an appropriate route.

DashboardComponent 导入到 AppRoutingModule 中。

Import the DashboardComponent in the AppRoutingModule.

import { DashboardComponent } from './dashboard/dashboard.component';
src/app/app-routing.module.ts (import DashboardComponent)
      
      import { DashboardComponent }   from './dashboard/dashboard.component';
    

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

Add a route to the AppRoutingModule.routes array that matches a path to the DashboardComponent.

{ path: 'dashboard', component: DashboardComponent },
src/app/app-routing.module.ts
      
      { path: 'dashboard', component: DashboardComponent },
    

添加默认路由

Add a default route

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

When the app starts, the browser's address bar points to the web site's root. That doesn't match any existing route so the router doesn't navigate anywhere. The space below the <router-outlet> is blank.

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

To make the app navigate to the dashboard automatically, add the following route to the AppRoutingModule.Routes array.

{ path: '', redirectTo: '/dashboard', pathMatch: 'full' },
src/app/app-routing.module.ts
      
      { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
    

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

This route redirects a URL that fully matches the empty path to the route whose path is '/dashboard'.

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

After the browser refreshes, the router loads the DashboardComponent and the browser address bar shows the /dashboard URL.

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

The user should be able to navigate back and forth between the DashboardComponent and the HeroesComponent by clicking links in the navigation area near the top of the page.

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

Add a dashboard navigation link to the AppComponent shell template, just above the Heroes link.

<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.html
      
      <h1>{{title}}</h1>
<nav>
  <a routerLink="/dashboard">Dashboard</a>
  <a routerLink="/heroes">Heroes</a>
</nav>
<router-outlet></router-outlet>
<app-messages></app-messages>
    

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

After the browser refreshes you can navigate freely between the two views by clicking the links.

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

The HeroDetailsComponent displays details of a selected hero. At the moment the HeroDetailsComponent is only visible at the bottom of the HeroesComponent

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

The user should be able to get to these details in three ways.

  1. 通过在仪表盘中点击某个英雄。

    By clicking a hero in the dashboard.

  2. 通过在英雄列表中点击某个英雄。

    By clicking a hero in the heroes list.

  3. 通过把一个“深链接” URL 粘贴到浏览器的地址栏中来指定要显示的英雄。

    By pasting a "deep link" URL into the browser address bar that identifies the hero to display.

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

In this section, you'll enable navigation to the HeroDetailsComponent and liberate it from the HeroesComponent.

HeroesComponent 中删除英雄详情

Delete hero details from HeroesComponent

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

When the user clicks a hero item in the HeroesComponent, the app should navigate to the HeroDetailComponent, replacing the heroes list view with the hero detail view. The heroes list view should no longer show hero details as it does now.

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

Open the HeroesComponent template (heroes/heroes.component.html) and delete the <app-hero-detail> element from the bottom.

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

Clicking a hero item now does nothing. You'll fix that shortly after you enable routing to the HeroDetailComponent.

添加英雄详情视图

Add a hero detail route

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

A URL like ~/detail/11 would be a good URL for navigating to the Hero Detail view of the hero whose id is 11.

打开 AppRoutingModule 并导入 HeroDetailComponent

Open AppRoutingModule and import HeroDetailComponent.

import { HeroDetailComponent } from './hero-detail/hero-detail.component';
src/app/app-routing.module.ts (import HeroDetailComponent)
      
      import { HeroDetailComponent }  from './hero-detail/hero-detail.component';
    

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

Then add a parameterized route to the AppRoutingModule.routes array that matches the path pattern to the hero detail view.

{ path: 'detail/:id', component: HeroDetailComponent },
src/app/app-routing.module.ts
      
      { path: 'detail/:id', component: HeroDetailComponent },
    

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

The colon (:) in the path indicates that :id is a placeholder for a specific hero id.

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

At this point, all application routes are in place.

const routes: Routes = [ { path: '', redirectTo: '/dashboard', pathMatch: 'full' }, { path: 'dashboard', component: DashboardComponent }, { path: 'detail/:id', component: HeroDetailComponent }, { path: 'heroes', component: HeroesComponent } ];
src/app/app-routing.module.ts (all routes)
      
      const routes: Routes = [
  { path: '', redirectTo: '/dashboard', pathMatch: 'full' },
  { path: 'dashboard', component: DashboardComponent },
  { path: 'detail/:id', component: HeroDetailComponent },
  { path: 'heroes', component: HeroesComponent }
];
    

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

The DashboardComponent hero links do nothing at the moment.

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

Now that the router has a route to HeroDetailComponent, fix the dashboard hero links to navigate via the parameterized dashboard route.

<a *ngFor="let hero of heroes" class="col-1-4" routerLink="/detail/{{hero.id}}"> <div class="module hero"> <h4>{{hero.name}}</h4> </div> </a>
src/app/dashboard/dashboard.component.html (hero links)
      
      <a *ngFor="let hero of heroes" class="col-1-4"
    routerLink="/detail/{{hero.id}}">
  <div class="module hero">
    <h4>{{hero.name}}</h4>
  </div>
</a>
    

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

You're using Angular interpolation binding within the *ngFor repeater to insert the current iteration's hero.id into each routerLink.

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

The hero items in the HeroesComponent are <li> elements whose click events are bound to the component's onSelect() method.

<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>
src/app/heroes/heroes.component.html (list with onSelect)
      
      <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>
    

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

Strip the <li> back to just its *ngFor, wrap the badge and name in an anchor element (<a>), and add a routerLink attribute to the anchor that is the same as in the dashboard template

<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.html (list with links)
      
      <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),让列表恢复到以前的外观。 修改后的样式表参见本指南底部的最终代码

You'll have to fix the private stylesheet (heroes.component.css) to make the list look as it did before. Revised styles are in the final code review at the bottom of this guide.

移除死代码(可选)

Remove dead code (optional)

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

While the HeroesComponent class still works, the onSelect() method and selectedHero property are no longer used.

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

It's nice to tidy up and you'll be grateful to yourself later. Here's the class after pruning away the dead code.

export class HeroesComponent implements OnInit { heroes: Hero[]; constructor(private heroService: HeroService) { } ngOnInit() { this.getHeroes(); } getHeroes(): void { this.heroService.getHeroes() .subscribe(heroes => this.heroes = heroes); } }
src/app/heroes/heroes.component.ts (cleaned up)
      
      export class HeroesComponent implements OnInit {
  heroes: Hero[];

  constructor(private heroService: HeroService) { }

  ngOnInit() {
    this.getHeroes();
  }

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

支持路由的 HeroDetailComponent

Routable HeroDetailComponent

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

Previously, the parent HeroesComponent set the HeroDetailComponent.hero property and the HeroDetailComponent displayed the hero.

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

HeroesComponent doesn't do that anymore. Now the router creates the HeroDetailComponent in response to a URL such as ~/detail/11.

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

The HeroDetailComponent needs a new way to obtain the hero-to-display. This section explains the following:

  • 获取创建本组件的路由

    Get the route that created it

  • 从这个路由中提取出 id

    Extract the id from the route

  • 通过 HeroService 从服务器上获取具有这个 id 的英雄数据。

    Acquire the hero with that id from the server via the HeroService

先添加下列导入语句:

Add the following imports:

import { ActivatedRoute } from '@angular/router'; import { Location } from '@angular/common'; import { HeroService } from '../hero.service';
src/app/hero-detail/hero-detail.component.ts
      
      import { ActivatedRoute } from '@angular/router';
import { Location } from '@angular/common';

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

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

Inject the ActivatedRoute, HeroService, and Location services into the constructor, saving their values in private fields:

constructor( private route: ActivatedRoute, private heroService: HeroService, private location: Location ) {}
src/app/hero-detail/hero-detail.component.ts
      
      constructor(
  private route: ActivatedRoute,
  private heroService: HeroService,
  private location: Location
) {}
    

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

The ActivatedRouteholds information about the route to this instance of the HeroDetailComponent. This component is interested in the route's parameters extracted from the URL. The "id" parameter is the id of the hero to display.

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

The HeroServicegets hero data from the remote server and this component will use it to get the hero-to-display.

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

The locationis an Angular service for interacting with the browser. You'll use it later to navigate back to the view that navigated here.

从路由参数中提取 id

Extract the id route parameter

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

In the ngOnInit() lifecycle hook call getHero() and define it as follows.

ngOnInit(): void { this.getHero(); } getHero(): void { const id = +this.route.snapshot.paramMap.get('id'); this.heroService.getHero(id) .subscribe(hero => this.hero = hero); }
src/app/hero-detail/hero-detail.component.ts
      
      ngOnInit(): void {
  this.getHero();
}

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

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

The route.snapshot is a static image of the route information shortly after the component was created.

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

The paramMap is a dictionary of route parameter values extracted from the URL. The "id" key returns the id of the hero to fetch.

路由参数总会是字符串。 JavaScript 的 (+) 操作符会把字符串转换成数字,英雄的 id 就是数字类型。

Route parameters are always strings. The JavaScript (+) operator converts the string to a number, which is what a hero id should be.

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

The browser refreshes and the app crashes with a compiler error. HeroService doesn't have a getHero() method. Add it now.

添加 HeroService.getHero()

Add HeroService.getHero()

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

Open HeroService and add the following getHero() method with the id after the getHeroes() method:

getHero(id: number): Observable<Hero> { // TODO: send the message _after_ fetching the hero this.messageService.add(`HeroService: fetched hero id=${id}`); return of(HEROES.find(hero => hero.id === id)); }
src/app/hero.service.ts (getHero)
      
      getHero(id: number): Observable<Hero> {
  // TODO: send the message _after_ fetching the hero
  this.messageService.add(`HeroService: fetched hero id=${id}`);
  return of(HEROES.find(hero => hero.id === id));
}
    

注意,反引号 ( ` ) 用于定义 JavaScript 的 模板字符串字面量,以便嵌入 id

Note the backticks ( ` ) that define a JavaScript template literal for embedding the id.

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

Like getHeroes(), getHero() has an asynchronous signature. It returns a mock hero as an Observable, using the RxJS of() function.

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

You'll be able to re-implement getHero() as a real Http request without having to change the HeroDetailComponent that calls it.

试试看

Try it

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

The browser refreshes and the app is working again. You can click a hero in the dashboard or in the heroes list and navigate to that hero's detail view.

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

If you paste localhost:4200/detail/11 in the browser address bar, the router navigates to the detail view for the hero with id: 11, "Dr Nice".

回到原路

Find the way back

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

By clicking the browser's back button, you can go back to the hero list or dashboard view, depending upon which sent you to the detail view.

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

It would be nice to have a button on the HeroDetail view that can do that.

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

Add a go back button to the bottom of the component template and bind it to the component's goBack() method.

<button (click)="goBack()">go back</button>
src/app/hero-detail/hero-detail.component.html (back button)
      
      <button (click)="goBack()">go back</button>
    

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

Add a goBack() method to the component class that navigates backward one step in the browser's history stack using the Location service that you injected previously.

goBack(): void { this.location.back(); }
src/app/hero-detail/hero-detail.component.ts (goBack)
      
      goBack(): void {
  this.location.back();
}
    

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

Refresh the browser and start clicking. Users can navigate around the app, from the dashboard to hero details and back, from heroes list to the mini detail to the hero details and back to the heroes again.

查看最终代码

Final code review

你的应用应该变成了这样现场演练 / 下载范例。本页所提及的代码文件如下:

Here are the code files discussed on this page and your app should look like this现场演练 / 下载范例.

AppRoutingModuleAppModuleHeroService

AppRoutingModule, AppModule, and HeroService

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 {}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 { }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[]> { // TODO: send the message _after_ fetching the heroes this.messageService.add('HeroService: fetched heroes'); return of(HEROES); } getHero(id: number): Observable<Hero> { // TODO: send the message _after_ fetching the hero this.messageService.add(`HeroService: fetched hero id=${id}`); return of(HEROES.find(hero => hero.id === id)); } }
      
      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 {}
    

AppComponent

<h1>{{title}}</h1> <nav> <a routerLink="/dashboard">Dashboard</a> <a routerLink="/heroes">Heroes</a> </nav> <router-outlet></router-outlet> <app-messages></app-messages>/* AppComponent's private CSS styles */ h1 { font-size: 1.2em; margin-bottom: 0; } h2 { font-size: 2em; margin-top: 0; padding-top: 0; } nav a { padding: 5px 10px; text-decoration: none; margin-top: 10px; display: inline-block; background-color: #eee; border-radius: 4px; } nav a:visited, a:link { color: #334953; } nav a:hover { color: #039be5; background-color: #cfd8dc; } nav a.active { color: #039be5; }
      
      <h1>{{title}}</h1>
<nav>
  <a routerLink="/dashboard">Dashboard</a>
  <a routerLink="/heroes">Heroes</a>
</nav>
<router-outlet></router-outlet>
<app-messages></app-messages>
    

DashboardComponent

<h3>Top Heroes</h3> <div class="grid grid-pad"> <a *ngFor="let hero of heroes" class="col-1-4" routerLink="/detail/{{hero.id}}"> <div class="module hero"> <h4>{{hero.name}}</h4> </div> </a> </div>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() { this.getHeroes(); } getHeroes(): void { this.heroService.getHeroes() .subscribe(heroes => this.heroes = heroes.slice(1, 5)); } }/* DashboardComponent's private CSS styles */ [class*='col-'] { float: left; padding-right: 20px; padding-bottom: 20px; } [class*='col-']:last-of-type { padding-right: 0; } a { text-decoration: none; } *, *:after, *:before { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; } h3 { text-align: center; margin-bottom: 0; } h4 { position: relative; } .grid { margin: 0; } .col-1-4 { width: 25%; } .module { padding: 20px; text-align: center; color: #eee; max-height: 120px; min-width: 120px; background-color: #3f525c; border-radius: 2px; } .module:hover { background-color: #eee; cursor: pointer; color: #607d8b; } .grid-pad { padding: 10px 0; } .grid-pad > [class*='col-']:last-of-type { padding-right: 20px; } @media (max-width: 600px) { .module { font-size: 10px; max-height: 75px; } } @media (max-width: 1024px) { .grid { margin: 0; } .module { min-width: 60px; } }
      
      <h3>Top Heroes</h3>
<div class="grid grid-pad">
  <a *ngFor="let hero of heroes" class="col-1-4"
      routerLink="/detail/{{hero.id}}">
    <div class="module hero">
      <h4>{{hero.name}}</h4>
    </div>
  </a>
</div>
    

HeroesComponent

<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>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() { this.getHeroes(); } getHeroes(): void { this.heroService.getHeroes() .subscribe(heroes => this.heroes = heroes); } }/* 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; 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: #333; text-decoration: none; position: relative; display: block; width: 250px; } .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:#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; }
      
      <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>
    

HeroDetailComponent

<div *ngIf="hero"> <h2>{{hero.name | uppercase}} Details</h2> <div><span>id: </span>{{hero.id}}</div> <div> <label>name: <input [(ngModel)]="hero.name" placeholder="name"/> </label> </div> <button (click)="goBack()">go back</button> </div>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; constructor( private route: ActivatedRoute, private heroService: HeroService, private location: Location ) {} ngOnInit(): void { this.getHero(); } getHero(): void { const id = +this.route.snapshot.paramMap.get('id'); this.heroService.getHero(id) .subscribe(hero => this.hero = hero); } goBack(): void { this.location.back(); } }/* HeroDetailComponent's private CSS styles */ label { display: inline-block; width: 3em; margin: .5em 0; color: #607D8B; font-weight: bold; } input { height: 2em; font-size: 1em; padding-left: .4em; } button { margin-top: 20px; font-family: Arial; background-color: #eee; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; cursor: hand; } button:hover { background-color: #cfd8dc; } button:disabled { background-color: #eee; color: #ccc; cursor: auto; }
      
      <div *ngIf="hero">
  <h2>{{hero.name | uppercase}} Details</h2>
  <div><span>id: </span>{{hero.id}}</div>
  <div>
    <label>name:
      <input [(ngModel)]="hero.name" placeholder="name"/>
    </label>
  </div>
  <button (click)="goBack()">go back</button>
</div>
    

小结

Summary

  • 添加了 Angular 路由器在各个不同组件之间导航。

    You added the Angular router to navigate among different components.

  • 你使用一些 <a> 链接和一个 <router-outlet>AppComponent 转换成了一个导航用的壳组件。

    You turned the AppComponent into a navigation shell with <a> links and a <router-outlet>.

  • 你在 AppRoutingModule 中配置了路由器。

    You configured the router in an AppRoutingModule

  • 你定义了一些简单路由、一个重定向路由和一个参数化路由。

    You defined simple routes, a redirect route, and a parameterized route.

  • 你在 <a> 元素中使用了 routerLink 指令。

    You used the routerLink directive in anchor elements.

  • 你把一个紧耦合的主从视图重构成了带路由的详情视图。

    You refactored a tightly-coupled master/detail view into a routed detail view.

  • 你使用路由链接参数来导航到所选英雄的详情视图。

    You used router link parameters to navigate to the detail view of a user-selected hero.

  • 在多个组件之间共享了 HeroService 服务。

    You shared the HeroService among multiple components.