组件通讯

This cookbook contains recipes for common component communication scenarios in which two or more components share information.

本烹饪宝典包含了常见的组件通讯场景,也就是让两个或多个组件之间共享信息的方法。

For an in-depth look at each fundamental concepts in component communication, we can find detailed description and samples in the Component Communication document.

要深入了解组件通讯的各个基本概念,在组件通讯文档中可以找到详细的描述和例子。

Table of contents

目录

Pass data from parent to child with input binding

使用输入型绑定,把数据从父组件传到子组件

Intercept input property changes with a setter

通过setter拦截输入属性值的变化

Intercept input property changes with ngOnChanges

使用ngOnChanges拦截输入属性值的变化

Parent listens for child event

父组件监听子组件的事件

Parent interacts with child via a local variable

父组件与子组件通过本地变量local variable互动

Parent calls a ViewChild

父组件调用ViewChild

Parent and children communicate via a service

父组件和子组件通过服务来通讯

See the .

参见在线例子

Pass data from parent to child with input binding

通过输入型绑定把数据从父组件传到子组件。

HeroChildComponent has two input properties, typically adorned with @Input decorations.

HeroChildComponent 有两个输入型属性,它们通常带@Input装饰器

import { Component, Input } from '@angular/core'; import { Hero } from './hero'; @Component({ selector: 'hero-child', template: ` <h3>{{hero.name}} says:</h3> <p>I, {{hero.name}}, am at your service, {{masterName}}.</p> ` }) export class HeroChildComponent { @Input() hero: Hero; @Input('master') masterName: string; }

The second @Input aliases the child component property name masterName as 'master'.

第二个@Input为子组件的属性名masterName指定一个别名master(译者注:不推荐为起别名,请参见风格指南).

The HeroParentComponent nests the child HeroChildComponent inside an *ngFor repeater, binding its master string property to the child's master alias and each iteration's hero instance to the child's hero property.

父组件HeroParentComponent把子组件的HeroChildComponent放到*ngFor循环器中,把自己的master字符串属性绑定到子组件的master别名上,并把每个循环的hero实例绑定到子组件的hero属性。

import { Component } from '@angular/core'; import { HEROES } from './hero'; @Component({ selector: 'hero-parent', template: ` <h2>{{master}} controls {{heroes.length}} heroes</h2> <hero-child *ngFor="let hero of heroes" [hero]="hero" [master]="master"> </hero-child> ` }) export class HeroParentComponent { heroes = HEROES; master: string = 'Master'; }

The running application displays three heroes:

运行应用程序会显示三个英雄:

Parent-to-child

Test it

测试

E2E test that all children were instantiated and displayed as expected:

端到端测试,用于确保所有的子组件都像所期待的那样被初始化并显示出来。

// ... let _heroNames = ['Mr. IQ', 'Magneta', 'Bombasto']; let _masterName = 'Master'; it('should pass properties to children properly', function () { let parent = element.all(by.tagName('hero-parent')).get(0); let heroes = parent.all(by.tagName('hero-child')); for (let i = 0; i < _heroNames.length; i++) { let childTitle = heroes.get(i).element(by.tagName('h3')).getText(); let childDetail = heroes.get(i).element(by.tagName('p')).getText(); expect(childTitle).toEqual(_heroNames[i] + ' says:'); expect(childDetail).toContain(_masterName); } }); // ...

Back to top

回到顶部

Intercept input property changes with a setter

通过setter截听输入属性值的变化

Use an input property setter to intercept and act upon a value from the parent.

使用一个输入属性的setter,以拦截父组件中值的变化,并采取行动。

The setter of the name input property in the child NameChildComponent trims the whitespace from a name and replaces an empty value with default text.

子组件NameChildComponent的输入属性name上的这个setter,会trim掉名字里的空格,并把空值替换成默认字符串。

import { Component, Input } from '@angular/core'; @Component({ selector: 'name-child', template: ` <h3>"{{name}}"</h3> ` }) export class NameChildComponent { _name: string = '<no name set>'; @Input() set name(name: string) { this._name = (name && name.trim()) || '<no name set>'; } get name() { return this._name; } }

