多个组件

The AppComponent is doing everything at the moment. In the beginning, it showed details of a single hero. Then it became a master/detail form with both a list of heroes and the hero detail. Soon there will be new requirements and capabilities. You can't keep piling features on top of features in one component; that's not maintainable.

此刻,AppComponent负责所有事。 起初,它只显示单个英雄的详情。然后,它变成了主从结构,同时显示英雄列表和一个英雄详情。 现在,我们很快又会有新需求了。 我们不能把这些需求全都放在一个组件中,否则将不可维护。

You'll need to break it up into sub-components, each focused on a specific task or workflow. Eventually, the AppComponent could become a simple shell that hosts those sub-components.

我们要把它拆分成一些子组件,每个子组件只聚焦在一个特定的任务或工作流上。 最后,AppComponent将会变成一个简单的壳,用来作为那些子组件的宿主。

In this page, you'll take the first step in that direction by carving out the hero details into a separate, reusable component. When you're done, the app should look like this .

本章中,我们要做的第一步就是把英雄详情拆分到一个独立的、可复用的组件中。 做完这些,应用是这样的:

Where you left off

延续上一步教程

Before getting started on this page, verify that you have the following structure from earlier in the Tour of Heroes. If not, go back to the previous pages.

在继续《英雄指南》之前,先检查一下,是否已经有了如下目录结构。如果没有,回上一章,看看错过了哪里。

angular-tour-of-heroes
src
app
app.component.ts
app.module.ts
main.ts
index.html
styles.css
systemjs.config.js
tsconfig.json
node_modules ...
package.json

Keep the app transpiling and running while you build the Tour of Heroes by entering the npm start command in a terminal window as you did before.

像以前一样,在终端窗口中输入npm start命令,以便在构建《英雄指南》时保持持续转译和运行。

Make a hero detail component

制作英雄详情组件

Add a file named hero-detail.component.ts to the app/ folder. This file will hold the new HeroDetailComponent.

app/文件夹下添加一个名叫hero-detail.component.ts的文件。这个文件中会存放这个新的HeroDetailComponent

The file and component names follow the standard described in the Angular style guide.

文件名和组件名遵循风格指南中的标准方式。

Start writing the HeroDetailComponent as follows:

HeroDetailComponent的代码如下:

app/hero-detail.component.ts (initial version)

import { Component } from '@angular/core'; @Component({ selector: 'hero-detail', }) export class HeroDetailComponent { }

To define a component, you always import the Component symbol.

要定义一个组件,我们总是要先导入符号Component

The @Component decorator provides the Angular metadata for the component. The CSS selector name, hero-detail, will match the element tag that identifies this component within a parent component's template. Near the end of this tutorial page, you'll add a <hero-detail> element to the AppComponent template.

@Component装饰器为组件提供了Angular元数据。 CSS选择器的名字hero-detail会匹配元素的标签名,用于在父组件的模板中标记出当前组件的位置。 本章的最后,我们会把<hero-detail>添加到AppComponent的模板中。

Always export the component class because you'll always import it elsewhere.

总是export这个组件类,因为你必然会在别处import它。

Hero detail template

英雄详情的模板

To move the hero detail view to the HeroDetailComponent, cut the hero detail content from the bottom of the AppComponent template and paste it into a new template property in the @Component metadata.

要把英雄详情的视图移入HeroDetailComponent,只要把英雄详情的 内容AppComponent模板的底部剪切出来, 粘贴到@Component元数据的template属性中就可以了。

The HeroDetailComponent has a hero, not a selected hero. Replace the word, "selectedHero", with the word, "hero", everywhere in the template. When you're done, the new template should look like this:

HeroDetailComponent有一个 hero属性,而不再是selectedHero。 所以我们也要在模板中把所有的selectedHero替换为hero。 这些完成之后,新的模板是这样的:

src/app/hero-detail.component.ts (template)

