完整项目地址:https://github.com/lixinsong123/heroGuide
行动计划
- 把
AppComponent
变成应用程序的“壳”,它只‘处理导航‘ - 把现在由
AppComponent
关注的英雄们移到一个独立的GeneralComponent
中 - 添加路由
- 创建一个新的
DashboardComponent
组件 - 把仪表盘加入导航结构中
路由是导航的另一个名字。路由器就是从一个视图导航到另一个视图的机制。
拆分Appcomponent.
现在的应用会加载AppComponent
组件,并且立刻显示出英雄列表。
我们修改后的应用将提供一个壳,它会选择仪表盘和英雄列表视图之一,然后默认显示它。
AppComponent
组件应该只处理导航。 我们来把英雄列表的显示职责,从AppComponent
移到GeneralComponent
组件中。
GeneralComponent组件
AppComponent
的职责已经被移交给GeneralComponent
了。 与其把AppComponent
中所有的东西都搬过去,不如索性把它改名为GeneralComponent
,然后单独创建一个新的AppComponent
壳。
步骤:
1.把app.component.ts 和app.component.html 和app.component.scss移入到General文件夹下。
2.app改为generals
3.类名AppComponent改为GeneralsComponent
4.选择器名称app-root改为my-general
创建AppComponent
新的AppComponent
将成为应用的“壳”。 它将在顶部放一些导航链接,并且把我们要导航到的页面放在下面的显示区中。
在./app下创建app.component.ts
- 添加支持性的
import
语句。 - 定义一个导出的
AppComponent
类。 - 在类的上方添加
@Component
元数据装饰器,装饰器带有app-root选择器。 - 将下面的项目从
HeroesComponent
移到AppComponent
: title
类属性@Component
模板中的<h1>
标签,它包含了对title
属性的绑定。- 在模板的标题下面添加
<my-heroes>
标签,以便我们仍能看到英雄列表。 - 添加
组件到根模块的GeneralComponent
declarations
数组中,以便 Angular 能认识<my-generals>
标签。 - 添加
GeneralService
到AppModule
的providers
数组中,因为我们的每一个视图都需要它。 - 从
的GeneralComponent
providers
数组中移除GeneralService
,因为它被提到模块了。 - 为
AppComponent
添加一些import
语句。
./app/app.component.ts
import { Component } from ‘@angular/core‘;
@Component({
selector: ‘app-root‘,
template: `
<my-generals></my-generals>
`
})
export class AppComponent {
}
./app/app.module.ts
import { NgModule } from ‘@angular/core‘;
import { BrowserModule } from ‘@angular/platform-browser‘;
import { FormsModule } from ‘@angular/forms‘;
import {GeneralService} from‘./general.service‘;
//注册指令--->用法类似于组件
import {HighlightDirective} from ‘../directive/drag.directive‘;
//测试管道所用的组件
import {HeroBirthdayComponent} from "../Pipes/hero-birthday1.component";
import {ExponentialStrengthPipe} from "../Pipes/exponential-strength.pipe"
import {GeneralsComponent} from ‘./general/generals.component‘;
import { AppComponent } from ‘./app.component‘;
import {GeneralDetailComponent} from ‘./general/general-detail.component‘;
@NgModule({
//列出程序中的组件
declarations: [
AppComponent,
GeneralDetailComponent,
GeneralsComponent,
// HighlightDirective,
// HeroBirthdayComponent,
// ExponentialStrengthPipe
],
//导入模块
imports: [
/*BrowserModule,每个运行在浏览器中的应用都必须导入它。
BrowserModule注册了一些关键的应用“服务”提供商。 它还包括了一些通用的指令,
例如NgIf和NgFor,所以这些指令在该模块的任何组件模板中都是可用的。
*/
BrowserModule,
//双向数据绑定依赖的模块
FormsModule
],
providers: [GeneralService],
/*@NgModule.bootstrap属性把这个AppComponent标记为引导 (bootstrap) 组件。
当 Angular 引导应用时,它会在 DOM 中渲染AppComponent,并把结果放进index.html的<app-root>元素标记内部。
*/
bootstrap: [AppComponent]
})
export class AppModule { }
./app/general/generals.component.ts
import { Component,OnInit} from ‘@angular/core‘;
import {General} from "../../bean/General";
import {GeneralService} from‘../general.service‘;
@Component({
selector: ‘my-generals‘,
templateUrl: ‘./generals.component.html‘,
styleUrls: [‘./generals.component.scss‘]
})
export class GeneralsComponent implements OnInit {
title = ‘MY General‘;
// generals:General[]=Generals;
color="pink";
constructor(private generalService:GeneralService ){
}
selectGeneral:General;
generals:General[];
getGenerals():void{
this.generalService.getGenerals()
.then(generals=>this.generals=generals);
}
ngOnInit(){
this.getGenerals();
}
oSelect(item:General):void{
this.selectGeneral=item;
}
}
添加路由
我们希望在用户点击按钮之后才显示英雄列表,而不是自动显示。 换句话说,我们希望用户能“导航”到英雄列表。
我们要使用 Angular 路由器进行导航。
Angular 路由器是一个可选的外部 Angular NgModule,名叫RouterModule
。 路由器包含了多种服务(RouterModule
)、多种指令(RouterOutlet、RouterLink、RouterLinkActive
)、 和一套配置(Routes
)。我们将先配置路由。
<base href> 组件
打开src/index.html
确保它的<head>
区顶部有一个<base href="...">
元素(或动态生成该元素的脚本)
配置路由 ./app/app.module.ts
a.导入路由所需要的模块
import { RouterModule } from ‘@angular/router‘;
b.在imports下写入
RouterModule.forRoot([
{
path: ‘generals‘,
component: GeneralsComponent
}
])
这个Routes
是一个路由定义的数组。 此时,我们只有一个路由定义,但别急,后面还会添加更多。
路由定义包括以下部分:
- Path: 路由器会用它来匹配浏览器地址栏中的地址,如
generals
。 - Component: 导航到此路由时,路由器需要创建的组件(
GeneralsComponent
)。
这里使用了forRoot()
方法,因为我们是在应用根部提供配置好的路由器。 forRoot()
方法提供了路由需要的“”路由服务提供商和指令”,并基于当前浏览器 URL 初始化导航。
路由出口(Outlet)
如果我们把路径/generals
粘贴到浏览器的地址栏,路由器会匹配到‘Generals‘
路由,并显示GeneralsComponent
组件。 我们必须告诉路由器它位置,所以我们把<router-outlet>
标签添加到模板的底部。 RouterOutlet
是由RouterModule
提供的指令之一。 当我们在应用中导航时,路由器就把激活的组件显示在<router-outlet>
里面。
路由器链接
我们当然不会真让用户往地址栏中粘贴路由的 URL, 而应该在模板中的什么地方添加一个锚标签。点击时,就会导航到GeneralsComponent
组件。
<a routerLink="/generals">Generals</a>
注意,锚标签中的[routerLink]
绑定。 我们把RouterLink
指令(ROUTER_DIRECTIVES
中的另一个指令)绑定到一个字符串。 它将告诉路由器,当用户点击这个链接时,应该导航到哪里。
由于这个链接不是动态的,我们只要用一次性绑定的方式绑定到路由的路径 (path) 就行了。 回来看路由配置表,我们清楚的看到,这个路径 —— ‘/heroes‘
就是指向HeroesComponent
的那个路由的路径。
现在./app/app.component.ts
import { Component } from ‘@angular/core‘;
@Component({
selector: ‘app-root‘,
template: `
<a routerLink="/generals">Generals</a>
<router-outlet></router-outlet>
`
})
export class AppComponent {
}
AppComponent现在加上了路由器,并能显示路由到的视图了。 因此,为了把它从其它种类的组件中区分出来,我们称这类组件为路由器组件。
添加一个仪表盘
当我们有多个视图的时候,路由才有意义。所以我们需要另一个视图。先创建一个DashboardComponent
的占位符,让用户可以导航到它或从它导航出来。
在./src/app下创建dashboard文件夹
在这下面创建dashboard.component.ts
import { Component } from ‘@angular/core‘;
@Component({
selector: ‘my-dashboard‘,
template: ‘<h3>My Dashboard</h3>‘
})
export class DashboardComponent { }
我们先不实现他
配置仪表盘路由
要让app.module.ts
能导航到仪表盘,就要先导入仪表盘组件,然后把下列路由定义添加到Routes
数组中。
{
path: ‘dashboard‘,
component: DashboardComponent
},
然后还得把DashboardComponent
添加到AppModule
的declarations
数组中。
DashboardComponent
添加重定向路由
浏览器启动时地址栏中的地址是/
。 当应用启动时,它应该显示仪表盘,并且在浏览器的地址栏显示URL:/dashboard,把下列路由定义添加到
Routes
数组中。
{
path: ‘‘,
redirectTo: ‘/dashboard‘,
pathMatch: ‘full‘
},
添加导航到模版中
在模板上添加一个到仪表盘的导航链接,就放在Generals(英雄列表)链接的上方。
<a routerLink="/generals">Generals</a>
<a routerLink="/dashboard">Dashboard</a>
<router-outlet></router-outlet>
刷新浏览器。应用显示出了仪表盘,并可以在仪表盘和英雄列表之间导航了。
把英雄添加到仪表盘
让仪表盘变得有趣。
把元数据中的template
属性替换为templateUrl
属性,它将指向一个新的模板文件。
templateUrl: ‘./dashboard.component.html‘
增加styleUrls:[‘./dashboard.component.scss‘]
dashboard.component.htm
<h3>Top Generals</h3>
<div>
<div *ngFor="let general of generals" class="col-1-4">
<div>
<h4>{{general.name}}</h4>
</div>
</div>
</div>
我们再次使用*ngFor
来在英雄列表上迭代,并显示它们的名字。 还添加了一个额外的<div>
元素,来帮助稍后的美化工作。
dashboard.component.ts
import { Component,OnInit} from ‘@angular/core‘;
import {General} from "../../bean/General";
import {GeneralService} from‘../general.service‘;
@Component({
selector: ‘my-dashboard‘,
templateUrl: ‘./dashboard.component.html‘,
styleUrls:[‘./dashboard.component.scss‘]
})
export class DashboardComponent implements OnInit {
generals:General[];
constructor(private generalService:GeneralService){}
ngOnInit(){
this.generalService.getGenerals().then(generals=>this.generals=generals.slice(1,5));
}
}
我们在之前的GeneralsComponent
中也看到过类似的逻辑:
- 创建一个
generals
数组属性。 - 在构造函数中注入
GeneralService
,并且把它保存在一个私有的generalService
字段中。 - 在 Angular 的
ngOnInit
生命周期钩子里面调用服务来获得英雄数据。
在仪表盘中我们用Array.slice
方法提取了四个英雄(第2、3、4、5个)。
刷新浏览器,在这个新的仪表盘中就看到了四个英雄。
导航到英雄详情
虽然我们在HeroesComponent
组件的底部显示了所选英雄的详情, 但用户还没法导航到GeneralDetailComponent
组件。我们可以用下列方式导航到GeneralDetailComponent
:
- 从Dashboard(仪表盘)导航到一个选定的英雄。
- 从Generals(英雄列表)导航到一个选定的英雄。
- 把一个指向该英雄的“深链接” URL 粘贴到浏览器的地址栏。
路由到一个英雄详情
我们将在app.module.ts
中添加一个到GeneralDetailComponent
的路由,也就是配置其它路由的地方。
这个新路由的不寻常之处在于,我们必须告诉GeneralDetailComponent
该显示哪个英雄。 之前,我们不需要告诉GeneralsComponent
组件和DashboardComponent
组件任何东西
参数化路由
我们可以把英雄的id
添加到 URL 中。当导航到一个id
为 11 的英雄时,我们期望的 URL 是这样的:
/detail/11
URL中的/detail/
部分是固定不变的,但结尾的数字id
部分会随着英雄的不同而变化。 我们要把路由中可变的那部分表示成一个参数 (parameter) 或令牌 (token) ,代表英雄的id
。
配置带参数的路由
{
path: ‘detail/:id‘,
component: GeneralDetailComponent
},
路径中的冒号 (:) 表示:id
是一个占位符,当导航到这个GeneralDetailComponent
组件时,它将被填入一个特定英雄的id
。
修改GeneralDetailComponent
模板不用修改,我们会用原来的方式显示英雄。导致这次大修的原因是如何获得这个英雄的数据。
我们不会再从父组件的属性绑定中接收英雄数据。 新的GeneralDetailComponent 应该从ActivatedRoute
服务的可观察对象params
中取得id
参数, 并通过GeneralService
服务获取具有这个指定id
的英雄数据。
先添加下列导入语句:
import { Component, Input, OnInit } from ‘@angular/core‘;
import { ActivatedRoute, ParamMap } from ‘@angular/router‘;
import { Location } from ‘@angular/common‘;
import ‘rxjs/add/operator/switchMap‘;
import {GeneralService} from‘../general.service‘;
然后注入ActivatedRoute
和HeroService
服务到构造函数中,将它们的值保存到私有变量中:
constructor( private generalService: GeneralService,
private route: ActivatedRoute,
private location: Location){}
在ngOnInit()
生命周期钩子中,我们从ActivatedRoute
服务的可观察对象params
中提取id
参数, 并且使用GeneralService
来获取具有这个id
的英雄数据。
ngOnInit(){
this.route.paramMap
.switchMap((params: ParamMap) => this.generalService.getGeneral(+params.get(‘id‘)))
.subscribe(general => this.general = general);
}
添加 GeneralService.getGeneral()
在前面的代码片段中GeneralService没有getGeneral()
方法。要解决这个问题,请打开GeneralService
并添加一个
方法,它会根据getGenera
()id
从
中过滤英雄列表。getGenerals
()
etGeneral(id: number): Promise<General> {
return this.getGenerals()
.then(generals => generals.find(general => general.id === id));
}
回到原路
一个goBack()
方法,它使用之前注入的Location
服务, 利用浏览器的历史堆栈,导航到上一步。
goBack(): void {
this.location.back();
}
然后,我们通过一个事件绑定把此方法绑定到模板底部的 Back(后退)按钮上。
<button (click)="goBack()">Back</button>
选择一个仪表盘中的英雄
当用户从仪表盘中选择了一位英雄时,本应用要导航到GeneralDetailComponent
以查看和编辑所选的英雄。
虽然仪表盘英雄被显示为像按钮一样的方块,但是它们的行为应该像锚标签一样。 当鼠标移动到一个英雄方块上时,目标 URL 应该显示在浏览器的状态条上,用户应该能拷贝链接或者在新的浏览器标签页中打开英雄详情视图。
要达到这个效果,再次打开dashboard.component.html
,将用来迭代的<div *ngFor...>
替换为<a>
,就像这样:
刷新浏览器,并从仪表盘中选择一位英雄,应用就会直接导航到英雄的详情。
重构路由为一个模块
AppModule
中有将近 20 行代码是用来配置四个路由的。 绝大多数应用有更多路由,并且它们还有守卫服务来保护不希望或未授权的导航。 路由的配置可能迅速占领这个模块,并掩盖其主要目的,即为 Angular 编译器设置整个应用的关键配置。
我们应该重构路由配置到它自己的类。 什么样的类呢? 当前的RouterModule.forRoot()
产生一个Angular ModuleWithProviders
,所以这个路由类应该是一种模块类。 它应该是一个路由模块
按约定,路由模块的名字应该包含 “Routing”,并与导航到的组件所在的模块的名称看齐。
在app.module.ts
所在目录创建app-routing.module.ts
文件。将下面从AppModule
类提取出来的代码拷贝进去:
import { NgModule } from ‘@angular/core‘;
import { RouterModule, Routes } from ‘@angular/router‘;
import {GeneralsComponent} from ‘./general/generals.component‘;
import {DashboardComponent} from ‘./dashboard/dashboard.component‘;
import {GeneralDetailComponent} from ‘./general/general-detail.component‘;
const routes: Routes = [
{ path: ‘‘, redirectTo: ‘/dashboard‘, pathMatch: ‘full‘ },
{ path: ‘dashboard‘, component: DashboardComponent },
{ path: ‘detail/:id‘, component: GeneralDetailComponent },
{ path: ‘generals‘, component: GeneralsComponent }
];
@NgModule({
imports: [ RouterModule.forRoot(routes) ],
exports: [ RouterModule ]
})
export class AppRoutingModule {}
- 将路由抽出到一个变量中。如果你将来要导出这个模块,这种 "路由模块" 的模式也会更加明确。
- 添加
RouterModule.forRoot(routes)
到imports
。 - 把
RouterModule
添加到路由模块的exports
中,以便关联模块(比如AppModule
)中的组件可以访问路由模块中的声明,比如RouterLink
和RouterOutlet
。 - 无
declarations
!声明是关联模块的任务。 - 如果有守卫服务,把它们添加到本模块的
providers
中(本例子中没有守卫服务)。
修改 AppModule
删除AppModule
中的路由配置,并导入AppRoutingModule
(使用 ES import
语句导入,并将它添加到NgModule.imports
列表)。
import { NgModule } from ‘@angular/core‘;
import { BrowserModule } from ‘@angular/platform-browser‘;
import { FormsModule } from ‘@angular/forms‘;
import {GeneralService} from‘./general.service‘;
//路由所需要的核心模块
import { RouterModule } from ‘@angular/router‘;
//路由模块
import {AppRoutingModule} from ‘./app-routing.module‘
//注册指令--->用法类似于组件
import {HighlightDirective} from ‘../directive/drag.directive‘;
//测试管道所用的组件
import {HeroBirthdayComponent} from "../Pipes/hero-birthday1.component";
import {ExponentialStrengthPipe} from "../Pipes/exponential-strength.pipe"
import {GeneralsComponent} from ‘./general/generals.component‘;
import {DashboardComponent} from ‘./dashboard/dashboard.component‘;
import { AppComponent } from ‘./app.component‘;
import {GeneralDetailComponent} from ‘./general/general-detail.component‘;
@NgModule({
//列出程序中的组件
declarations: [
AppComponent,
GeneralDetailComponent,
GeneralsComponent,
DashboardComponent
// HighlightDirective,
// HeroBirthdayComponent,
// ExponentialStrengthPipe
],
//导入模块
imports: [
/*BrowserModule,每个运行在浏览器中的应用都必须导入它。
BrowserModule注册了一些关键的应用“服务”提供商。 它还包括了一些通用的指令,
例如NgIf和NgFor,所以这些指令在该模块的任何组件模板中都是可用的。
*/
BrowserModule,
//双向数据绑定依赖的模块
FormsModule,
//路由模块
AppRoutingModule
],
providers: [GeneralService],
/*@NgModule.bootstrap属性把这个AppComponent标记为引导 (bootstrap) 组件。
当 Angular 引导应用时,它会在 DOM 中渲染AppComponent,并把结果放进index.html的<app-root>元素标记内部。
*/
bootstrap: [AppComponent]
})
export class AppModule { }
在 GeneralsComponent中选择一位英雄
添加 mini 版英雄详情
在<div class="controller"> 添加如下代码
<div class="row">
<div *ngIf="selectGeneral">
<h2>
{{selectGeneral.name | uppercase}} is my hero
</h2>
<button (click)="gotoDetail()">View Details</button>
</div>
</div>
更新 GeneralsComponent类
点击按钮时,GeneralsComponent导航到GeneralDetailComponent
。 该按钮的点击事件绑定到了gotoDetail()
方法,它使用命令式的导航,告诉路由器去哪儿。
该方法需要对组件类做一些修改:
具体代码
import { Component,OnInit} from ‘@angular/core‘;
import { Router } from ‘@angular/router‘;
import {General} from "../../bean/General";
import {GeneralService} from‘../general.service‘;
@Component({
selector: ‘my-generals‘,
templateUrl: ‘./generals.component.html‘,
styleUrls: [‘./generals.component.scss‘]
})
export class GeneralsComponent implements OnInit {
title = ‘MY General‘;
// generals:General[]=Generals;
color="pink";
constructor(private generalService:GeneralService,private router: Router){
}
selectGeneral:General;
generals:General[];
getGenerals():void{
this.generalService.getGenerals()
.then(generals=>this.generals=generals);
}
ngOnInit(){
this.getGenerals();
}
oSelect(item:General):void{
this.selectGeneral=item;
}
gotoDetail(): void {
this.router.navigate([‘/detail‘, this.selectGeneral.id]);
}
}
刷新浏览器,并开始点击。 我们能在应用中导航:从仪表盘到英雄详情再回来,从英雄列表到 mini 版英雄详情到英雄详情,再回到英雄列表。 我们可以在仪表盘和英雄列表之间跳来跳去。
美化项目