组件间通讯的必要性
组件就像零散的积木,我们需要把这些积木按照一定的规则拼装起来,而且要让它们互相之间能进行通讯,这样才能构成一个有机的完整系统。
在真实的应用中,组件最终会构成树形结构,就像人类社会中的家族树一样
在树形结构里面,组件之间有几种典型的关系:父子关系、兄弟关系、亦或没有直接关系。
相应地,组件之间有以下几种典型的通讯方案:
- 直接的父子关系:父组件直接访问子组件的 public 属性和方法。
- 直接的父子关系:借助于 @Input 和 @Output 进行通讯
- 没有直接关系:借助于 Service 单例进行通讯。
- 利用 cookie 和 localstorage 进行通讯。
- 利用 session 进行通讯。

无论使用什么前端框架,组件之间的通讯都离开不以上几种方案,这些方案与具体框架无关。
(父子)直接调用
对于有直接父子关系的组件,父组件可以直接访问子组件里面 public 型的属性和方法,示例代码片段如下:
组件的模板部分
parent.component.html
1 2 3 4 5 6 7
| <div class="panel panel-primary"> <div class="panel-heading">第一种:父子组件之间通讯</div> <div class="panel-body"> <app-child #child></app-child> <button (click)="child.childFn()" class="btn btn-success">调用子组件方法</button> </div> </div>
|
child.component.html
1 2 3
| <div> <div class="btn">这是子组件</div> </div>
|
显然,子组件里面必须暴露一个 public 型的 childFn 方法,就像这样:
child.component.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import { Component, OnInit } from '@angular/core';
@Component({ selector: 'app-child', templateUrl: './child.component.html', styleUrls: ['./child.component.css'] }) export class ChildComponent implements OnInit {
constructor() { } public childFn():void { alert("调用了子组件的方法") } ngOnInit() { }
}
|
以上是通过在模板里面定义局部变量的方式来直接调用子组件里面的 public 型方法,点击调用子组件方法的按钮,即可成功弹窗。
如果父组件的内部想访问到子组件的实例,需要利用到 @ViewChild 装饰器,示例如下:
parent.component.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import { Component, ViewChild, OnInit } from '@angular/core'; import { ChildComponent } from './child/child.component';
@Component({ selector: 'app-parent', templateUrl: './parent.component.html', styleUrls: ['./parent.component.css'] }) export class ParentChildComponent implements OnInit {
constructor() { } @ViewChild(ChildComponent) private childComponent: ChildComponent; ngOnInit() { }
}
|
很明显,如果父组件直接访问子组件,那么两个组件之间的关系就被固定死了。父子两个组件紧密依赖,谁也离不开谁,也就都不能单独使用了。所以,除非知道自己在做什么,最好不要直接在父组件里面直接访问子组件上的属性和方法,以免未来一改一大片。
我们可以利用 @Input 装饰器,让父组件直接给子组件传递参数;
也可以利用 @Output 装饰器,让父组件接收子组件派发的事件。
子组件上需要这样写:
app-child.component.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import { Component, Input, Output, OnInit, EventEmitter } from '@angular/core';
@Component({ selector: 'app-child', templateUrl: './child.component.html', styleUrls: ['./child.component.css'] }) export class ChildComponent implements OnInit {
constructor() { } @Input() public childText:string; @Output() public follow = new EventEmitter<string>(); public emitAnEvent() { this.follow.emit("follow") } ngOnInit() { }
}
|
在子组件的html模板里使用childText这个参数,顺便不要忘了新增按钮用来触发fowllow事件,具体代码如下
组件的模板部分
child
1 2 3 4
| <div class="panel"> <div class="btn">{{childText}}</div> <button (click)="emitAnEvent()" class="btn btn-success">点击触发事件</button> </div>
|
在父组件的html模板里修改这个参数,并定义触发fowllow事件后应该做什么:
parent
1 2 3 4 5 6 7
| <div class="panel panel-primary"> <div class="panel-heading">第一种:父子组件之间通讯</div> <div class="panel-body"> <app-child (follow)="doSomething()" childText="父组件在模板里修改了子组件参数"></app-child> <button class="btn btn-success">父组件</button> </div> </div>
|
parent.component.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| import { Component, ViewChild, OnInit } from '@angular/core'; import { ChildComponent } from './child/child.component';
@Component({ selector: 'app-parent', templateUrl: './parent.component.html', styleUrls: ['./parent.component.css'] }) export class ParentChildComponent implements OnInit {
constructor() { }
doSomething() { alert("触发子组件的事件,调用了父组件的方法") }
ngOnInit() { }
}
|
点击子组件的按钮可以触发事件,父组件捕获到了这个事件,然后执行了弹窗的方法
利用 Service 单例进行通讯
如果你在根模块(一般是 app.module.ts)的 providers 里面注册一个 Service,那么这个 Service 就是全局单例的,这样一来我们就可以利用这个单例的 Service 在不同的组件之间进行通讯了。
首先使用angular-cli生成组件及service(这样做的目的是省去了一堆麻烦的手动操作)
1 2 3 4
| ng g c brother ng g c brother/child-1 ng g c brother/child-1 ng g s brother/service/event-bus
|
注意最后一个命令,生成后仍须手动配置app.module.ts,下文会有代码示例。
- 比较粗暴的方式:我们可以在 Service 里面定义 public 型的共享变量,然后让不同的组件都来访问这块变量,从而达到共享数据的目的。
- 优雅一点的方式:利用 RxJS,在 Service 里面定义一个 public 型的 Subject(主题),然后让所有组件都来subscribe(订阅)这个主题,类似于一种“事件总线”的效果。
由于第一种方式太过于简单粗暴,本次代码展示以第二种方式为例
思维导图

