做为一名 Web 开发者,在平常工做中,常常都会遇到消息通讯的场景。好比实现组件间通讯、实现插件间通讯、实现不一样的系统间通讯。那么针对这些场景,咱们应该怎么实现消息通讯呢?本文阿宝哥将带你们一块儿来学习如何优雅的实现消息通讯。javascript
好的,接下来咱们立刻步入正题,这里阿宝哥以一个文章订阅的例子来拉开本文的序幕。小秦与小王是阿宝哥的两个好朋友,他们在阿宝哥的 “全栈修仙之路” 博客中发现了 TS 专题文章,恰好他们近期也打算系统地学习 TS,因此他们就开启了 TS 的学习之旅。html
时间就这样过了半个月,小秦和小王都陆续找到了阿宝哥,说 “全栈修仙之路” 博客上的 TS 文章都差很少学完了,他们有空的时候都会到 “全栈修仙之路” 博客上查看是否有新发的 TS 文章。他们以为这样挺麻烦的,看能不能在阿宝哥发完新的 TS 文章以后,主动通知他们。前端
好友提的建议,阿宝哥怎能拒绝呢?因此阿宝哥分别跟他们说:“我会给博客加个订阅的功能,功能发布后,你填写一下邮箱地址。之后发布新的 TS 文章,系统会及时给你发邮件”。此时新的流程以下图所示:vue
在阿宝哥的一顿 “操做” 以后,博客的订阅功能上线了,阿宝哥第一时间通知了小秦与小王,让他们填写各自的邮箱。以后,每当阿宝哥发布新的 TS 文章,他们就会收到新的邮件通知了。java
阿宝哥是个技术宅,对新的技术也很感兴趣。在遇到 Deno 以后,阿宝哥燃起了学习 Deno 的热情,同时也开启了新的 Deno 专题。在写了几篇 Deno 专题文章以后,两个读者小池和小郭分别联系到我,说他们看到了阿宝哥的 Deno 文章,想跟阿宝哥一块儿学习 Deno。node
在了解他们的状况以后,阿宝哥忽然想到了以前小秦与小王提的建议。所以,又是一顿 “操做” 以后,阿宝哥为了博客增长了专题订阅功能。该功能上线以后,阿宝哥及时联系了小池和小郭,邀请他们订阅 Deno 专题。以后小池和小郭也成为了阿宝哥博客的订阅者。如今的流程变成这样:git
这个例子看起来很简单,但它背后却与一些设计思想和设计模式相关联。所以,接下来阿宝哥将分析以上三个场景与软件开发中一些设计思想和设计模式的关联性。github
在第一个场景中,小秦和小王为了能查看阿宝哥新发的 TS 文章,他们须要不断地访问 “全栈修仙之路” 博客:web
这个场景跟软件开发过程当中的轮询模式相似。早期,不少网站为了实现推送技术,所用的技术都是轮询。轮询是指由浏览器每隔一段时间向服务器发出 HTTP 请求,而后服务器返回最新的数据给客户端。常见的轮询方式分为轮询与长轮询,它们的区别以下图所示:redis
这种传统的模式带来很明显的缺点,即浏览器须要不断的向服务器发出请求,然而 HTTP 请求与响应可能会包含较长的头部,其中真正有效的数据可能只是很小的一部分,因此这样会消耗不少带宽资源。为了解决上述问题 HTML5 定义了 WebSocket 协议,能更好的节省服务器资源和带宽,而且可以更实时地进行通信。
WebSocket 是一种网络传输协议,可在单个 TCP 链接上进行全双工通讯,位于 OSI 模型的应用层。WebSocket 协议在 2011 年由 IETF 标准化为 RFC 6455,后由 RFC 7936 补充规范。
既然已经提到了 OSI(Open System Interconnection Model)模型,这里阿宝哥来分享一张很生动、很形象描述 OSI 模型的示意图:
(图片来源:https://www.networkingsphere....)
WebSocket 使得客户端和服务器之间的数据交换变得更加简单,容许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只须要完成一次握手,二者之间就能够建立持久性的链接,并进行双向数据传输。
介绍完轮询和 WebSocket 的相关内容以后,接下来咱们来看一下 XHR Polling 与 WebSocket 之间的区别:
对于 XHR Polling 与 WebSocket 来讲,它们分别对应了消息通讯的两种模式,即 Pull(拉)模式与 Push(推)模式:
场景一咱们就介绍到这里,对轮询和 WebSocket 感兴趣的小伙伴能够阅读阿宝哥写的 “你不知道的 WebSocket” 这一篇文章。下面咱们来继续分析第二个场景。
在第二个场景中,为了让小秦和小王能及时收到阿宝哥新发布的 TS 文章,阿宝哥给博客增长了订阅功能。这里假设阿宝哥博客一开始只发布 TS 专题的文章。
针对这个场景,咱们能够考虑使用设计模式中观察者模式来实现上述功能。 观察者模式,它定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会通知全部的观察者对象,使得它们可以自动更新本身。
在观察者模式中有两个主要角色:Subject(主题)和 Observer(观察者)。
在第二个场景中,Subject(主题)就是阿宝哥的 TS 专题文章,而观察者就是小秦和小王。因为观察者模式支持简单的广播通讯,当消息更新时,会自动通知全部的观察者。所以对于第二个场景,咱们能够考虑使用观察者设计模式来实现上述的功能。接下来,咱们来继续分析第三个场景。
在第三个场景中,为了让小池和小郭能及时收到阿宝哥新发布的 Deno 文章,阿宝哥给博客增长了专题订阅功能。即支持为阿宝哥博客的订阅者分别推送新发布的 TS 或 Deno 文章。
针对这个场景,咱们能够考虑使用发布订阅模式来实现上述功能。在软件架构中,发布 — 订阅是一种消息范式,消息的发送者(称为发布者)不会将消息直接发送给特定的接收者(称为订阅者)。而是将发布的消息分为不一样的类别,而后分别发送给不一样的订阅者。一样的,订阅者能够表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者存在。
在发布订阅模式中有三个主要角色:Publisher(发布者)、 Channels(通道)和 Subscriber(订阅者)。
在第三个场景中,Publisher(发布者)是阿宝哥,Channels(通道)中 Topic A 和 Topic B 分别对应于 TS 专题和 Deno 专题,而 Subscriber(订阅者)就是小秦、小王、小池和小郭。好的,了解完发布订阅模式,下面咱们来介绍一下它的一些应用场景。
在一些主流的前端框架中,内部也会提供用于模块间或页面间通讯的组件。好比在 Vue 框架中,咱们能够经过 new Vue()
来建立 EventBus 组件。而在 Ionic 3 中咱们可使用 ionic-angular
模块中的 Events 组件来实现模块间或页面间的消息通讯。下面咱们来分别介绍在 Vue 和 Ionic 中如何实现模块/页面间的消息通讯。
在 Vue 中咱们能够经过建立 EventBus 来实现组件间或模块间的消息通讯,使用方式很简单。在下图中包含两个 Vue 组件:Greet 和 Alert 组件。Alert 组件用于显示消息,而 Greet 组件中包含一个按钮,即下图中 ”显示问候消息“ 的按钮。当用户点击按钮时,Greet 组件会经过 EventBus 把消息传递给 Alert 组件,该组件接收到消息后,会调用 alert
方法把收到的消息显示出来。
以上示例对应的代码以下:
main.js
Vue.prototype.$bus = new Vue();
Alert.vue
<script> export default { name: "alert", created() { // 监听alert:message事件 this.$bus.$on("alert:message", msg => { this.showMessage(msg); }); }, methods: { showMessage(msg) { alert(msg); }, }, beforeDestroy: function() { // 组件销毁时,移除alert:message事件监听 this.$bus.$off("alert:message"); } } </script>
Greet.vue
<template> <div> <button @click="greet(message)">显示问候信息</button> </div> </template> <script> export default { name: "Greet", data() { return { message: "你们好,我是阿宝哥", }; }, methods: { greet(msg) { this.$bus.$emit("alert:message", msg); } } }; </script>
在 Ionic 3 项目中,要实现页面间消息通讯很简单。咱们只要经过构造注入的方式注入 ionic-angular
模块中提供的 Events 组件便可。具体的使用示例以下所示:
import { Events } from 'ionic-angular'; // first page (publish an event when a user is created) constructor(public events: Events) {} createUser(user) { console.log('User created!') this.events.publish('user:created', user, Date.now()); } // second page (listen for the user created event after function is called) constructor(public events: Events) { events.subscribe('user:created', (user, time) => { // user and time are the same arguments passed in `events.publish(user, time)` console.log('Welcome', user, 'at', time); }); }
介绍完发布订阅模式在 Vue 和 Ionic 框架中的应用以后,接下来阿宝哥将介绍该模式在微内核架构中是如何实现插件通讯的。
微内核架构(Microkernel Architecture),有时也被称为插件化架构(Plug-in Architecture),是一种面向功能进行拆分的可扩展性架构,一般用于实现基于产品的应用。微内核架构模式容许你将其余应用程序功能做为插件添加到核心应用程序,从而提供可扩展性以及功能分离和隔离。
微内核架构模式包括两种类型的架构组件:核心系统(Core System)和插件模块(Plug-in modules)。应用逻辑被分割为独立的插件模块和核心系统,提供了可扩展性、灵活性、功能隔离和自定义处理逻辑的特性。
<img src="http://cdn.semlinker.com/microkernel-architecture-pattern.png" alt="" style="zoom:60%;" />
对于微内核的核心系统设计来讲,它涉及三个关键技术:插件管理、插件链接和插件通讯,这里咱们重点来分析一下插件通讯。
插件通讯是指插件间的通讯。虽然设计的时候插件间是彻底解耦的,但实际业务运行过程当中,必然会出现某个业务流程须要多个插件协做,这就要求两个插件间进行通讯;因为插件之间没有直接联系,通讯必须经过核心系统,所以核心系统须要提供插件通讯机制。
这种状况和计算机相似,计算机的 CPU、硬盘、内存、网卡是独立设计的配置,但计算机运行过程当中,CPU 和内存、内存和硬盘确定是有通讯的,计算机经过主板上的总线提供了这些组件之间的通讯功能。
下面阿宝哥将以基于微内核架构设计的西瓜播放器为例,介绍它的内部是如何提供插件通讯机制。在西瓜播放器内部,定义了一个 Player
类来建立播放器实例:
let player = new Player({ id: 'mse', url: '//abc.com/**/*.mp4' });
Player
类继承于 Proxy
类,而在 Proxy
类内部会经过构造继承的方式继承 EventEmitter
事件派发器:
import EventEmitter from 'event-emitter' class Proxy { constructor (options) { this._hasStart = false; // 省略大部分代码 EventEmitter(this); } }
因此咱们建立的西瓜播放器也是一个事件派发器,利用它就能够实现插件的通讯。为了让你们可以更好地理解具体的通讯流程,咱们之内置的 poster 插件为例,来看一下它内部如何使用事件派发器。
poster 插件用于在播放器播放音视频前显示海报图,该插件的使用方式以下:
new Player({ el:document.querySelector('#mse'), url: 'video_url', poster: '//abc.com/**/*.png' // 默认值"" });
poster 插件的对应源码以下:
import Player from '../player' let poster = function () { let player = this; let util = Player.util let poster = util.createDom('xg-poster', '', {}, 'xgplayer-poster'); let root = player.root if (player.config.poster) { poster.style.backgroundImage = `url(${player.config.poster})` root.appendChild(poster) } // 监听播放事件,播放时隐藏封面图 function playFunc () { poster.style.display = 'none' } player.on('play', playFunc) // 监听销毁事件,执行清理操做 function destroyFunc () { player.off('play', playFunc) player.off('destroy', destroyFunc) } player.once('destroy', destroyFunc) } Player.install('poster', poster)
(https://github.com/bytedance/...
经过观察源码可知,在注册 poster 插件时,会把播放器实例注入到插件中。以后,在插件内部会使用 player 这个事件派发器来监听播放器的 play
和 destroy
事件。当 poster 插件监听到播放器的 play
事件以后,就会隐藏海报图。而当 poster 插件监听到播放器的 destroy
事件时,就会执行清理操做,好比移除已绑定的事件。
看到这里咱们就已经很清楚了,西瓜播放器内部使用 EventEmitter
来提供插件通讯机制,每一个插件都会注入 player 这个全局的事件派发器,经过它就能够轻松地实现插件间通讯了。
提到 EventEmitter
,相信不少小伙伴对它并不会陌生。在 Node.js 中有一个名为 events
的内置模块,经过它咱们能够方便地实现一个自定义的事件派发器,好比:
const EventEmitter = require('events'); class MyEmitter extends EventEmitter {} const myEmitter = new MyEmitter(); myEmitter.on('event', () => { console.log('你们好,我是阿宝哥!'); }); myEmitter.emit('event');
在前面咱们介绍了发布订阅模式在单个系统中的应用。其实,在平常开发过程当中,咱们也会遇到不一样系统间通讯的问题。接下来阿宝哥将介绍如何利用 Redis 提供的发布与订阅功能实现系统间的通讯,不过在介绍具体应用前,咱们得先熟悉一下 Redis 提供的发布与订阅功能。
Redis 订阅功能
经过 Redis 的 subscribe 命令,咱们能够订阅感兴趣的通道,其语法为:SUBSCRIBE channel [channel …]
。
➜ ~ redis-cli 127.0.0.1:6379> subscribe deno ts Reading messages... (press Ctrl-C to quit) 1) "subscribe" 2) "deno" 3) (integer) 1 1) "subscribe" 2) "ts" 3) (integer) 2
在上述命令中,咱们经过 subscribe
命令订阅了 deno 和 ts 两个通道。接下来咱们新开一个命令行窗口,来测试 Redis 的发布功能。
Redis 发布功能
经过 Redis 的 publish 命令,咱们能够为指定的通道发布消息,其语法为: PUBLISH channel message
。
➜ ~ redis-cli 127.0.0.1:6379> publish ts "pub/sub design mode" (integer) 1
当成功发布消息以后,订阅该通道的客户端就会收到消息,对应的控制台就会输出以下信息:
1) "message" 2) "ts" 3) "pub/sub design mode"
了解完 Redis 的发布与订阅功能,接下来阿宝哥将介绍如何利用 Redis 提供的发布与订阅功能实现不一样系统间的通讯。
这里咱们使用 Node.js 的 Express 框架和 redis 模块来快速搭建不一样的 Web 应用,首先建立一个新的 Web 项目并安装一下相关的依赖:
$ npm init --yes $ npm install express redis
接着建立一个发布者应用:
publisher.js
const redis = require("redis"); const express = require("express"); const publisher = redis.createClient(); const app = express(); app.get("/", (req, res) => { const article = { id: "666", name: "TypeScript实战之发布订阅模式", }; publisher.publish("ts", JSON.stringify(article)); res.send("阿宝哥写了一篇TS文章"); }); app.listen(3005, () => { console.log(`server is listening on PORT 3005`); });
而后分别建立两个订阅者应用:
subscriber-1.js
const redis = require("redis"); const express = require("express"); const subscriber = redis.createClient(); const app = express(); subscriber.on("message", (channel, message) => { console.log("小王收到了阿宝哥的TS文章: " + message); }); subscriber.subscribe("ts"); app.get("/", (req, res) => { res.send("我是阿宝哥的粉丝,小王"); }); app.listen(3006, () => { console.log("server is listening to port 3006"); });
subscriber-2.js
const redis = require("redis"); const express = require("express"); const subscriber = redis.createClient(); // https://dev.to/ganeshmani/implementing-redis-pub-sub-in-node-js-application-12he const app = express(); subscriber.on("message", (channel, message) => { console.log("小秦收到了阿宝哥的TS文章: " + message); }); subscriber.subscribe("ts"); app.get("/", (req, res) => { res.send("我是阿宝哥的粉丝,小秦"); }); app.listen(3007, () => { console.log("server is listening to port 3007"); });
接着分别启动上面的三个应用,当全部应用都成功启动以后,在浏览器中访问 http://localhost:3005/
地址,此时上面的两个订阅者应用对应的终端会分别输出如下信息:
subscriber-1.js
server is listening to port 3006 小王收到了阿宝哥的TS文章: {"id":"666","name":"TypeScript实战之发布订阅模式"}
subscriber-2.js
server is listening to port 3007 小秦收到了阿宝哥的TS文章: {"id":"666","name":"TypeScript实战之发布订阅模式"}
以上示例对应的通讯流程以下图所示:
到这里发布订阅模式的应用场景,已经介绍完了。最后,阿宝哥来介绍一下如何使用 TS 实现一个支持发布与订阅功能的 EventEmitter 组件。
type EventHandler = (...args: any[]) => any; class EventEmitter { private c = new Map<string, EventHandler[]>(); // 订阅指定的主题 subscribe(topic: string, ...handlers: EventHandler[]) { let topics = this.c.get(topic); if (!topics) { this.c.set(topic, topics = []); } topics.push(...handlers); } // 取消订阅指定的主题 unsubscribe(topic: string, handler?: EventHandler): boolean { if (!handler) { return this.c.delete(topic); } const topics = this.c.get(topic); if (!topics) { return false; } const index = topics.indexOf(handler); if (index < 0) { return false; } topics.splice(index, 1); if (topics.length === 0) { this.c.delete(topic); } return true; } // 为指定的主题发布消息 publish(topic: string, ...args: any[]): any[] | null { const topics = this.c.get(topic); if (!topics) { return null; } return topics.map(handler => { try { return handler(...args); } catch (e) { console.error(e); return null; } }); } }
const eventEmitter = new EventEmitter(); eventEmitter.subscribe("ts", (msg) => console.log(`收到订阅的消息:${msg}`) ); eventEmitter.publish("ts", "TypeScript发布订阅模式"); eventEmitter.unsubscribe("ts"); eventEmitter.publish("ts", "TypeScript发布订阅模式");
以上代码成功运行以后,控制台会输出如下信息:
收到订阅的消息:TypeScript发布订阅模式