首页 > 代码库 > 我们把主从结构的页面重构成多个组件

我们把主从结构的页面重构成多个组件

多个组件

我们的应用正在成长中。现在又有新的用例:重复使用组件,传递数据给组件并创建更多可复用。 我们来把英雄详情从英雄列表中分离出来,让这个英雄详情组件可以被复用。

首先老规矩,我们得让我们的代码运行起来:

 

让应用代码保持转译和运行

 

我们要启动 TypeScript 编译器,它会监视文件变更,并启动开发服务器。只要敲:

npm start

 

这个命令会在我们构建《英雄指南》的时候让应用得以持续运行。

制作英雄详情组件

目前,英雄列表和英雄详情位于同一个文件的同一个组件中。 它们现在还很小,但很快它们都会长大。 我们将来肯定会收到新需求:针对这一个,却不能影响另一个。 然而,每一次更改都会给这两个组件带来风险和双倍的测试负担,却没有任何好处。 如果我们需要在应用的其它地方复用英雄详情组件,英雄列表组件也会跟着混进去。

我们当前的组件违反了单一职责原则。 虽然这只是一个教程,但我们还是得坚持做正确的事 — 况且,做正确的事这么容易,在此过程中,我们又能学习如何构建 Angular 应用。

我们来把英雄详情拆分成一个独立的组件。

拆分英雄详情组件

app目录下添加一个名叫hero-detail.component.ts的文件,并且创建HeroDetailComponent。代码如下:

技术分享
import {Component,Input } from ‘@angular/core‘;@Component({ selector:"my-hero-detail",})export class HeroDetailComponent{}
View Code

命名约定

我们希望一眼就能看出哪些类是组件,哪些文件包含组件。

你会注意到,在名叫app.component.ts的文件中有一个AppComponent组件,在名叫hero-detail.component.ts的文件中有一个HeroDetailComponent组件。

我们的所有组件名都以Component结尾。所有组件的文件名都以.component结尾。

这里我们使用小写中线命名法 (也叫烤串命名法)拼写文件名, 所以不用担心它在服务器或者版本控制系统中出现大小写问题。

我们先从 Angular 中导入ComponentInput装饰器,因为马上就会用到它们。

我们使用@Component装饰器创建元数据。在元数据中,我们指定选择器的名字,用以标识此组件的元素。 然后,我们导出这个类,以便其它组件可以使用它

做完这些,我们把它导入AppComponent组件,并创建相应的<my-hero-detail>元素。

英雄详情模板

此时,AppComponent的英雄列表和英雄详情视图被组合进同一个模板中。 让我们从AppComponent剪切出英雄详情的内容,并且粘贴HeroDetailComponent组件的template属性中。

之前我们绑定了AppComponentselectedHero.name属性。 HeroDetailComponent组件将会有一个hero属性,而不是selectedHero属性。 所以,我们要把模板中的所有selectedHero替换为hero。只改这些就够了。 最终结果如下所示:

技术分享
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>`
View Code

现在,我们的英雄详情布局只存在于HeroDetailComponent组件中。

添加 HERO 属性

把刚刚所说的hero属性添加到组件类。

hero: Hero;

我们声明hero属性是Hero类型,但是我们的Hero类还在app.component.ts文件中。 我们有了两个组件,它们位于各自的文件,并且都需要引用Hero类。

要解决这个问题,我们从app.component.ts文件中把Hero类移到属于它自己的hero.ts文件中。

app/hero.ts

技术分享
export class Hero {  id: number;  name: string;}
View Code

我们从hero.ts中导出Hero类,因为我们要从两个组件文件中引用它。 在app.component.tshero-detail.component.ts的顶部添加下列 import 语句:

import {Hero} from  ‘./hero‘;

HERO属性是一个输入属性

还得告诉HeroDetailComponent显示哪个英雄。谁告诉它呢?自然是父组件AppComponent了!

AppComponent确实知道该显示哪个英雄:用户从列表中选中的那个。 用户选择的英雄在它的selectedHero属性中。

我们马上升级AppComponent的模板,把该组件的selectedHero属性绑定到HeroDetailComponent组件的hero属性上。 绑定看起来可能是这样的:

<my-hero-detail [hero]="selectedHero"></my-hero-detail>

注意,hero是属性绑定的目标 — 它位于等号 (=) 左边方括号中。

Angular 希望我们把目标属性声明为组件的输入属性,否则,Angular 会拒绝绑定,并抛出错误。

我们在这里详细解释了输入属性,以及为什么目标属性需要这样的特殊待遇,而源属性却不需要。

我们有几种方式把hero声明成输入属性。 这里我们采用首选的方式:使用我们前面导入的@Input装饰器向hero属性添加注解。

 @Input()  hero: Hero;

更新 AppModule

回到应用的根模块AppModule,让它使用HeroDetailComponent组件。

我们先导入HeroDetailComponent组件,后面好引用它。

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

接下来,添加HeroDetailComponentNgModule装饰器中的declarations数组。 这个数组包含了所有由我们创建的并属于应用模块的组件、管道和指令。

技术分享
@NgModule({  imports: [    BrowserModule,    FormsModule  ],  declarations: [    AppComponent,    HeroDetailComponent  ],  bootstrap: [ AppComponent ]})export class AppModule { }
View Code

更新 AppComponent

现在,应用知道了我们的HeroDetailComponent, 找到我们刚刚从模板中移除英雄详情的地方, 放上用来表示HeroDetailComponent组件的元素标签。

<my-hero-detail></my-hero-detail>

这两个组件目前还不能协同工作,直到我们把AppComponent组件的selectedHero 属性和HeroDetailComponent组件的hero属性绑定在一起,就像这样:

<my-hero-detail [hero]="selectedHero"></my-hero-detail>
AppComponent的模板是这样的:
技术分享
template: `  <h1>{{title}}</h1>  <h2>My Heroes</h2>  <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>  <my-hero-detail [hero]="selectedHero"></my-hero-detail>`,
View Code

感谢数据绑定机制,HeroDetailComponent应该能接收来自AppComponent的英雄数据,并在列表下方显示英雄的详情。 每当用户选中一个新的英雄时,详情信息应该随之更新。

搞定!

当在浏览器中查看应用时,可以看到英雄列表。 当选中一个英雄时,可以看到所选英雄的详情。

值得关注的进步是:我们可以在应用中的任何地方使用这个HeroDetailComponent组件来显示英雄详情。

我们创建了第一个可复用组件!

完整的代码文件如下

app/hero-detail.component.ts

技术分享
import { Component, Input } from ‘@angular/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>  `})export class HeroDetailComponent {  @Input()  hero: Hero;}
View Code

