[Angular2] Case Study:Tour of Heroes - 路由

接着[Angular2] Case Study:Tour of Heroes - 多组件、服务继续,添加一个视图,实现在多个视图之间的路由。

我们的目标是:
1.将AppComponent改成只处理页面导航
2.将AppComponent中的关于heroes的逻辑移到HeroesComponent组件中
3.添加路由
4.添加DashboardComponent组件
5.将Dashboard添加到导航结构中

拆分AppComponent

直接将AppComponent重命名为HeroesComponent,再创建一个新的AppComponent

  1. app.component.ts文件重命名为heroes.component.ts
  2. AppComponent重命名为HeroesComponent
  3. 将选择器由my-app改成my-heroes

创建AppComponent

新的AppComponent相当于应用的shell,它的顶部有几个导航链接,下面是一块显示区域,用于显示导航到的页面。

import { Component }       from 'angular2/core';
import { HeroService }     from './hero.service';
import { HeroesComponent } from './heroes.component';
@Component({
  selector: 'my-app',
  template: `
 <h1>{{title}}</h1>
 <my-heroes></my-heroes>
 `,
  directives: [HeroesComponent],
  providers: [
    HeroService
  ]
})
export class AppComponent {
  title = 'Tour of Heroes';
}

注意,这里的providers数组中包含了HeroService,因此需要将HeroesComponentproviders中的HeroService去掉。

添加路由

index.html中添加路由模块:

<script src="node_modules/angular2/bundles/router.dev.js"></script>

同时,在index.html<head>内的顶部添加<base href="/">

<head>
  <base href="/">

路由组件是一个服务,因此需要引入它,并且将它添加到providers数组中。在app.component.ts中添加:

...
import { RouteConfig, ROUTER_DIRECTIVES, ROUTER_PROVIDERS } from 'angular2/router';
...
  directives: [ROUTER_DIRECTIVES],
  providers: [
    ROUTER_PROVIDERS,
    HeroService
  ]
...

使用@RouteConfig装饰器来为AppComponent添加路由并配置:

@RouteConfig([
  {
    path: '/heroes',
    name: 'Heroes',
    component: HeroesComponent
  }
])

@RouteConfig接收测参数是路由定义数组。一个路由定义包含三部分:

  • path:该路由匹配的URL路径。
  • name:路由的名称。为了避免和path混淆,路由名称的首字母必须大写。
  • component:导航到该路由时需要创建的组件。

路由出口(Router Outlet)

如果在浏览器中访问/heroes,这个路径是与路由Heroes匹配的,应该显示HeroesComponent组件,但是在哪里显示呢?

在模板的下方添加<router-outlet>标记。RouterOutlet是一个ROUTER_DIRECTIVES指令。路由会将导航到的组件显示在<router-outlet>的下方。

添加一个链接标签,点击它触发导航到相应的组件:

// app.component.js

template: `
 <h1>{{title}}</h1>
 <a [routerLink]="['Heroes']">Heroes</a>
 <router-outlet></router-outlet>
`,

RouterLink也是一个ROUTER_DIRECTIVES指令。

现在,在浏览器中可以看到,页面打开是不显示heroes列表了,点击Heroes链接后才显示。

到这里,完整的app.component.ts文件如下:

import { Component } from 'angular2/core';
import { RouteConfig, ROUTER_DIRECTIVES, ROUTER_PROVIDERS } from 'angular2/router';
import { HeroService }     from './hero.service';
import { HeroesComponent } from './heroes.component';
@Component({
  selector: 'my-app',
  template: `
 <h1>{{title}}</h1>
 <a [routerLink]="['Heroes']">Heroes</a>
 <router-outlet></router-outlet>
 `,
  directives: [ROUTER_DIRECTIVES],
  providers: [
    ROUTER_PROVIDERS,
    HeroService
  ]
})
@RouteConfig([
  {
    path: '/heroes',
    name: 'Heroes',
    component: HeroesComponent
  }
])
export class AppComponent {
  title = 'Tour of Heroes';
}

添加新视图

路由只有在有多个视图时才有意义。添加一个新的视图:

import { Component } from 'angular2/core';

@Component({
  selector: 'my-dashboard',
  template: '<h3>My Dashboard</h3>'
})
export class DashboardComponent { }