@Component({ selector: '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> ` })

Add the hero property

添加hero属性

The HeroDetailComponent template binds to the component's hero property. Add that property to the HeroDetailComponent class like this:

HeroDetailComponent模板绑定到了该组件的hero属性上。 把这个属性添加到HeroDetailComponent类上,就像这样:

src/app/hero-detail.component.ts (hero property)

hero: Hero;

The hero property is typed as an instance of Hero. The Hero class is still in the app.component.ts file. Now there are two components that need to reference the Hero class. The Angular style guide recommends one class per file anyway.

hero属性的类型是HeroHero类仍然在app.component.ts文件中。 现在,有两个组件需要Hero类的引用。 而Angular风格指南建议每个文件中只有一个类。

Move the Hero class from app.component.ts to its own hero.ts file.

因此我们要把Hero类从app.component.ts移到它自己的hero.ts文件中:

src/app/hero.ts

export class Hero { id: number; name: string; }

Now that the Hero class is in its own file, the AppComponent and the HeroDetailComponent have to import it. Add the following import statement near the top of both the app.component.ts and the hero-detail.component.ts files.

现在,Hero类有了自己的文件,AppComponentHeroDetailComponent 就要import它了。 把下列import语句添加到app.component.tshero-detail.component.ts文件的顶部。

import { Hero } from './hero';

The hero property is an input property

hero属性是一个输入属性

Later in this page, the parent AppComponent will tell the child HeroDetailComponent which hero to display by binding its selectedHero to the hero property of the HeroDetailComponent. The binding will look like this:

在本章稍后的部分, 父组件AppComponent会告诉子组件HeroDetailComponent要显示哪个英雄, 告诉的方法是把它的selectedHero属性绑定到HeroDetailComponenthero属性上。 这种绑定是这样的:

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

Putting square brackets around the hero property, to the left of the equal sign (=), makes it the target of a property binding expression. You must declare a target binding property to be an input property. Otherwise, Angular rejects the binding and throws an error.

在等号的左边,是方括号围绕的hero属性,这表示它是属性绑定表达式的目标。 我们要绑定到的目标属性必须是一个输入属性,否则Angular会拒绝绑定,并抛出一个错误。

First, amend the @angular/core import statement to include the Input symbol.

首先,修改@angular/core导入语句,使其包含符号Input

src/app/hero-detail.component.ts (excerpt)

import { Component, Input } from '@angular/core';

Then declare that hero is an input property by preceding it with the @Input decorator that you imported earlier.

然后,通过在hero属性前面加上@Input装饰器,来表明它是一个输入属性。

src/app/hero-detail.component.ts (excerpt)

@Input() hero: Hero;

Read more about input properties in the Attribute Directives page.

要了解输入属性的更多知识,参见属性型指令页。

That's it. The hero property is the only thing in the HeroDetailComponent class.

现在,hero属性是HeroDetailComponent类中唯一的东西。

export class HeroDetailComponent { @Input() hero: Hero; }

All it does is receive a hero object through its hero input property and then bind to that property with its template.

它所做的一切就是通过它的输入属性hero接收一个英雄对象,然后把这个属性绑定到自己的模板中。

Here's the complete HeroDetailComponent.

下面是完整的HeroDetailComponent

src/app/hero-detail.component.ts

import { Component, Input } from '@angular/core'; import { Hero } from './hero'; @Component({ selector: '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; }

Declare HeroDetailComponent in the AppModule

AppModule中声明HeroDetailComponent

Every component must be declared in one—and only one—Angular module.

每个组件都必须在一个(且只有一个)Angular模块中声明。

Open app.module.ts in your editor and import the HeroDetailComponent so you can refer to it.

打开app.module.ts并且导入HeroDetailComponent,以便我们可以引用它。

src/app/app.module.ts

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

Add HeroDetailComponent to the module's declarations array.

HeroDetailComponent添加到该模块的declarations数组中。

src/app/app.module.ts

declarations: [ AppComponent, HeroDetailComponent ],

In general, the declarations array contains a list of application components, pipes, and directives that belong to the module. A component must be declared in a module before other components can reference it. This module declares only the two application components, AppComponent and HeroDetailComponent.

通常,declarations数组包含应用中属于该模块的组件、管道和指令的列表。 组件在被其它组件引用之前必须先在一个模块中声明过。 这个模块只声明了两个组件:AppComponentHeroDetailComponent

Read more about Angular modules in the NgModules guide.

要了解关于Angular模块的更多知识,参见Angular模块页。

Add the HeroDetailComponent to the AppComponent

HeroDetailComponent添加到AppComponent

The AppComponent is still a master/detail view. It used to display the hero details on its own, before you cut out that portion of the template. Now it will delegate to the HeroDetailComponent.

AppComponent仍然是主从视图。 在我们剪切模板之前,它自己显示英雄的详情。 现在,它委托给了HeroDetailComponent

Recall that hero-detail is the CSS selector in the HeroDetailComponent metadata. That's the tag name of the element that represents the HeroDetailComponent.

回想一下,hero-detail正是HeroDetailComponent元数据中使用的 CSS selector 它是一个HTML元素的标签名,用于表示HeroDetailComponent

Add a <hero-detail> element near the bottom of the AppComponent template, where the hero detail view used to be.

<hero-detail>元素添加到AppComponent模板的底部,那里就是英雄详情视图所在的位置。

Coordinate the master AppComponent with the HeroDetailComponent by binding the selectedHero property of the AppComponent to the hero property of the HeroDetailComponent.

协调主视图AppComponentHeroDetailComponent的方式是把AppComponentselectedHero属性绑定到HeroDetailComponenthero属性上。

app.component.ts (excerpt)

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

Now every time the selectedHero changes, the HeroDetailComponent gets a new hero to display.

每当selectedHero变化时,HeroDetailComponent就会显示一个新的英雄。

The revised AppComponent template should look like this:

修改后的AppComponent模板是这样的:

app.component.ts (excerpt)

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> <hero-detail [hero]="selectedHero"></hero-detail> `,

What changed?

有哪些变化?

As before, whenever a user clicks on a hero name, the hero detail appears below the hero list. But now the HeroDetailView is presenting those details.

仍然像以前一样,一旦用户点击了英雄的名字,英雄详情就会显示在英雄列表的下方。 不过现在改用HeroDetailView来表示英雄详情了。

Refactoring the original AppComponent into two components yields benefits, both now and in the future:

我们把原来的AppComponent重构成了两个组件具有一些显著优点,无论是现在还是未来:

  1. You simplified the AppComponent by reducing its responsibilities.

    通过缩减AppComponent的职责,我们简化了它。

  2. You can evolve the HeroDetailComponent into a rich hero editor without touching the parent AppComponent.

    我们将来可以把HeroDetailComponent改进为功能更丰富的英雄编辑器,而不用动AppComponent

  3. You can evolve the AppComponent without touching the hero detail view.

    同样,我们也可以改进AppComponent而不用动英雄详情视图。

  4. You can re-use the HeroDetailComponent in the template of some future parent component.

    我们可以在未来的其它父组件的模板中复用HeroDetailComponent

Review the app structure

审视本应用的代码结构

Verify that you have the following structure:

验证它是否已经有了如下结构:

angular-tour-of-heroes
src
app
app.component.ts
app.module.ts
hero.ts
hero-detail.component.ts
main.ts
index.html
styles.css
systemjs.config.js
tsconfig.json
node_modules ...
package.json

Here are the code files discussed in this page.

下面是我们在本章讨论的代码文件:

import { Component, Input } from '@angular/core'; import { Hero } from './hero'; @Component({ selector: '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; } 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> <hero-detail [hero]="selectedHero"></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; } } export class Hero { id: number; name: string; } 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 { }

The road you’ve travelled

走过的路

Here's what you achieved in this page:

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

Your app should look like this .

现在,应用应该变成了这样:

The road ahead

前方的路

The Tour of Heroes app is more reusable with shared components, but its (mock) data is still hard coded within the AppComponent. That's not sustainable. Data access should be refactored to a separate service and shared among the components that need data.

通过抽取共享组件,我们的《英雄指南》变得更有复用性了,但在AppComponent中,我们仍然使用着硬编码的模拟数据。显然,这种方式不能“可持续发展”。 我们要把数据访问逻辑重构到一个独立的服务中,并在需要数据的组件之间共享。

You’ll learn to create services in the next tutorial page.

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

下一步

服务