event-bus.service.ts
1 2 3 4 5 6 7 8 9 10 11 12 13
| import { Injectable } from '@angular/core';
import { Subject } from "rxjs/Subject";
@Injectable() export class EventBusService { public eventBus:Subject<string> = new Subject<string>();
constructor() { }
}
|
app.module.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import { BrowserModule } from '@angular/platform-browser'; import { NgModule } from '@angular/core';
import { AppComponent } from './app.component'; import { BrotherComponent } from './brother/brother.component'; import { Child1Component } from './brother/child-1/child-1.component'; import { Child2Component } from './brother/child-2/child-2.component';
import { EventBusService } from './brother/service/event-bus.service';
@NgModule({ declarations: [ AppComponent, BrotherComponent, Child1Component, Child2Component ], imports: [ BrowserModule ], providers: [EventBusService], bootstrap: [AppComponent] }) export class AppModule { }
|
组件的模板部分
brother
1 2 3 4 5 6 7
| <div class="panel panel-primary"> <div class="panel-heading">第二种:没有父子关系的组件间通讯</div> <div class="panel-body"> <app-child-1></app-child-1> <app-child-2></app-child-2> </div> </div>
|
child-1
1 2 3 4 5 6
| <div class="panel panel-primary"> <div class="panel-heading">第一个组件</div> <div class="panel-body"> <button (click)="triggerEventBus()" class="btn btn-success">触发一个事件</button> </div> </div>
|
child-2
1 2 3 4 5 6
| <div class="panel panel-primary"> <div class="panel-heading">第二个组件</div> <div class="panel-body"> <p *ngFor="let event of events">{{event}}</p> </div> </div>
|
child-1.component.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import { Component, OnInit } from '@angular/core'; import { EventBusService } from '../service/event-bus.service';
@Component({ selector: 'app-child-1', templateUrl: './child-1.component.html', styleUrls: ['./child-1.component.css'] }) export class Child1Component implements OnInit {
constructor( public eventBusService:EventBusService ) { }
ngOnInit() { } public index:number = 1; public triggerEventBus() { this.eventBusService.eventBus.next(`第一个组件触发了第${this.index}个事件`); this.index++; } }
|
child-2.component.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import { Component, OnInit } from '@angular/core'; import { EventBusService } from '../service/event-bus.service';
@Component({ selector: 'app-child-2', templateUrl: './child-2.component.html', styleUrls: ['./child-2.component.css'] }) export class Child2Component implements OnInit { public events:Array<any> = [];
constructor( public eventBusService:EventBusService ) { } ngOnInit() { this.eventBusService.eventBus.subscribe( (value)=>{ this.events.push(value) } ) }
}
|
利用 cookie 或者 localstorage 进行通讯
依旧用命令行生成三个组件:父组件local-storage,两个子组件:local-child-1,local-child-2
思维导图

代码示例:
组件的模板部分
local-storage
1 2 3 4 5 6 7
| <div class="panel panel-primary"> <div class="panel-heading">第三种方案:利用localStorge通讯</div> <div class="panel-body"> <app-local-child-1></app-local-child-1> <app-local-child-2></app-local-child-2> </div> </div>
|
local-child-1
1 2 3 4 5 6
| <div class="panel panel-primary"> <div class="panel-heading">第一个组件</div> <div class="panel-body"> <button (click)="writeData()" class="btn btn-success">写入数据</button> </div> </div>
|
local-child-2
1 2 3 4 5 6 7 8 9 10 11 12 13
| <div class="panel panel-primary"> <div class="panel-heading">第二个组件</div> <div class="panel-body"> <button (click)="readData()" class="btn btn-success">更新数据</button> </div> <table *ngIf="values.index" class="table table-bordered table-hover"> <caption class="h4 text-info text-center">数据信息</caption> <tr class="text-center"> <td>{{values.index}}</td> <td>{{values.date | date:'yyyy-MM-dd HH:mm:ss'}}</td> </tr> </table> </div>
|
local-child-1.component.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import { Component, OnInit } from '@angular/core';
@Component({ selector: 'app-local-child-1', templateUrl: './local-child-1.component.html', styleUrls: ['./local-child-1.component.css'] }) export class LocalChild1Component implements OnInit {
constructor() { }
ngOnInit() { } public index:number = 1; writeData() { window.localStorage.setItem('value', JSON.stringify({ index: this.index, date: new Date() })) this.index++; } }
|
local-child-2.component.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| import { Component, OnInit } from '@angular/core';
@Component({ selector: 'app-local-child-2', templateUrl: './local-child-2.component.html', styleUrls: ['./local-child-2.component.css'] }) export class LocalChild2Component implements OnInit {
constructor() { }
ngOnInit() { }
public values:any = {}
public readData() { let value = window.localStorage.getItem("value"); this.values = JSON.parse(value); }
}
|
如上所示,所有业务逻辑都是通过原生API完成的。很多朋友写 Angular 代码的时候出现了思维定势,总感觉 Angular 会封装所有东西,实际上并非如此。比如 cookie、localstorage 这些东西都可以直接用原生的 API 进行操作的。千万别忘记原生的那些 API 啊,都能用的!
利用 session 进行通讯
思维导图

与上边类似,不再赘述
小结
组件间的通讯方案是通用的,无论你使用什么样的前端框架,都会面临这个问题,而解决的方案无外乎本文所列出的几种。