app/app.component.ts

技术分享
import { Component } from ‘@angular/core‘;import { Hero } from ‘./hero‘;const HEROES: Hero[] = [  { id: 11, name: ‘Mr. Nice‘ },  { id: 12, name: ‘Narco‘ },  { id: 13, name: ‘Bombasto‘ },  { id: 14, name: ‘Celeritas‘ },  { id: 15, name: ‘Magneta‘ },  { id: 16, name: ‘RubberMan‘ },  { id: 17, name: ‘Dynama‘ },  { id: 18, name: ‘Dr IQ‘ },  { id: 19, name: ‘Magma‘ },  { id: 20, name: ‘Tornado‘ }];@Component({  selector: ‘my-app‘,  template: `    <h1>{{title}}</h1>    <h2>My Heroes</h2>    <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>    <my-hero-detail [hero]="selectedHero"></my-hero-detail>  `,  styles: [`    .selected {      background-color: #CFD8DC !important;      color: white;    }    .heroes {      margin: 0 0 2em 0;      list-style-type: none;      padding: 0;      width: 15em;    }    .heroes li {      cursor: pointer;      position: relative;      left: 0;      background-color: #EEE;      margin: .5em;      padding: .3em 0;      height: 1.6em;      border-radius: 4px;    }    .heroes li.selected:hover {      background-color: #BBD8DC !important;      color: white;    }    .heroes li:hover {      color: #607D8B;      background-color: #DDD;      left: .1em;    }    .heroes .text {      position: relative;      top: -3px;    }    .heroes .badge {      display: inline-block;      font-size: small;      color: white;      padding: 0.8em 0.7em 0 0.7em;      background-color: #607D8B;      line-height: 1em;      position: relative;      left: -1px;      top: -4px;      height: 1.8em;      margin-right: .8em;      border-radius: 4px 0 0 4px;    }  `]})export class AppComponent {  title = ‘Tour of Heroes‘;  heroes = HEROES;  selectedHero: Hero;  onSelect(hero: Hero): void {    this.selectedHero = hero;  }}
View Code

app/hero.ts

技术分享
export class Hero {  id: number;  name: string;}
View Code

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 { HeroDetailComponent } from ‘./hero-detail.component‘;@NgModule({  imports: [    BrowserModule,    FormsModule  ],  declarations: [    AppComponent,    HeroDetailComponent  ],  bootstrap: [ AppComponent ]})export class AppModule { }
View Code

 

 

走过的路

来盘点一下我们已经构建了什么。

  • 我们创建了一个可复用组件

  • 我们学会了如何让一个组件接收输入

  • 我们学会了在 Angular 模块中声明该应用所需的指令。 只要把这些指令列在NgModule装饰器的declarations数组中就可以了。

  • 我们学会了把父组件绑定到子组件。

 

前方的路

通过抽取共享组件,我们的《英雄指南》变得更有复用性了。

AppComponent中,我们仍然使用着模拟数据。 显然,这种方式不能“可持续发展”。 我们要把数据访问逻辑抽取到一个独立的服务中,并在需要数据的组件之间共享。

在下一步,我们将学习如何创建服务。 

我们把主从结构的页面重构成多个组件