Here's the NameParentComponent demonstrating name variations including a name with all spaces:

下面的NameParentComponent展示了各种名字的处理方式,包括一个全是空格的名字。

import { Component } from '@angular/core'; @Component({ selector: 'name-parent', template: ` <h2>Master controls {{names.length}} names</h2> <name-child *ngFor="let name of names" [name]="name"> </name-child> ` }) export class NameParentComponent { // Displays 'Mr. IQ', '<no name set>', 'Bombasto' names = ['Mr. IQ', ' ', ' Bombasto ']; }
Parent-to-child-setter

Test it

测试

E2E tests of input property setter with empty and non-empty names:

端到端测试:输入属性的setter,分别使用空名字和非空名字。

// ... it('should display trimmed, non-empty names', function () { let _nonEmptyNameIndex = 0; let _nonEmptyName = '"Mr. IQ"'; let parent = element.all(by.tagName('name-parent')).get(0); let hero = parent.all(by.tagName('name-child')).get(_nonEmptyNameIndex); let displayName = hero.element(by.tagName('h3')).getText(); expect(displayName).toEqual(_nonEmptyName); }); it('should replace empty name with default name', function () { let _emptyNameIndex = 1; let _defaultName = '"<no name set>"'; let parent = element.all(by.tagName('name-parent')).get(0); let hero = parent.all(by.tagName('name-child')).get(_emptyNameIndex); let displayName = hero.element(by.tagName('h3')).getText(); expect(displayName).toEqual(_defaultName); }); // ...

Back to top

回到顶部

Intercept input property changes with ngOnChanges

通过ngOnChanges来截听输入属性值的变化

Detect and act upon changes to input property values with the ngOnChanges method of the OnChanges lifecycle hook interface.

使用OnChanges生命周期钩子接口的ngOnChanges方法来监测输入属性值的变化并做出回应。

May prefer this approach to the property setter when watching multiple, interacting input properties.

当需要监视多个、交互式输入属性的时候,本方法比用属性的setter更合适。

Learn about ngOnChanges in the LifeCycle Hooks chapter.

学习关于ngOnChanges的更多知识,参见生命周期钩子一章。

This VersionChildComponent detects changes to the major and minor input properties and composes a log message reporting these changes:

这个VersionChildComponent会监测输入属性majorminor的变化,并把这些变化编写成日志以报告这些变化。

import { Component, Input, OnChanges, SimpleChange } from '@angular/core'; @Component({ selector: 'version-child', template: ` <h3>Version {{major}}.{{minor}}</h3> <h4>Change log:</h4> <ul> <li *ngFor="let change of changeLog">{{change}}</li> </ul> ` }) export class VersionChildComponent implements OnChanges { @Input() major: number; @Input() minor: number; changeLog: string[] = []; ngOnChanges(changes: {[propKey: string]: SimpleChange}) { let log: string[] = []; for (let propName in changes) { let changedProp = changes[propName]; let from = JSON.stringify(changedProp.previousValue); let to = JSON.stringify(changedProp.currentValue); log.push( `${propName} changed from ${from} to ${to}`); } this.changeLog.push(log.join(', ')); } }

The VersionParentComponent supplies the minor and major values and binds buttons to methods that change them.

VersionParentComponent提供minormajor值,把修改它们值的方法绑定到按钮上。

import { Component } from '@angular/core'; @Component({ selector: 'version-parent', template: ` <h2>Source code version</h2> <button (click)="newMinor()">New minor version</button> <button (click)="newMajor()">New major version</button> <version-child [major]="major" [minor]="minor"></version-child> ` }) export class VersionParentComponent { major: number = 1; minor: number = 23; newMinor() { this.minor++; } newMajor() { this.major++; this.minor = 0; } }

Here's the output of a button-pushing sequence:

下面是点击按钮的结果。

Parent-to-child-onchanges

Test it

测试

Test that both input properties are set initially and that button clicks trigger the expected ngOnChanges calls and values:

测试确保这两个输入属性值都被初始化了,当点击按钮后,ngOnChanges应该被调用,属性的值也符合预期。

// ... // Test must all execute in this exact order it('should set expected initial values', function () { let actual = getActual(); let initialLabel = 'Version 1.23'; let initialLog = 'major changed from {} to 1, minor changed from {} to 23'; expect(actual.label).toBe(initialLabel); expect(actual.count).toBe(1); expect(actual.logs.get(0).getText()).toBe(initialLog); }); it('should set expected values after clicking \'Minor\' twice', function () { let repoTag = element(by.tagName('version-parent')); let newMinorButton = repoTag.all(by.tagName('button')).get(0); newMinorButton.click().then(function() { newMinorButton.click().then(function() { let actual = getActual(); let labelAfter2Minor = 'Version 1.25'; let logAfter2Minor = 'minor changed from 24 to 25'; expect(actual.label).toBe(labelAfter2Minor); expect(actual.count).toBe(3); expect(actual.logs.get(2).getText()).toBe(logAfter2Minor); }); }); }); it('should set expected values after clicking \'Major\' once', function () { let repoTag = element(by.tagName('version-parent')); let newMajorButton = repoTag.all(by.tagName('button')).get(1); newMajorButton.click().then(function() { let actual = getActual(); let labelAfterMajor = 'Version 2.0'; let logAfterMajor = 'major changed from 1 to 2, minor changed from 25 to 0'; expect(actual.label).toBe(labelAfterMajor); expect(actual.count).toBe(4); expect(actual.logs.get(3).getText()).toBe(logAfterMajor); }); }); function getActual() { let versionTag = element(by.tagName('version-child')); let label = versionTag.element(by.tagName('h3')).getText(); let ul = versionTag.element((by.tagName('ul'))); let logs = ul.all(by.tagName('li')); return { label: label, logs: logs, count: logs.count() }; } // ...

Back to top

回到顶部

Parent listens for child event

父组件监听子组件的事件

The child component exposes an EventEmitter property with which it emitsevents when something happens. The parent binds to that event property and reacts to those events.

子组件暴露一个EventEmitter属性,当事件发生时,子组件利用该属性emits(向上弹射)事件。父组件绑定到这个事件属性,并在事件发生时作出回应。

The child's EventEmitter property is an output property, typically adorned with an @Output decoration as seen in this VoterComponent:

子组件的EventEmitter属性是一个输出属性,通常带有@Output装饰器,就像在VoterComponent中看到的。

import { Component, EventEmitter, Input, Output } from '@angular/core'; @Component({ selector: 'my-voter', template: ` <h4>{{name}}</h4> <button (click)="vote(true)" [disabled]="voted">Agree</button> <button (click)="vote(false)" [disabled]="voted">Disagree</button> ` }) export class VoterComponent { @Input() name: string; @Output() onVoted = new EventEmitter<boolean>(); voted = false; vote(agreed: boolean) { this.onVoted.emit(agreed); this.voted = true; } }

Clicking a button triggers emission of a true or false (the boolean payload).

点击按钮会触发truefalse(布尔型有效载荷)的事件。

The parent VoteTakerComponent binds an event handler (onVoted) that responds to the child event payload ($event) and updates a counter.

父组件VoteTakerComponent绑定了一个事件处理器(onVoted),用来响应子组件的事件($event)并更新一个计数器。

import { Component } from '@angular/core'; @Component({ selector: 'vote-taker', template: ` <h2>Should mankind colonize the Universe?</h2> <h3>Agree: {{agreed}}, Disagree: {{disagreed}}</h3> <my-voter *ngFor="let voter of voters" [name]="voter" (onVoted)="onVoted($event)"> </my-voter> ` }) export class VoteTakerComponent { agreed = 0; disagreed = 0; voters = ['Mr. IQ', 'Ms. Universe', 'Bombasto']; onVoted(agreed: boolean) { agreed ? this.agreed++ : this.disagreed++; } }

The framework passes the event argument — represented by $event — to the handler method, and the method processes it:

框架(Angular)把事件参数(用$event表示)传给事件处理方法,这个方法会处理:

Child-to-parent

Test it

测试

Test that clicking the Agree and Disagree buttons update the appropriate counters:

测试确保点击AgreeDisagree按钮时,计数器被正确更新。

// ... it('should not emit the event initially', function () { let voteLabel = element(by.tagName('vote-taker')) .element(by.tagName('h3')).getText(); expect(voteLabel).toBe('Agree: 0, Disagree: 0'); }); it('should process Agree vote', function () { let agreeButton1 = element.all(by.tagName('my-voter')).get(0) .all(by.tagName('button')).get(0); agreeButton1.click().then(function() { let voteLabel = element(by.tagName('vote-taker')) .element(by.tagName('h3')).getText(); expect(voteLabel).toBe('Agree: 1, Disagree: 0'); }); }); it('should process Disagree vote', function () { let agreeButton1 = element.all(by.tagName('my-voter')).get(1) .all(by.tagName('button')).get(1); agreeButton1.click().then(function() { let voteLabel = element(by.tagName('vote-taker')) .element(by.tagName('h3')).getText(); expect(voteLabel).toBe('Agree: 1, Disagree: 1'); }); }); // ...

Back to top

回到顶部

Parent interacts with child via local variable

父组件与子组件通过本地变量互动

A parent component cannot use data binding to read child properties or invoke child methods. We can do both by creating a template reference variable for the child element and then reference that variable within the parent template as seen in the following example.

父组件不能使用数据绑定来读取子组件的属性或调用子组件的方法。但可以在父组件模板里,新建一个本地变量来代表子组件,然后利用这个变量来读取子组件的属性和调用子组件的方法,如下例所示。


We have a child CountdownTimerComponent that repeatedly counts down to zero and launches a rocket. It has start and stop methods that control the clock and it displays a countdown status message in its own template.

子组件CountdownTimerComponent进行倒计时,归零时发射一个导弹。startstop方法负责控制时钟并在模板里显示倒计时的状态信息。

import { Component, OnDestroy, OnInit } from '@angular/core'; @Component({ selector: 'countdown-timer', template: '<p>{{message}}</p>' }) export class CountdownTimerComponent implements OnInit, OnDestroy { intervalId = 0; message = ''; seconds = 11; clearTimer() { clearInterval(this.intervalId); } ngOnInit() { this.start(); } ngOnDestroy() { this.clearTimer(); } start() { this.countDown(); } stop() { this.clearTimer(); this.message = `Holding at T-${this.seconds} seconds`; } private countDown() { this.clearTimer(); this.intervalId = window.setInterval(() => { this.seconds -= 1; if (this.seconds === 0) { this.message = 'Blast off!'; } else { if (this.seconds < 0) { this.seconds = 10; } // reset this.message = `T-${this.seconds} seconds and counting`; } }, 1000); } }

Let's see the CountdownLocalVarParentComponent that hosts the timer component.

让我们来看看计时器组件的宿主组件CountdownLocalVarParentComponent

import { Component } from '@angular/core'; import { CountdownTimerComponent } from './countdown-timer.component'; @Component({ selector: 'countdown-parent-lv', template: ` <h3>Countdown to Liftoff (via local variable)</h3> <button (click)="timer.start()">Start</button> <button (click)="timer.stop()">Stop</button> <div class="seconds">{{timer.seconds}}</div> <countdown-timer #timer></countdown-timer> `, styleUrls: ['demo.css'] }) export class CountdownLocalVarParentComponent { }

The parent component cannot data bind to the child's start and stop methods nor to its seconds property.

父组件不能通过数据绑定使用子组件的startstop方法,也不能访问子组件的seconds属性。

We can place a local variable (#timer) on the tag (<countdown-timer>) representing the child component. That gives us a reference to the child component itself and the ability to access any of its properties or methods from within the parent template.

把本地变量(#timer)放到(<countdown-timer>)标签中,用来代表子组件。这样父组件的模板就得到了子组件的引用,于是可以在父组件的模板中访问子组件的所有属性和方法。

In this example, we wire parent buttons to the child's start and stop and use interpolation to display the child's seconds property.

在这个例子中,我们把父组件的按钮绑定到子组件的startstop方法,并用插值表达式来显示子组件的seconds属性。

Here we see the parent and child working together.

下面是父组件和子组件一起工作时的效果。

countdown timer

Test it

测试

Test that the seconds displayed in the parent template match the seconds displayed in the child's status message. Test also that clicking the Stop button pauses the countdown timer:

测试确保在父组件模板中显示的秒数和子组件状态信息里的秒数同步。它还会点击Stop按钮来停止倒计时:

// ... it('timer and parent seconds should match', function () { let parent = element(by.tagName(parentTag)); let message = parent.element(by.tagName('countdown-timer')).getText(); browser.sleep(10); // give `seconds` a chance to catchup with `message` let seconds = parent.element(by.className('seconds')).getText(); expect(message).toContain(seconds); }); it('should stop the countdown', function () { let parent = element(by.tagName(parentTag)); let stopButton = parent.all(by.tagName('button')).get(1); stopButton.click().then(function() { let message = parent.element(by.tagName('countdown-timer')).getText(); expect(message).toContain('Holding'); }); }); // ...

Back to top

回到顶部

Parent calls a ViewChild

父组件调用ViewChild

The local variable approach is simple and easy. But it is limited because the parent-child wiring must be done entirely within the parent template. The parent component itself has no access to the child.

这个本地变量方法是个简单便利的方法。但是它也有局限性,因为父组件-子组件的连接必须全部在父组件的模板中进行。父组件本身的代码对子组件没有访问权。

We can't use the local variable technique if an instance of the parent component class must read or write child component values or must call child component methods.

如果父组件的需要读取子组件的属性值或调用子组件的方法,就不能使用本地变量方法。

When the parent component class requires that kind of access, we inject the child component into the parent as a ViewChild.

当父组件需要这种访问时,可以把子组件作为ViewChild注入到父组件里面。

We'll illustrate this technique with the same Countdown Timer example. We won't change its appearance or behavior. The child CountdownTimerComponent is the same as well.

我们将会用同一个倒计时范例来解释这种技术。我们没有改变它的样子或行为。子组件CountdownTimerComponent也和原来一样。

We are switching from the local variable to the ViewChild technique solely for the purpose of demonstration.

本地变量切换到ViewChild技术的唯一目的就是做示范。

Here is the parent, CountdownViewChildParentComponent:

下面是父组件CountdownViewChildParentComponent:

import { AfterViewInit, ViewChild } from '@angular/core'; import { Component } from '@angular/core'; import { CountdownTimerComponent } from './countdown-timer.component'; @Component({ selector: 'countdown-parent-vc', template: ` <h3>Countdown to Liftoff (via ViewChild)</h3> <button (click)="start()">Start</button> <button (click)="stop()">Stop</button> <div class="seconds">{{ seconds() }}</div> <countdown-timer></countdown-timer> `, styleUrls: ['demo.css'] }) export class CountdownViewChildParentComponent implements AfterViewInit { @ViewChild(CountdownTimerComponent) private timerComponent: CountdownTimerComponent; seconds() { return 0; } ngAfterViewInit() { // Redefine `seconds()` to get from the `CountdownTimerComponent.seconds` ... // but wait a tick first to avoid one-time devMode // unidirectional-data-flow-violation error setTimeout(() => this.seconds = () => this.timerComponent.seconds, 0); } start() { this.timerComponent.start(); } stop() { this.timerComponent.stop(); } }

It takes a bit more work to get the child view into the parent component class.

把子组件的视图插入到父组件类需要做一点额外的工作。

We import references to the ViewChild decorator and the AfterViewInit lifecycle hook.

需要通过ViewChild装饰器导入这个引用,并挂上AfterViewInit生命周期钩子。

We inject the child CountdownTimerComponent into the private timerComponent property via the @ViewChild property decoration.

通过@ViewChild属性装饰器,将子组件CountdownTimerComponent注入到私有属性timerComponent里面。

The #timer local variable is gone from the component metadata. Instead we bind the buttons to the parent component's own start and stop methods and present the ticking seconds in an interpolation around the parent component's seconds method.

组件元数据里就不再需要#timer本地变量了。而是把按钮绑定到父组件自己的startstop方法,使用父组件的seconds方法的插值表达式来展示秒数变化。

These methods access the injected timer component directly.

这些方法可以直接访问被注入的计时器组件。

The ngAfterViewInit lifecycle hook is an important wrinkle. The timer component isn't available until after Angular displays the parent view. So we display 0 seconds initially.

ngAfterViewInit生命周期钩子是非常重要的一步。被注入的计时器组件只有在Angular显示了父组件视图之后才能访问,所以我们先把秒数显示为0.

Then Angular calls the ngAfterViewInit lifecycle hook at which time it is too late to update the parent view's display of the countdown seconds. Angular's unidirectional data flow rule prevents us from updating the parent view's in the same cycle. We have to wait one turn before we can display the seconds.

然后Angular会调用ngAfterViewInit生命周期钩子,但这时候再更新父组件视图的倒计时就已经太晚了。Angular的单向数据流规则会阻止在同一个周期内更新父组件视图。我们在显示秒数之前会被迫再等一轮

We use setTimeout to wait one tick and then revise the seconds method so that it takes future values from the timer component.

使用setTimeout来等下一轮,然后改写seconds方法,这样它接下来就会从注入的这个计时器组件里获取秒数的值。

Test it

测试

Use the same countdown timer tests as before.

使用和之前一样的倒计时测试

Back to top

回到顶部

Parent and children communicate via a service

父组件和子组件通过服务来通讯

A parent component and its children share a service whose interface enables bi-directional communication within the family.

父组件和它的子组件共享同一个服务,利用该服务在家庭内部实现双向通讯。

The scope of the service instance is the parent component and its children. Components outside this component subtree have no access to the service or their communications.

该服务实例的作用域被限制在父组件和其子组件内。这个组件子树之外的组件将无法访问该服务或者与它们通讯。

This MissionService connects the MissionControlComponent to multiple AstronautComponent children.

这个MissionServiceMissionControlComponent和多个AstronautComponent子组件连接起来。

import { Injectable } from '@angular/core'; import { Subject } from 'rxjs/Subject'; @Injectable() export class MissionService { // Observable string sources private missionAnnouncedSource = new Subject<string>(); private missionConfirmedSource = new Subject<string>(); // Observable string streams missionAnnounced$ = this.missionAnnouncedSource.asObservable(); missionConfirmed$ = this.missionConfirmedSource.asObservable(); // Service message commands announceMission(mission: string) { this.missionAnnouncedSource.next(mission); } confirmMission(astronaut: string) { this.missionConfirmedSource.next(astronaut); } }

The MissionControlComponent both provides the instance of the service that it shares with its children (through the providers metadata array) and injects that instance into itself through its constructor:

MissionControlComponent提供服务的实例,并将其共享给它的子组件(通过providers元数据数组),子组件可以通过构造函数将该实例注入到自身。

import { Component } from '@angular/core'; import { MissionService } from './mission.service'; @Component({ selector: 'mission-control', template: ` <h2>Mission Control</h2> <button (click)="announce()">Announce mission</button> <my-astronaut *ngFor="let astronaut of astronauts" [astronaut]="astronaut"> </my-astronaut> <h3>History</h3> <ul> <li *ngFor="let event of history">{{event}}</li> </ul> `, providers: [MissionService] }) export class MissionControlComponent { astronauts = ['Lovell', 'Swigert', 'Haise']; history: string[] = []; missions = ['Fly to the moon!', 'Fly to mars!', 'Fly to Vegas!']; nextMission = 0; constructor(private missionService: MissionService) { missionService.missionConfirmed$.subscribe( astronaut => { this.history.push(`${astronaut} confirmed the mission`); }); } announce() { let mission = this.missions[this.nextMission++]; this.missionService.announceMission(mission); this.history.push(`Mission "${mission}" announced`); if (this.nextMission >= this.missions.length) { this.nextMission = 0; } } }

The AstronautComponent also injects the service in its constructor. Each AstronautComponent is a child of the MissionControlComponent and therefore receives its parent's service instance:

AstronautComponent也通过自己的构造函数注入该服务。由于每个AstronautComponent都是MissionControlComponent的子组件,所以它们获取到的也是父组件的这个服务实例。

import { Component, Input, OnDestroy } from '@angular/core'; import { MissionService } from './mission.service'; import { Subscription } from 'rxjs/Subscription'; @Component({ selector: 'my-astronaut', template: ` <p> {{astronaut}}: <strong>{{mission}}</strong> <button (click)="confirm()" [disabled]="!announced || confirmed"> Confirm </button> </p> ` }) export class AstronautComponent implements OnDestroy { @Input() astronaut: string; mission = '<no mission announced>'; confirmed = false; announced = false; subscription: Subscription; constructor(private missionService: MissionService) { this.subscription = missionService.missionAnnounced$.subscribe( mission => { this.mission = mission; this.announced = true; this.confirmed = false; }); } confirm() { this.confirmed = true; this.missionService.confirmMission(this.astronaut); } ngOnDestroy() { // prevent memory leak when component destroyed this.subscription.unsubscribe(); } }

Notice that we capture the subscription and unsubscribe when the AstronautComponent is destroyed. This is a memory-leak guard step. There is no actual risk in this app because the lifetime of a AstronautComponent is the same as the lifetime of the app itself. That would not always be true in a more complex application.

注意,通过subscription服务订阅任务,并在AstronautComponent被销毁的时候退订。这是一个用于防止内存泄漏的保护措施。实际上,在这个应用程序中并没有这个风险,因为AstronautComponent的生命期和应用程序的生命期一样长。但在更复杂的应用程序环境中就不一定了。

We do not add this guard to the MissionControlComponent because, as the parent, it controls the lifetime of the MissionService.

不需要在MissionControlComponent中添加这个保护措施,因为作为父组件,它控制着MissionService的生命期。

The History log demonstrates that messages travel in both directions between the parent MissionControlComponent and the AstronautComponent children, facilitated by the service:

History日志证明了:在父组件MissionControlComponent和子组件AstronautComponent之间,信息通过该服务实现了双向传递。

bidirectional-service

Test it

测试

Tests click buttons of both the parent MissionControlComponent and the AstronautComponent children and verify that the History meets expectations:

测试确保点击父组件MissionControlComponent和子组件AstronautComponent两个的组件的按钮时,History日志和预期的一样。

// ... it('should announce a mission', function () { let missionControl = element(by.tagName('mission-control')); let announceButton = missionControl.all(by.tagName('button')).get(0); announceButton.click().then(function () { let history = missionControl.all(by.tagName('li')); expect(history.count()).toBe(1); expect(history.get(0).getText()).toMatch(/Mission.* announced/); }); }); it('should confirm the mission by Lovell', function () { testConfirmMission(1, 2, 'Lovell'); }); it('should confirm the mission by Haise', function () { testConfirmMission(3, 3, 'Haise'); }); it('should confirm the mission by Swigert', function () { testConfirmMission(2, 4, 'Swigert'); }); function testConfirmMission(buttonIndex: number, expectedLogCount: number, astronaut: string) { let _confirmedLog = ' confirmed the mission'; let missionControl = element(by.tagName('mission-control')); let confirmButton = missionControl.all(by.tagName('button')).get(buttonIndex); confirmButton.click().then(function () { let history = missionControl.all(by.tagName('li')); expect(history.count()).toBe(expectedLogCount); expect(history.get(expectedLogCount - 1).getText()).toBe(astronaut + _confirmedLog); }); } // ...

Back to top

回到顶部