有一些《英雄之旅》的新需求:
完成时,用户就能像这样在应用中导航:
在 Angular 中,最好在一个独立的顶层模块中加载和配置路由器,它专注于路由功能,然后由根模块 AppModule
导入它。
按照惯例,这个模块类的名字叫做 AppRoutingModule
,并且位于 src/app
下的 app-routing.module.ts
文件中。
使用 CLI 生成它。
ng generate module app-routing --flat --module=app
参数 | 详情 |
---|---|
| 把这个文件放进了 |
| 告诉 CLI 把它注册到 |
生成的文件是这样的:
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
具有两个属性:
属性 | 详情 |
---|---|
| 用来匹配浏览器地址栏中 URL 的字符串。 |
| 导航到该路由时,路由器应该创建的组件。 |
这会告诉路由器把该 URL 与 path:'heroes'
匹配。如果网址类似于 localhost:4200/heroes
就显示 HeroesComponent
。
@NgModule
元数据会初始化路由器,并开始监听浏览器地址的变化。
下面的代码行将 RouterModule
添加到 AppRoutingModule
的 imports
数组中,同时通过调用 RouterModule.forRoot()
来用这些 routes
配置它:
imports: [ RouterModule.forRoot(routes) ],
这个方法之所以叫 forRoot()
,是因为你要在应用的顶层配置这个路由器。forRoot()
方法会提供路由所需的服务提供者和指令,还会基于浏览器的当前 URL 执行首次导航。
接下来,AppRoutingModule
导出 RouterModule
,以便它在整个应用程序中生效。
exports: [ RouterModule ]
打开 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
。浏览器就会刷新,并且显示本应用的标题,而不显示英雄列表。
理想情况下,用户应该能通过点击链接进行导航,而不用被迫把路由的 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
中。
把这三个文件中的内容改成这样:
<h2>Top Heroes</h2>src/app/dashboard/dashboard.component.ts
<div class="heroes-menu">
<a *ngFor="let hero of heroes">
{{hero.name}}
</a>
</div>
import { Component, OnInit } from '@angular/core';src/app/dashboard/dashboard.component.css
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));
}
}
/* 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
数组属性。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
的底部看到。
用户应该能通过三种途径看到这些详情。
在这一节,你将能导航到 HeroDetailComponent
,并把它从 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
中的英雄连接还没有反应。
路由器已经有一个指向 HeroDetailComponent
的路由了,修改仪表盘中的英雄连接,让它们通过参数化的英雄详情路由进行导航。
<a *ngFor="let hero of heroes"
routerLink="/detail/{{hero.id}}">
{{hero.name}}
</a>
你正在 *ngFor
复写器中使用 Angular 的插值绑定来把当前迭代的 hero.id
插入到每个 routerLink
中。
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);
}
}
以前,父组件 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 的服务,用来与浏览器打交道。 稍后,你就会使用它来导航回上一个视图。
在 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
,并在 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
里之后,其细节看起来会更好,如下面的“查看最终代码”标签页中所示。
下面是本页所提到的源代码。
import { NgModule } from '@angular/core';src/app/app-routing.module.ts
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 { NgModule } from '@angular/core';src/app/hero.service.ts
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 { 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);
}
}
<h1>{{title}}</h1>src/app/app.component.ts
<nav>
<a routerLink="/dashboard">Dashboard</a>
<a routerLink="/heroes">Heroes</a>
</nav>
<router-outlet></router-outlet>
<app-messages></app-messages>
import { Component } from '@angular/core';src/app/app.component.css
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
title = 'Tour of Heroes';
}
/* 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;
}
<h2>Top Heroes</h2>src/app/dashboard/dashboard.component.ts
<div class="heroes-menu">
<a *ngFor="let hero of heroes"
routerLink="/detail/{{hero.id}}">
{{hero.name}}
</a>
</div>
import { Component, OnInit } from '@angular/core';src/app/dashboard/dashboard.component.css
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));
}
}
/* 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;
}
<h2>My Heroes</h2>src/app/heroes/heroes.component.ts
<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';src/app/heroes/heroes.component.css
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);
}
}
/* 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;
}
<div *ngIf="hero">src/app/hero-detail/hero-detail.component.ts
<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>
import { Component, OnInit } from '@angular/core';src/app/hero-detail/hero-detail.component.css
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();
}
}
/* 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;
}
<a>
链接和一个 <router-outlet>
把 AppComponent
转换成了一个导航用的壳组件AppRoutingModule
中配置了路由器<a>
元素中使用了 routerLink
指令HeroService
服务