前言
目前在接手公司的一个开发相册的任务, 在编码的过程中, 发现一个会阻碍之后开发而且非常棘手的问题, 先了解一下现在的环境:
目前的结构组织大概是这样:
1 2 3 4
| - photoComponent // 父组件 - headerComponent // 子组件 - contentComponent // 子组件 - footerComponent // 子组件
|
目前的组件输出结构大概是这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <div class="wrapper"> <header></header> <div class="main" [class.full-width]="fullWidth"> <div ng-view autoscroll="true"> <router-outlet></router-outlet> </div> </div> <footer></footer> </div>
|
那么问题来了:
在 header
中有个菜单是来改变 content
中相册的布局的, 布局有分: 年/月/周, 当我在 header
子组件中选择某个布局的下拉值时, 需要告诉 content
子组件这个值是什么, 看过 Angular2
组件通讯的大家应该知道, 通讯大部分通过属性绑定来传递和接收, 但是这种情况只能是, 直系亲属父与子的传递, 因为在上述结构视图中, 很明显父组件中直接引入了子组件的布局, 我们现在的问题是 content
子组件虽然也是在 photo
父组件中来展示, 但是很不幸的是下面这行:
1
| <router-outlet></router-outlet>
|
这个路由输出阻断了传递, 它不能帮你传递任何绑定的属性值, 就是不能在这行代码是想办法, 绑定属性会报错, 而且不管加什么, 子组件 content
也不会接收到值.
但是我想着AngularJs2肯定有轻松解决的办法, 然后就通读Angular2-组件通讯, 篇幅很长, 第一遍不懂, 第二遍第三遍尝试, 终于明白点原理了, 这里的组件通讯有很多方法, 但是能解决现在问题的方法, 就是父与子组件通过服务来通讯
AngularJs2
官网的资料有时候写的不容易理解, 在这里我来简单阐述一下怎么运用服务来通讯.
先了解一下服务通讯的基本机制
抛开父与子的关系, 简单理解就是: 一个负责传递
, 一个负责订阅接收
单项服务传递
我来先介绍一个单项传递, 就是由 header
点击某个菜单时, 把这个值传递给 content
来接收.
- 先写一个
headerService
服务:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import { Injectable } from '@angular/core'; import { Subject } from 'rxjs/Subject'; @Injectable() export class HeaderService { private viewType = new Subject<string>(); view$ = this.viewType.asObservable(); announceViewType(view: string): void { this.viewType.next(view); } }
|
1
部分代码: 声明私有资源 Subject
的对象, string
是传递值的类型, 可以不写, 如:
1 2 3 4 5 6 7
| private viewType = new Subject(); private viewType = new Subject<number>(); private viewType = new Subject<boolean>(); private viewType = new Subject<any>();
|
2
部分代码: 声明对外公共的可观察对象, 关键词是: asObservable
- 作为可观察对象
3
部分代码: 创建通讯广播的方法, 括号内是你要广播的值, 在这里我们是 header
点击的菜单, next
很重要, 是把这个 可观察对象
向订阅它的组件中扩散信息, 订阅之后会讲就是我们说的接收.
2.把服务注入到供应商中, 这个随你爱好, 注入在父组件的 providers
中也行, 注入在总入口模块 module
的 providers
中也行, 注入在这两个总入口中的话, 子组件就不需要单独注入了, 只负责在构造函数 constructor
实例化该服务即可.
1 2 3
| providers: [ HeaderService ]
|
3.我们开始在 HeaderComponent
传递了, 情景是点击 header
中的菜单时, 广播发散信息到订阅该值的地方:
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 29 30
| import { Component } from '@angular/core'; import { HeaderService } from "../../../services/components/common/header/header.service"; @Component({ moduleId: module.id, selector: 'header', templateUrl: 'header.component.html', styleUrls: ['header.component.css'] }) export class HeaderComponent { constructor(private headerService: HeaderService) {} private viewStyles = [ {typeTitle: '默认视图', viewType: 'years'}, {typeTitle: '周视图', viewType: 'weeks'}, {typeTitle: '月视图', viewType: 'months'} ]; private viewStylesSelected = this.viewStyles[0]; private onSelect(view: any): void { this.viewStylesSelected = view; this.headerService.announceViewType(this.viewStylesSelected.viewType); } }
|
上述代码中, 做了两件事情, 一是实例化了 headerService
服务, 二是在 onSelect
点击菜单时, 调用 headerService
中广播的方法 announceViewType
, 让它利用 next
的性能去传播这个值, 哪里订阅这个值就传播在哪里.
4.传播好了, 那我们就开始在 ContentComponent
中接收, 在这里是用订阅来完成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import { Component, AfterViewInit} from '@angular/core'; import { HeaderService } from "../../services/components/common/header/header.service"; @Component({ moduleId: module.id, selector: 'my-content', templateUrl: 'content.component.html', styleUrls: ['content.component.css'] }) export class ContentComponent implements AfterViewInit { constructor(private headerService: HeaderService) {} ngAfterViewInit(): void { _that.headerService.view$.subscribe((view: string) => { }); } }
|
在 ContentComponent
组件中, 做了这么几件事情:
一是, 我们在构造函数内声明headerService的实例;
二是, 订阅, 关键字是subscribe, 调用headerService的对外的可观察变量, 订阅它即可, 因为我们在服务中已声明该对象为Subject的一个’asObservable’.
三是, 在哪里订阅呢, 在这还涉及一个知识点, 生命周期钩子, 看下图总结:
指令和组件的变更检测与生命周期钩子 |
(作为类方法实现) |
constructor(myService: MyService, …) { … } |
类的构造函数会在所有其它生命周期钩子之前调用。使用它来注入依赖,但是要避免用它做较重的工作。 |
ngOnChanges(changeRecord) { … } |
在输入属性每次变化了之后、开始处理内容或子视图之前被调用。 |
ngOnInit() { … } |
在执行构造函数、初始化输入属性、第一次调用完ngOnChanges之后调用。 |
ngDoCheck() { … } |
每当检查组件或指令的输入属性是否变化时调用。通过它,可以用自定义的检查方式来扩展变更检测逻辑。 |
ngAfterContentInit() { … } |
当组件或指令的内容已经初始化、ngOnInit完成之后调用。 |
ngAfterContentChecked() { … } |
在每次检查完组件或指令的内容之后调用。 |
ngAfterViewInit() { … } |
当组件的视图已经初始化完毕,每次ngAfterContentInit之后被调用。只适用于组件。 |
ngAfterViewChecked() { … } |
每次检查完组件的视图之后调用。只适用于组件。 |
ngOnDestroy() { … } |
在所属实例被销毁前,只调用一次。 |
从上述表格可以看出
执行一次的有: constructor()
, ngOnInit()
, ngAfterContentInit()
, ngAfterViewInit()
, ngOnDestroy()
检查属性输入或变化的有: ngOnChanges()
检查组件视图变化的有, 执行多次: ngAfterContentChecked()
, ngAfterViewChecked()
, ngDoCheck()
在这里我选用的是 ngAfterViewInit()
的钩子. 除了执行多次的几个和 constructor()
, ngOnDestroy()
不建议使用, 其他都可以用来订阅信息, 另外 ngOnChanges()
是监控属性的, 现在的问题不是用属性传递的, 所以在这里也不适用.
以上我们就完成了脱离父与子, 子与子之间用服务来广播值的方法, 为单项传递.
那双向通讯是什么呢?
双向通讯服务传递
其实懂了单项通讯, 双向通讯就是让它们的位置换了一下, 直接改良一下上述三个文件的代码, 看一下就可以理解了
1.headerService
服务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import { Injectable } from '@angular/core'; import { Subject } from 'rxjs/Subject'; @Injectable() export class HeaderService { private viewType = new Subject<string>(); private viewTypeCurrent = new Subject<string>(); view$ = this.viewType.asObservable(); viewCurrent$ = this.viewTypeCurrent.asObservable(); announceViewType(view: string): void { this.viewType.next(view); } announceViewTypeCurrent(view: string): void { this.viewTypeCurrent.next(view); } }
|
上述服务又生成一个 viewCurrent$
可订阅的对象
2.ContentComponent
广播信息
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 29 30 31 32
| import { Component, OnInit, AfterViewInit} from '@angular/core'; import { HeaderService } from "../../services/components/common/header/header.service"; @Component({ moduleId: module.id, selector: 'my-content', templateUrl: 'content.component.html', styleUrls: ['content.component.css'] }) export class ContentComponent implements OnInit, AfterViewInit { constructor(private headerService: HeaderService) {} private currentView: string = 'years'; ngOnInit(): void { let _that = this; _that.headerService.announceViewTypeCurrent(_that.currentView); } ngAfterViewInit(): void { _that.headerService.view$.subscribe((view: string) => { }); } }
|
上述组件是在 ngOnInit()
的钩子中做了服务的广播, 因为我们进入该页面的时候, header
的菜单始终是以默认年
视图展示, 避免其他页面会影响该默认值, 所以我选择了在 ngOnInit()
初始页面时传递给 header
默认的值. 大家可根据个人情景而定.
3.HeaderComponent
订阅接收信息
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 29 30 31 32 33 34 35 36 37 38 39 40
| import { Component, OnInit } from '@angular/core'; import { HeaderService } from "../../../services/components/common/header/header.service"; @Component({ moduleId: module.id, selector: 'header', templateUrl: 'header.component.html', styleUrls: ['header.component.css'] }) export class HeaderComponent implements OnInit { constructor(private headerService: HeaderService) {} private viewStyles = [ {typeTitle: '默认视图', viewType: 'years'}, {typeTitle: '周视图', viewType: 'weeks'}, {typeTitle: '月视图', viewType: 'months'} ]; private viewStylesSelected = this.viewStyles[0]; ngOnInit(): void { let _that = this; _that.headerService.viewCurrent$.subscribe((view: string) => { if (view === 'years') { _that.viewStylesSelected = _that.viewStyles[0]; } }); } private onSelect(view: any): void { this.viewStylesSelected = view; this.headerService.announceViewType(this.viewStylesSelected.viewType); } }
|
上述代码中, 我也是选择用 ngOnInit()
的钩子去订阅 ContentComponent
传送来的值.
至此, 问题已经完美解决了, 大家可按照自己的情景去按照这个步骤去传递:
1.在服务声明可观察对象, 用到的是 `Subject`的 `asObservable()`
2.一个负责广播讯息, 用到的是 `next()`
3.一个负责订阅, 用到的是 `subscribe()`
其他服务通讯
这里解决了由 <router-outlet></router-outlet>
截断的组件通讯, 用服务通讯传递完成了.
其他如果是建立在父与子的通讯之间, 可尝试运用属性绑定, 或者 EventEmitter
由子组件向上弹射(即父组件接收).
AngularJs2 服务通讯的方法有:
使用输入型绑定,把数据从父组件传到子组件
通过setter拦截输入属性值的变化
使用ngOnChanges拦截输入属性值的变化
父组件监听子组件的事件
父组件与子组件通过本地变量local variable互动
父组件调用ViewChild
父组件和子组件通过服务来通讯, 这个是这篇文章阐述的.
肯定会出现的问题提前知悉与解决
一般只要你订阅
了, 就不能退订了, 而且它会记录你多次订阅的信息, 如果一个组件重复被使用, 接收同样的订阅信息, 就会被叠加, 并不是这个组件内的问题, 而是服务的问题, 服务是单独的服务, 它会记录在 SPA
中所有的轨迹.
所以, 在这里 photoComponent
这个页面可能被其他路由使用, 所以我要保证这个组件始终是最新初始化, 而且没有被记录的新组件,
接下来, 我要做当离开这个页面时, 即当我们路由被转换后, 取消订阅.
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 29 30 31 32 33 34 35 36 37 38 39 40 41
| import { Component, OnInit, AfterViewInit, OnDestroy } from '@angular/core'; import { Subscription } from 'rxjs/Subscription'; import { HeaderService } from "../../services/components/common/header/header.service"; @Component({ moduleId: module.id, selector: 'my-content', templateUrl: 'content.component.html', styleUrls: ['content.component.css'] }) export class ContentComponent implements OnInit, AfterViewInit, OnDestroy { constructor(private headerService: HeaderService) {} subscriptionForHeaderService: Subscription; private currentView: string = 'years'; ngOnInit(): void { let _that = this; _that.headerService.announceViewTypeCurrent(_that.currentView); } ngAfterViewInit(): void { _that.subscriptionForHeaderService = _that.headerService.view$.subscribe((view: string) => { }); } ngOnDestroy(): void { this.subscriptionForHeaderService.unsubscribe(); } }
|
在上述代码中注意的几点是:
1.我用的是 ngOnDestroy()
生命周期钩子, 它代表页面离开时做的操作
2.引入 import { Subscription } from 'rxjs/Subscription';
3.声明 subscriptionForHeaderService: Subscription;
4.赋值 _that.subscriptionForHeaderService = _that.headerService.view$.subscribe((view: string) => {});
5.最后 ngOnDestroy()
取消订阅 this.subscriptionForHeaderService.unsubscribe();
reported on June 7 2017
Communication between two sibling components in angular 2 #2663
Look at the demo by @chennamouli posted
Well Done ~