本文来自于华为云云岭团队屈金雄同学的分享,通过对比几种Angular组件间的通信方式,给大家介绍了一种新的Angular组件间通信的解决方案。其中首创了公共的dataService,用于任意组件间通信。 dataService通过angular service特性和注册表的使用,可以实现消息的一收一发,无需再写单独的service逻辑。
背景
一般来讲,Angular 已有的组件间通信方式有哪些?
序号 | 通信方式 | 描述 |
---|---|---|
1 | 输入属性(@Input) | 通过属性绑定,将数据从父组件传递给子组件。 |
2 | 输出属性(@Output) | 通过事件绑定,子组件可以发送事件给父组件,并传递数据。 |
3 | 父子组件直接访问 | 在某些情况下,父组件可以通过 ViewChild 或 ContentChild 装饰器直接访问子组件或模板中的元素。 |
4 | 服务(Service) | 创建共享的服务,组件可以注入该服务来存储和获取数据。 |
5 | RxJS Subject 和 Observable | 使用 RxJS 中的 Subject 和 Observable 来实现组件之间的消息传递。 |
6 | Angular 路由参数 | 通过路由参数在不同组件之间传递数据。 |
7 | NgRx | 使用 NgRx 状态管理库来实现更复杂的组件间通信和数据共享。 |
但以上方法各有局限性,要么代码繁多,要么学习成本高,尤其是跨越多个组件的通信,例如下图中从组件D
到组件G
,用前两种方式就太繁琐了,方式 4 和 5 可能是常用解决方案,但是仍然有些繁琐——需要为每一组消息传递写专有的 service 代码。
组件A
/ \
组件B 组件C
/ \ / \
组件D 组件E 组件F 组件G
本文介绍的通信方式是在方式 4 和 5 的基础上,进行了做了特殊的抽象处理,实现了一个公共的 dataService,最终实现了消息一收一发,无需再写中间环节的代码。
常规方式实现组件间消息通信
为了做对比,这里先介绍一下前文所示的方式4+5
方案,这个方案通过 service 和 rxjs 的 subject 结合使用,实现任意组件间通信。
首先,我们创建一个名为MessageService
的服务,用于在组件之间传递消息:
// message.service.ts
import { Injectable } from '@angular/core';
import { Subject } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class MessageService {
private messageSubject = new Subject<string>();
// Observable string stream
message$ = this.messageSubject.asObservable();
// Service method to send a message
sendMessage(message: string) {
this.messageSubject.next(message);
}
}
接下来,我们有两个组件,SenderComponent
和ReceiverComponent
。SenderComponent
用于发送消息,而ReceiverComponent
用于接收消息。
// sender.component.ts
import { Component } from '@angular/core';
import { MessageService } from './message.service';
@Component({
selector: 'app-sender',
template: `
<input type="text" [(ngModel)]="message" />
<button (click)="sendMessage()">发送消息</button>
`,
})
export class SenderComponent {
message: string;
constructor(private messageService: MessageService) {}
sendMessage() {
this.messageService.sendMessage(this.message);
this.message = ''; // 清空输入框
}
}
// receiver.component.ts
import { Component } from '@angular/core';
import { MessageService } from './message.service';
@Component({
selector: 'app-receiver',
template: ` <div>接收到的消息: {{ receivedMessage }}</div> `,
})
export class ReceiverComponent {
receivedMessage: string;
constructor(private messageService: MessageService) {
this.messageService.message$.subscribe(message => {
this.receivedMessage = message;
});
}
}
在这个示例中,我们通过MessageService
来实现了SenderComponent
向ReceiverComponent
发送消息的功能。MessageService
中使用了 RxJS 的Subject
来创建一个可观察的消息流,然后在SenderComponent
中调用sendMessage
方法来发送消息,而在ReceiverComponent
中使用subscribe
来订阅消息流并接收消息。
请注意,为了使MessageService
成为全局可用的单例服务,我们在@Injectable
装饰器中设置了providedIn: 'root'
。这样一来,MessageService
将成为整个应用程序中所有组件共享的单一实例。
为了使示例正常工作,别忘了将SenderComponent
和ReceiverComponent
添加到所属的模块中,并在模块的模板中放置对应的组件选择器。
这样,SenderComponent
发送的消息将通过MessageService
传递给ReceiverComponent
,并显示在ReceiverComponent
中。这就完成了通过 Service 和 RxJS 的 Subject 实现组件间消息通信的示例。
新的解决方案
理解本方案,默认需要熟悉 Angular 的 service 存储传递数据原理 和 rxjs 的多播用法。
本方案原理是,通过 service 单例的特性(service 在模块内组件间是共享的)和 Subject 的多播特性,实现一个公共的 service,通过公共的 service 实现数据传递。相对的,如同前文中的message.service.ts
文件所示,开发者需要为每一组通信单独创建 service 文件,单独写响应的逻辑。
理解本方案的三个关键点:
- 单例 通过在
@Injectable
装饰器中设置providedIn: 'root'
,service 成为整个应用程序中所有组件共享的单一实例。因为是共享的,所以能作为通信中消息的载体。这是本方案的根本前提。- rxjs 的多播 基础原理是观察者模式(即发布订阅模式)
- 注册表 为了复用 service,简化代码,本方案引入了一个注册表,用来存储对应每个消息事件的 Subject 对象。Subject 对象在创建监听时(需要接收消息的地方)创建。
service 代码如下:
// dataService.service.ts
import { Injectable } from '@angular/core';
import { Subject, Subscription } from 'rxjs';
@Injectable({
providedIn: 'root',
})
export class DataService<T> {
// 创建注册表,用于存放监听器
private events = new Map();
/**
* 发送消息
*
* @param {string} event 事件。用于区别不同的监听
* @param {T} value 消息内容
* @returns {void}
* @memberof DataService
*/
sendMessage(event: string, value: T): void {
if (!this.events.has(event)) {
return;
}
this.events.get(event).subject.next(value);
}
/**
* 获取监听器
*
* 监听器其实就是一个rxjs Subject对象,通过订阅来获取数据。
*
* 注意:
* 1.getListener()应该在sendMessage()之前,否则sendMessage()中获取不到监听器,无法发消息
* 2.getListener()应放在ngOnInit()、ngAfterViewInit()等只会执行一次的生命周期函数中
*
* @param {string} event 事件。用于区别不同的监听
* @returns {Subject<T>}
* @memberof DataService
*/
getListener(event: string): Subject<T> {
// 多处监听时会走到此分支
if (this.events.has(event)) {
const current = this.events.get(event);
current.count++;
return current.subject;
}
const listener = {
count: 1, // 该字段用于记录监听(订阅)者个数
subject: new Subject<T>(),
};
/**
* 创建监听器,并将其加入注册表
*
* 所在函数在创建监听(订阅)时调用,监听发生在发送消息之前,所以在监听这里将监听器加入注册表
*/
this.events.set(event, listener);
return listener.subject;
}
/**
* 取消订阅
*
* 必须手动取消订阅。
* 取消时检查监听者个数。如果没有监听者了,就移除监听器。
*
* @param {string} event
* @param {Subscription} subscription
* @returns
* @memberof DataService
*/
cancelSubscription(event: string, subscription: Subscription) {
if (!this.events.has(event)) {
return;
}
const current = this.events.get(event);
current.count--;
if (current.count === 0) {
// 没有监听者了,就移除监听器
this.events.delete(event);
}
subscription.unsubscribe();
}
}
使用时只需引入上面的公共 dataService,然后以如下示例的方式直接调用 api 就行。
创建监听的示例:
// 接收传递过来的消息
receiveChangeMessage;
ngOnInit() {
...
this.subscription = this.dataService.getListener('event_name').subscribe(message => {
this.receiveChangeMessage = message;
});
...
}
ngOnDestroy() {
this.dataService.cancelSubscription('event_name', this.subscription);
}
发送消息的示例:
public onSomethingChange(value: boolean): void {
...
this.dataService.sendMessage('event_name', value);
}
给出一个较完整的使用示例
以下是一个使用上文提供的DataService
实现组件间消息通信的示例:
假设我们有两个组件:SenderComponent
和ReceiverComponent
,它们需要通过DataService
来传递消息。
// sender.component.ts
import { Component } from '@angular/core';
import { DataService } from './data.service';
@Component({
selector: 'app-sender',
template: `
<input type="text" [(ngModel)]="message" />
<button (click)="sendMessage()">发送消息</button>
`,
})
export class SenderComponent {
message: string;
constructor(private dataService: DataService<string>) {}
sendMessage() {
this.dataService.sendMessage('customEvent', this.message);
this.message = ''; // 清空输入框
}
}
// receiver.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { DataService } from './data.service';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-receiver',
template: ` <div>接收到的消息: {{ receivedMessage }}</div> `,
})
export class ReceiverComponent implements OnInit, OnDestroy {
receivedMessage: string;
subscription: Subscription;
constructor(private dataService: DataService<string>) {}
ngOnInit() {
this.subscription = this.dataService.getListener('customEvent').subscribe(message => {
this.receivedMessage = message;
});
}
ngOnDestroy() {
this.dataService.cancelSubscription('customEvent', this.subscription);
}
}
在这个示例中,我们通过DataService
实现了SenderComponent
向ReceiverComponent
发送消息的功能。DataService
的sendMessage
方法用于发送消息,而getListener
方法用于订阅消息,并在接收到消息时更新ReceiverComponent
中的receivedMessage
属性。cancelSubscription
方法用于取消订阅,并在不再有监听者时从注册表中移除监听器。
请确保将SenderComponent
和ReceiverComponent
添加到所属的模块中,并在模块的模板中放置对应的组件选择器。
通过使用DataService
,SenderComponent
发送的消息将传递给ReceiverComponent
,并显示在ReceiverComponent
中。这样,我们成功地实现了组件间消息通信。
实践拓展
后续可以做成插件,以装饰器的形式调用。
export class ReceiveComponent {
@listen('messageEvent')
message;
}
export class SendComponent {
@send('messageEvent')
message;
}
以上就是 屈金雄 同学的分享,如果你也有更多前端技术想与我们交流,欢迎投稿。除此之外,也欢迎你参与到 OpenTiny 开源中来👏,一起共建项目,一起研讨前端技术。
关于 OpenTiny
OpenTiny 是一套企业级组件库解决方案,适配 PC 端 / 移动端等多端,涵盖 Vue2 / Vue3 / Angular 多技术栈,拥有主题配置系统 / 中后台模板 / CLI 命令行等效率提升工具,可帮助开发者高效开发 Web 应用。
核心亮点:
跨端跨框架
:使用 Renderless 无渲染组件设计架构,实现了一套代码同时支持 Vue2 / Vue3,PC / Mobile 端,并支持函数级别的逻辑定制和全模板替换,灵活性好、二次开发能力强。组件丰富
:PC 端有80+组件,移动端有30+组件,包含高频组件 Table、Tree、Select 等,内置虚拟滚动,保证大数据场景下的流畅体验,除了业界常见组件之外,我们还提供了一些独有的特色组件,如:Split 面板分割器、IpAddress IP地址输入框、Calendar 日历、Crop 图片裁切等配置式组件
:组件支持模板式和配置式两种使用方式,适合低代码平台,目前团队已经将 OpenTiny 集成到内部的低代码平台,针对低码平台做了大量优化周边生态齐全
:提供了基于 Angular + TypeScript 的 TinyNG 组件库,提供包含 10+ 实用功能、20+ 典型页面的 TinyPro 中后台模板,提供覆盖前端开发全流程的 TinyCLI 工程化工具,提供强大的在线主题配置平台 TinyTheme
联系我们:
- 官方公众号:
OpenTiny
- OpenTiny 官网:https://opentiny.design/
- OpenTiny 代码仓库:https://github.com/opentiny/
- Vue 组件库:https://github.com/opentiny/tiny-vue (欢迎 Star)
- Angluar组件库:https://github.com/opentiny/ng (欢迎 Star)
- CLI工具:https://github.com/opentiny/tiny-cli (欢迎 Star)
更多视频内容也可以关注OpenTiny社区,B站/抖音/小红书/视频号。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。