首页 > 代码库 > 我们把主从结构的页面重构成多个组件
我们把主从结构的页面重构成多个组件
多个组件
我们的应用正在成长中。现在又有新的用例:重复使用组件,传递数据给组件并创建更多可复用。 我们来把英雄详情从英雄列表中分离出来,让这个英雄详情组件可以被复用。
首先老规矩,我们得让我们的代码运行起来:
让应用代码保持转译和运行
我们要启动 TypeScript 编译器,它会监视文件变更,并启动开发服务器。只要敲:
npm start
这个命令会在我们构建《英雄指南》的时候让应用得以持续运行。
制作英雄详情组件
目前,英雄列表和英雄详情位于同一个文件的同一个组件中。 它们现在还很小,但很快它们都会长大。 我们将来肯定会收到新需求:针对这一个,却不能影响另一个。 然而,每一次更改都会给这两个组件带来风险和双倍的测试负担,却没有任何好处。 如果我们需要在应用的其它地方复用英雄详情组件,英雄列表组件也会跟着混进去。
我们当前的组件违反了单一职责原则。 虽然这只是一个教程,但我们还是得坚持做正确的事 — 况且,做正确的事这么容易,在此过程中,我们又能学习如何构建 Angular 应用。
我们来把英雄详情拆分成一个独立的组件。
拆分英雄详情组件
在app
目录下添加一个名叫hero-detail.component.ts
的文件,并且创建HeroDetailComponent
。代码如下:
import {Component,Input } from ‘@angular/core‘;@Component({ selector:"my-hero-detail",})export class HeroDetailComponent{}
命名约定
我们希望一眼就能看出哪些类是组件,哪些文件包含组件。
你会注意到,在名叫app.component.ts
的文件中有一个AppComponent
组件,在名叫hero-detail.component.ts
的文件中有一个HeroDetailComponent
组件。
我们的所有组件名都以Component
结尾。所有组件的文件名都以.component
结尾。
这里我们使用小写中线命名法 (也叫烤串命名法)拼写文件名, 所以不用担心它在服务器或者版本控制系统中出现大小写问题。
我们先从 Angular 中导入Component
和Input
装饰器,因为马上就会用到它们。
我们使用@Component
装饰器创建元数据。在元数据中,我们指定选择器的名字,用以标识此组件的元素。 然后,我们导出这个类,以便其它组件可以使用它
做完这些,我们把它导入AppComponent
组件,并创建相应的<my-hero-detail>
元素。
英雄详情模板
此时,AppComponent
的英雄列表和英雄详情视图被组合进同一个模板中。 让我们从AppComponent
中剪切出英雄详情的内容,并且粘贴到HeroDetailComponent
组件的template
属性中。
之前我们绑定了AppComponent
的selectedHero.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>`
现在,我们的英雄详情布局只存在于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;}
我们从hero.ts
中导出Hero
类,因为我们要从两个组件文件中引用它。 在app.component.ts
和hero-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‘;
接下来,添加HeroDetailComponent
到NgModule
装饰器中的declarations
数组。 这个数组包含了所有由我们创建的并属于应用模块的组件、管道和指令。
@NgModule({ imports: [ BrowserModule, FormsModule ], declarations: [ AppComponent, HeroDetailComponent ], bootstrap: [ AppComponent ]})export class AppModule { }
更新 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>`,
感谢数据绑定机制,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;}
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; }}
app/hero.ts
export class Hero { id: number; name: string;}
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 { }
走过的路
来盘点一下我们已经构建了什么。
我们创建了一个可复用组件
我们学会了如何让一个组件接收输入
我们学会了在 Angular 模块中声明该应用所需的指令。 只要把这些指令列在
NgModule
装饰器的declarations
数组中就可以了。我们学会了把父组件绑定到子组件。
前方的路
通过抽取共享组件,我们的《英雄指南》变得更有复用性了。
在AppComponent
中,我们仍然使用着模拟数据。 显然,这种方式不能“可持续发展”。 我们要把数据访问逻辑抽取到一个独立的服务中,并在需要数据的组件之间共享。
在下一步,我们将学习如何创建服务。
我们把主从结构的页面重构成多个组件