关于前端:如何优雅的实现消息通信

50次阅读

共计 9933 个字符,预计需要花费 25 分钟才能阅读完成。

一、背景

作为一名 Web 开发者,在日常工作中,常常都会遇到音讯通信的场景。比方实现组件间通信、实现插件间通信、实现不同的零碎间通信。那么针对这些场景,咱们应该怎么实现音讯通信呢?本文阿宝哥将带大家一起来学习如何优雅的实现音讯通信。

好的,接下来咱们马上步入正题,这里阿宝哥以一个文章订阅的例子来拉开本文的尾声。小秦与小王是阿宝哥的两个好敌人,他们在阿宝哥的“全栈修仙之路”博客中发现了 TS 专题文章,刚好他们近期也打算系统地学习 TS,所以他们就开启了 TS 的学习之旅。

工夫就这样过了半个月,小秦和小王都陆续找到了阿宝哥,说“全栈修仙之路”博客上的 TS 文章都差不多学完了,他们有空的时候都会到“全栈修仙之路”博客上查看是否有新发的 TS 文章。他们感觉这样挺麻烦的,看能不能在阿宝哥发完新的 TS 文章之后,被动告诉他们。

好友提的倡议,阿宝哥怎能回绝呢?所以阿宝哥别离跟他们说:“我会给博客加个订阅的性能,性能公布后,你填写一下邮箱地址。当前公布新的 TS 文章,零碎会及时给你发邮件”。此时新的流程如下图所示:

在阿宝哥的一顿“操作”之后,博客的订阅性能上线了,阿宝哥第一工夫告诉了小秦与小王,让他们填写各自的邮箱。之后,每当阿宝哥公布新的 TS 文章,他们就会收到新的邮件告诉了。

阿宝哥是个技术宅,对新的技术也很感兴趣。在遇到 Deno 之后,阿宝哥燃起了学习 Deno 的激情,同时也开启了新的 Deno 专题。在写了几篇 Deno 专题文章之后,两个读者小池和小郭别离分割到我,说他们看到了阿宝哥的 Deno 文章,想跟阿宝哥一起学习 Deno。

在理解他们的状况之后,阿宝哥忽然想到了之前小秦与小王提的倡议。因而,又是一顿“操作”之后,阿宝哥为了博客减少了专题订阅性能。该性能上线之后,阿宝哥及时分割了小池和小郭,邀请他们订阅 Deno 专题。之后小池和小郭也成为了阿宝哥博客的订阅者。当初的流程变成这样:

这个例子看起来很简略,但它背地却与一些设计思维和设计模式相关联。因而,接下来阿宝哥将剖析以上三个场景与软件开发中一些设计思维和设计模式的关联性。

二、场景与模式

2.1 音讯轮询模式

在第一个场景中,小秦和小王为了能查看阿宝哥新发的 TS 文章,他们须要一直地拜访“全栈修仙之路”博客:

这个场景跟软件开发过程中的轮询模式相似。晚期,很多网站为了实现推送技术,所用的技术都是轮询。轮询是指由浏览器每隔一段时间向服务器收回 HTTP 申请,而后服务器返回最新的数据给客户端。常见的轮询形式分为轮询与长轮询,它们的区别如下图所示:

这种传统的模式带来很显著的毛病, 即浏览器须要一直的向服务器发出请求,然而 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” 这一篇文章。上面咱们来持续剖析第二个场景。

2.2 观察者模式

在第二个场景中,为了让小秦和小王能及时收到阿宝哥新公布的 TS 文章,阿宝哥给博客减少了订阅性能。这里假如阿宝哥博客一开始只公布 TS 专题的文章。

针对这个场景,咱们能够思考应用设计模式中观察者模式来实现上述性能。 观察者模式,它定义了一种一对多的关系,让多个观察者对象同时监听某一个主题对象,这个主题对象的状态发生变化时就会告诉所有的观察者对象,使得它们可能自动更新本人。

在观察者模式中有两个次要角色:Subject(主题)和 Observer(观察者)。