AppComponent中添加路由配置:

{
  path: '/dashboard',
  name: 'Dashboard',
  component: DashboardComponent,
  useAsDefault: true
},

PS:useAsDefault属性之指定为true,那么页面导航到/后会默认导航到该路由。

在模板中添加Dashboard的导航链接,:

template: `
  <h1>{{title}}</h1>
  <nav>
    <a [routerLink]="['Dashboard']">Dashboard</a>
    <a [routerLink]="['Heroes']">Heroes</a>
  </nav>
  <router-outlet></router-outlet>
`,

PS:这里还添加了<nav>标签,方便后面添加样式。

为Dashboard添加内容

在Dashboard中显示最前面的四个hero。这里我们将模板放到单独的文件中,创建文件ashboard.component.html

<h3>Top Heroes</h3>
<div class="grid grid-pad">
    <div *ngFor="#hero of heroes" (click)="gotoDetail(hero)" class="col-1-4" >
        <div class="module hero">
            <h4>{{hero.name}}</h4>
        </div>
    </div>
</div>

将元数据中的template改成templateUrl指向dashboard.component.html:

templateUrl:'app/dashboard.component.html'

这时,gotoDetail方法和heroes属性都还没有定义,接下来完善它。

共享HeroService

之前,我们将HeroServiceHeroesComponentproviders中移到了AppComponent中,这样应用会创建一个单例的HeroService,对所有组件都是可用的,在DashboardComponent中直接注入就可以了。

修改DashboardComponent,引入所需的组件:

import { Component, OnInit } from 'angular2/core';

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

添加的实现:

export class DashboardComponent implements OnInit {
  heroes: Hero[] = [];
  constructor(private _heroService: HeroService) { }
  ngOnInit() {
    this._heroService.getHeroes()
      .then(heroes => this.heroes = heroes.slice(1,5));
  }
  gotoDetail(){ /* not implemented yet */}
}

带参数的路由

接下来,添加一个路由,导航到HeroDetailComponent来显示Hero Detail。继续在AppComponent中添加路由配置。

但是,这里有点不同,我们必须告诉HeroDetailComponent显示哪一个hero。

可以在URL中指定hero的id。例如/detail/11。路由定义为:

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

注意这时AppComponent中要添加HeroDetailComponent的引用:

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

改造HeroDetailComponent

目前HeroDetailComponent是这样的:

import {Component} from 'angular2/core';
import {Hero} from './hero';
@Component({
  selector: 'my-hero-detail',
  template: `
 <div *ngIf="hero">
 <h2>{{hero.name}} details</h2>
 <div>
 <label>id: </label>{{hero.id}}
 </div>
 <div>
 <label>name: </label>
 <input [(ngModel)]="hero.name" placeholder="name"/>
 </div>
 </div>
 `,
  inputs: ['hero']
})
export class HeroDetailComponent {
  hero: Hero;
}

现在不再需要从父组件绑定hero属性来接收参数了,新的HeroDetailComponent应该从路由中获取参数id,然后使用HeroService来获取数据。

添加相关的引用:

import {RouteParams} from 'angular2/router';
import { HeroService } from './hero.service';
import { Component, OnInit } from 'angular2/core';

在构造函数中注入RouteParamsHeroService

constructor(
  private _heroService: HeroService,
  private _routeParams: RouteParams) {
}

实现ngOnInit,从RouteParams中获取参数id的值,然后使用HeroService来获取该id的hero:

ngOnInit() {
  let id = +this._routeParams.get('id');
  this._heroService.getHero(id)
    .then(hero => this.hero = hero);
}

使用RouteParams.get方法来获取路由中的参数。路由参数始终是字符串。这里使用+将它转换成了数字。

HeroService中添加getHero方法:

javascript getHero(id: number) { return Promise.resolve(HEROES).then( heroes => heroes.filter(hero => hero.id === id)[0] ); }

添加导航回退

当导航到HeroDetailComponent后,如何再导航到其它页面呢?

用户可以点击AppComponent的导航链接,或者使用浏览器的回退按钮。还可以在HeroDetailComponent中添加一个回退按钮,顺便将模板移到单独的文件hero-detail.component.html中:

<div *ngIf="hero">
  <h2>{{hero.name}} details!</h2>
  <div>
    <label>id: </label>{{hero.id}}</div>
  <div>
    <label>name: </label>
    <input [(ngModel)]="hero.name" placeholder="name" />
  </div>
  <button (click)="goBack()">Back</button>
</div>

HeroDetailComponent中添加goBack方法:

goBack() {
  window.history.back();
}

现在完善一下DashboardComponent中的gotoDetail方法:

gotoDetail(hero: Hero) {
  let link = ['HeroDetail', { id: hero.id }];
  this._router.navigate(link);
}

DashboardComponent添加路由:

import { Router } from 'angular2/router';
constructor(
  private _router: Router,
  private _heroService: HeroService) {
}

现在在浏览器中点击dashboard中的hero,就会跳转到hero detail页面,再点击Back就能回到前一个页面。

重构HeroesComponent

接下来重构HeroesComponent,去掉<my-hero-detail>标签,替换为:

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

PS:这里使用了UpperCasePipe,将字符串处理成字母全部大写。

这时,整个heroes.component.ts文件内容显得有点太长了,将模板和样式分别移到单独的文件heroes.component.htmlheroes.component.css中,在元数据中使用templateUrlstyleUrls来分别引用它们。然后,引用router并在构造函数中注入,实现gotoDetail方法。最后heroes.component.ts为:

import {Component, OnInit} from 'angular2/core';
import { Router } from 'angular2/router';
import {Hero} from './hero';
import {HeroDetailComponent} from './hero-detail.component';
import {HeroService} from './hero.service';

@Component({
  selector: 'my-heroes',
  templateUrl: 'app/heroes.component.html',
  styleUrls:  ['app/heroes.component.css'],
  directives: [HeroDetailComponent]
})
export class HeroesComponent implements OnInit {
  heroes: Hero[];
  selectedHero: Hero;
  constructor(
    private _router: Router,
    private _heroService: HeroService) { }
  getHeroes() {
    this._heroService.getHeroes().then(heroes => this.heroes = heroes);
  }
  ngOnInit() {
    this.getHeroes();
  }
  onSelect(hero: Hero) { this.selectedHero = hero; }
  gotoDetail() {
    this._router.navigate(['HeroDetail', { id: this.selectedHero.id }]);
  }
}

添加样式

app文件夹下创建文件hero-detail.component.cssdashboard.component.cssapp.component.css,分别在各自组件的元数据中配置styleUrls

/* hero-detail.component.css */

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;
}
/* dashboard.component.css */

[class*='col-'] {
  float: left;
}
*, *:after, *:before {
    -webkit-box-sizing: border-box;
    -moz-box-sizing: border-box;
    box-sizing: border-box;
}
h3 {
  text-align: center; margin-bottom: 0;
}
[class*='col-'] {
  padding-right: 20px;
  padding-bottom: 20px;
}
[class*='col-']:last-of-type {
  padding-right: 0;
}
.grid {
  margin: 0;
}
.col-1-4 {
  width: 25%;
}
.module {
    padding: 20px;
    text-align: center;
    color: #eee;
    max-height: 120px;
    min-width: 120px;
    background-color: #607D8B;
    border-radius: 2px;
}
h4 {
  position: relative;
}
.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;
    }
}
/* app.component.css */

h1 {
  font-size: 1.2em;
  color: #999;
  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: #607D8B;
}
nav a:hover {
  color: #039be5;
  background-color: #CFD8DC;
}
nav a.router-link-active {
  color: #039be5;
}

在项目的根目录下添加创建styles.css

h2 {
  color: #444;
  font-family: Arial, Helvetica, sans-serif;
  font-weight: lighter;
}
body {
  margin: 2em;
}
body, input[text], button {
  color: #888;
  font-family: Cambria, Georgia;
}
button {
  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: #aaa;
  cursor: auto;
}
/* everywhere else */
* {
  font-family: Arial, Helvetica, sans-serif;
}

并在index.html中添加样式文件的引用:

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

最后效果如图:

[Angular2] Case Study:Tour of Heroes - 路由_第1张图片

源码

参考资料

Angular2官方文档

你可能感兴趣的:([Angular2] Case Study:Tour of Heroes - 路由)