在第二个场景中,Subject(主题)就是阿宝哥的 TS 专题文章,而观察者就是小秦和小王。因为观察者模式反对简略的播送通信,当音讯更新时,会主动告诉所有的观察者。因而对于第二个场景,咱们能够思考应用观察者设计模式来实现上述的性能。接下来,咱们来持续剖析第三个场景。

2.3 公布订阅模式

在第三个场景中,为了让小池和小郭能及时收到阿宝哥新公布的 Deno 文章,阿宝哥给博客减少了专题订阅性能。即反对为阿宝哥博客的订阅者别离推送新公布的 TS 或 Deno 文章。

针对这个场景,咱们能够思考应用公布订阅模式来实现上述性能。在软件架构中,公布 — 订阅是一种音讯范式,音讯的发送者(称为发布者)不会将音讯间接发送给特定的接收者(称为订阅者)。而是将公布的音讯分为不同的类别,而后别离发送给不同的订阅者。同样的,订阅者能够表白对一个或多个类别的趣味,只接管感兴趣的音讯,无需理解哪些发布者存在。

在公布订阅模式中有三个次要角色:Publisher(发布者)、Channels(通道)和 Subscriber(订阅者)。

在第三个场景中,Publisher(发布者)是阿宝哥,Channels(通道)中 Topic A 和 Topic B 别离对应于 TS 专题和 Deno 专题,而 Subscriber(订阅者)就是小秦、小王、小池和小郭。好的,理解完公布订阅模式,上面咱们来介绍一下它的一些利用场景。

三、公布订阅模式的利用

3.1 前端框架中模块 / 页面间音讯通信

在一些支流的前端框架中,外部也会提供用于模块间或页面间通信的组件。比方在 Vue 框架中,咱们能够通过 new Vue() 来创立 EventBus 组件。而在 Ionic 3 中咱们能够应用 ionic-angular 模块中的 Events 组件来实现模块间或页面间的音讯通信。上面咱们来别离介绍在 Vue 和 Ionic 中如何实现模块 / 页面间的音讯通信。

3.1.1 Vue 应用 EventBus 进行音讯通信

在 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>
3.1.2 Ionic 应用 Events 组件进行音讯通信

在 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 框架中的利用之后,接下来阿宝哥将介绍该模式在微内核架构中是如何实现插件通信的。

3.2 微内核架构中插件通信

微内核架构(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 这个事件派发器来监听播放器的 playdestroy 事件。当 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');

3.3 基于 Redis 实现不同零碎间通信

在后面咱们介绍了公布订阅模式在单个零碎中的利用。其实,在日常开发过程中,咱们也会遇到不同零碎间通信的问题。接下来阿宝哥将介绍如何利用 Redis 提供的公布与订阅性能实现零碎间的通信,不过在介绍具体利用前,咱们得先相熟一下 Redis 提供的公布与订阅性能。

3.3.1 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 提供的公布与订阅性能实现不同零碎间的通信。

3.3.2 实现不同零碎间的通信

这里咱们应用 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 组件。

四、公布订阅模式实战

4.1 定义 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;
      }
    });
  }
}

4.2 应用示例

const eventEmitter = new EventEmitter();
eventEmitter.subscribe("ts", (msg) => console.log(` 收到订阅的音讯:${msg}`) );

eventEmitter.publish("ts", "TypeScript 公布订阅模式");
eventEmitter.unsubscribe("ts");
eventEmitter.publish("ts", "TypeScript 公布订阅模式");

以上代码胜利运行之后,控制台会输入以下信息:

 收到订阅的音讯:TypeScript 公布订阅模式 

五、参考资源

  • 维基百科 – 公布 / 订阅
  • Ionic 3 – Events
  • implementing-redis-pub-sub-in-node-js-application

六、举荐浏览

  • 了不起的 TypeScript 入门教程
  • 一文读懂 TypeScript 泛型及利用
  • 你不晓得的 WebSocket
  • 你不晓得的 Blob
  • 你不晓得的 WeakMap

正文完
 0