Как элегантно реализовать обмен сообщениями?

внешний интерфейс Шаблоны проектирования
Как элегантно реализовать обмен сообщениями?

1. Предпосылки

Как веб-разработчик, в своей повседневной работе мы часто сталкиваемся со сценариями обмена сообщениями. Например, для реализации связи между компонентами, для реализации связи между подключаемыми модулями и для реализации связи между различными системами. Итак, как нам реализовать обмен сообщениями для этих сценариев? В этой статье Brother A Bao расскажет вам, как элегантно реализовать обмен сообщениями.

Хорошо, давайте сразу к делу. Здесь брат Абао начинает эту статью с примера подписки на статьи. Сяо Цинь и Сяо Ван — два хороших друга брата А Бао.Путь к комплексному бессмертному совершенствованию” Они нашли в блоге статью о TS и просто планировали систематически изучать TS в ближайшем будущем, поэтому они начали изучать TS.

Спустя полмесяца Сяо Цинь и Сяо Ван один за другим нашли брата А Бао, заявив, что статьи ТС в блоге «Дорога совершенствования полного стека» почти закончены, и они будут там, когда освободятся.Путь к комплексному бессмертному совершенствованию" Проверьте блог, чтобы узнать, есть ли новые статьи о TS. Они думают, что это очень хлопотно, и посмотрите, могут ли они взять на себя инициативу, чтобы уведомить их после того, как брат А Бао опубликовал новые статьи TS.

Как мог брат А Бао отказаться от предложения друга? Поэтому брат А Бао сказал им отдельно: «Я добавлю в блог функцию подписки. После того, как функция будет выпущена, вы можете заполнить свой адрес электронной почты. Когда в будущем будут опубликованы новые статьи TS, система отправит вам электронное письмо. во время." На этом этапе новый процесс показан на следующем рисунке:

После «операции» брата А. Бао была запущена функция подписки на блог.Брат А. Бао немедленно уведомил Сяо Цинь и Сяо Вана и попросил их заполнить соответствующие адреса электронной почты. После этого они будут получать новое уведомление по электронной почте каждый раз, когда A Baoge публикует новую статью TS.

Брат А Бао — технарь, а также очень интересуется новыми технологиями. при встречеDenoПосле этого брат Абао воспылал энтузиазмом к изучению Дено, а также начал новую тему Дено. После написания нескольких статей о Дено со мной связались два читателя, Сяо Чи и Сяо Го, соответственно, и сказали, что они видели статью брата А Бао о Дено и хотят изучать Дено с братом А Бао.

Узнав об их положении, брат Абао внезапно подумал о предложениях, сделанных ранее Сяо Цинь и Сяо Ван. Поэтому после очередной «операции» Брат Абао добавил специальную функцию подписки на блог. После того, как эта функция была запущена, брат Абао вовремя связался с Сяочи и Сяогуо и предложил им подписаться на тему Deno. После этого Сяочи и Сяо Го также стали подписчиками блога Абаогэ. Текущий процесс выглядит следующим образом:

Этот пример выглядит просто, но за ним скрываются некоторые дизайнерские идеи и шаблоны проектирования. Поэтому A Baoge проанализирует взаимосвязь между тремя вышеуказанными сценариями и некоторыми дизайнерскими идеями и шаблонами проектирования в разработке программного обеспечения.

2. Сценарии и режимы

2.1 Режим опроса сообщений

В первой сцене Сяо Цинь и Сяо Ван должны постоянно посещать блог «Дорога полного стека», чтобы иметь возможность просматривать новые статьи брата А Бао:

Этот сценарий похож на шаблон опроса в разработке программного обеспечения. В первые дни многие веб-сайты использовали опросы для реализации технологии push. Опрос означает, что браузер через регулярные промежутки времени отправляет HTTP-запросы на сервер, а затем сервер возвращает последние данные клиенту. Распространенные методы опроса делятся на опрос и длинный опрос, разница между которыми показана на следующем рисунке:

Эта традиционная модель имеет очевидные недостатки,То есть браузеру необходимо постоянно отправлять запросы на сервер, однако HTTP-запросы и ответы могут содержать длинные заголовки, а действительно достоверных данных может быть лишь небольшая часть, поэтому это будет потреблять много ресурсов полосы пропускания.. Чтобы решить вышеупомянутые проблемы, HTML5 определяет протокол WebSocket, который может лучше экономить ресурсы сервера и пропускную способность, а также обеспечивать больше связи в реальном времени.

WebSocket — это сетевой транспортный протокол, который обеспечивает полнодуплексную связь по одному TCP-соединению и находится на прикладном уровне модели OSI. Протокол WebSocket был стандартизирован IETF в 2011 году какRFC 6455, послеRFC 7936Дополнительные спецификации.

Поскольку было упомянутоМодель OSI (модель взаимодействия открытых систем), вот Brother A Bao, чтобы поделиться очень яркой и яркой диаграммой модели OSI:

(Источник изображения:woohoo.networkingsphere.com/2019/07/Я…

WebSocket упрощает обмен данными между клиентом и сервером, позволяя серверу активно передавать данные клиенту.В API WebSocket браузеру и серверу нужно только выполнить рукопожатие, и между ними может быть создано постоянное соединение, и может быть выполнена двусторонняя передача данных.

После представления связанного содержимого опроса и WebSocket давайте посмотрим на разницу между опросом XHR и WebSocket:

Для XHR Polling и WebSocket они соответствуют двум режимам передачи сообщений, а именно режиму Pull и режиму Push:

Мы познакомим вас со сценой здесь.Те, кто интересуется опросами и WebSocket, могут прочитать, что написал Brother A Bao."Чего вы не знаете о WebSocket"эта статья. Продолжим анализ второго сценария.

2.2 Шаблон наблюдателя

Во второй сцене, чтобы Сяо Цинь и Сяо Ван своевременно получали новые статьи TS, выпущенные братом А Бао, брат А Бао добавил в блог функцию подписки. Здесь предполагается, что в блоге Абаоге вначале публикуются только статьи на темы ТС.

Для этого сценария мы можем рассмотреть возможность использования шаблона наблюдателя в шаблоне проектирования для реализации вышеуказанных функций.Режим наблюдателя, определяющий отношение "один ко многим", позволяющее нескольким объектам-наблюдателям одновременно отслеживать объект-субъект.При изменении состояния объекта-субъекта все объекты-наблюдатели будут уведомлены, чтобы их можно было автоматически обновить. Своя.

В шаблоне Observer есть две основные роли: Subject и Observer.

Во второй сцене Субъект — тематическая статья брата А Бао о ТС, а наблюдатели — Сяо Цинь и Сяо Ван. Поскольку шаблон наблюдателя поддерживает простую широковещательную связь, все наблюдатели автоматически уведомляются об обновлении сообщения. Таким образом, для второго сценария мы можем рассмотреть возможность использования шаблона проектирования наблюдателя для достижения вышеуказанных функций. Далее продолжим анализ третьего сценария.

2.3 Модель публикации-подписки

В третьей сцене, чтобы Сяочи и Сяо Го вовремя получали недавно выпущенные статьи Deno от брата Абао, брат Абао добавил в блог специальную функцию подписки. То есть он поддерживает отправку недавно выпущенных статей TS или Deno для подписчиков блога Abaoge соответственно.

Для этого сценария мы можем рассмотреть возможность использования модели публикации-подписки для достижения вышеуказанных функций. В архитектуре программного обеспечения публикация-подписка — это парадигма обмена сообщениями, в которой отправители сообщений (называемые издателями) не отправляют сообщения напрямую конкретным получателям (называемым подписчиками). Вместо этого опубликованные сообщения группируются в разные категории и отправляются разным подписчикам. Аналогичным образом подписчики могут проявлять интерес к одной или нескольким категориям и получать только интересующие их сообщения, не зная, какие существуют издатели.

В модели публикации-подписки есть три основные роли: Publisher (издатель), Channels (канал) и Subscriber (подписчик).

В третьем сценарии издателем является Brother A Bao, тема A и тема B в каналах соответствуют темам TS и темам Deno соответственно, а подписчиками являются Xiao Qin, Xiao Wang, Xiao Chi и Xiao Guo. Хорошо, разобравшись с моделью публикации-подписки, давайте представим некоторые сценарии ее применения.

3. Применение модели публикации-подписки

3.1 Обмен сообщениями между модулями/страницами во внешнем интерфейсе

В некоторых основных интерфейсных средах компоненты для межмодульного или межстраничного взаимодействия также предоставляются внутри. Например, в среде Vue мы можем передатьnew Vue()для создания компонента EventBus. В то время как в Ionic 3 мы можем использоватьionic-angularКомпонент Events в модуле реализует обмен сообщениями между модулями или между страницами. Давайте представим, как реализовать обмен сообщениями между модулями/страницами в Vue и Ionic соответственно.

3.1.1 Vue использует EventBus для обмена сообщениями

Во Vue мы можем добиться обмена сообщениями между компонентами или модулями, создав EventBus, который очень прост в использовании. На диаграмме ниже представлены два компонента Vue: компоненты Greet и Alert. Компонент «Предупреждение» используется для отображения сообщения, а компонент «Приветствие» содержит кнопку, которая на изображении ниже является кнопкой «Показать приветственное сообщение». Когда пользователь нажимает кнопку, компонент Greet передает сообщение компоненту Alert через EventBus.После того, как компонент получает сообщение, он вызывает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 Взаимодействие подключаемых модулей в микроядерной архитектуре

Архитектура микроядра, иногда называемая архитектурой подключаемых модулей, представляет собой функционально-ориентированную расширяемую архитектуру, которая обычно используется для реализации приложений на основе продуктов. Архитектурный шаблон микроядра позволяет добавлять дополнительные функции приложения в виде подключаемых модулей к основному приложению, обеспечивая расширяемость, функциональное разделение и изоляцию.

Архитектурный шаблон микроядра включает в себя два типа архитектурных компонентов: базовая система и подключаемые модули.Логика приложения разделена на независимые подключаемые модули и базовые системы, что обеспечивает масштабируемость, гибкость, функциональную изоляцию и настраиваемые функции логики обработки.

Для проектирования базовой системы микроядра используются три ключевые технологии:Управление подключаемыми модулями, подключение к подключаемым модулям и связь с подключаемыми модулями, здесь мы сосредоточимся на анализе взаимодействий подключаемых модулей.

Подключаемая связь относится к связи между подключаемыми модулями. Несмотря на то, что при проектировании подключаемые модули полностью отделены друг от друга, в реальном бизнес-процессе неизбежно будет бизнес-процесс, требующий взаимодействия нескольких подключаемых модулей, что требует взаимодействия между двумя подключаемыми модулями;Поскольку между плагинами нет прямой связи, связь должна проходить через базовую систему, поэтому базовая система должна обеспечивать механизм связи плагинов..

Эта ситуация аналогична ситуации с компьютером.ЦП компьютера, жесткий диск, память и сетевая карта представляют собой независимо разработанные конфигурации.Однако во время работы компьютера должна быть связь между ЦП и памятью, памятью и жестким диском. , Компьютер обеспечивает их через шину на материнской плате Функции связи между компонентами.

Затем Brother Abao возьмет в качестве примера проигрыватель «арбуз», разработанный на основе архитектуры микроядра, и расскажет, как он обеспечивает встроенный механизм связи внутри. Внутри арбузного игрока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);
  }
}

Таким образом, созданный нами арбузный проигрыватель также является диспетчером событий, который можно использовать для связи с плагинами. Чтобы все лучше понимали конкретный процесс коммуникации, давайте в качестве примера возьмем встроенный плагин для плакатов и посмотрим, как внутри него используется диспетчер событий.

Плагин плаката используется для отображения изображения плаката до того, как проигрыватель воспроизведет аудио и видео.

new Player({
  el:document.querySelector('#mse'),
  url: 'video_url',
  poster: '//abc.com/**/*.png' // 默认值""
});

Соответствующий исходный код плагина плаката выглядит следующим образом:

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)

(GitHub.com/byte dance/small…)

Наблюдая за исходным кодом, мы можем видеть, что когда плагин плаката зарегистрирован, экземпляр игрока будет внедрен в плагин. После этого диспетчер событий плеера будет использоваться внутри плагина для мониторинга состояния плеера.playиdestroyсобытие. Когда плагин плаката слушает игрокаplayПосле мероприятия изображение афиши скрывается. И когда плагин плаката слушаетdestroyПри возникновении события выполняются операции очистки, такие как удаление связанных событий.

Увидев это, нам уже очень ясно, внутреннее использование арбузного игрокаEventEmitterЧтобы обеспечить механизм связи между подключаемыми модулями, каждый подключаемый модуль будет внедрять проигрыватель, глобальный диспетчер событий, с помощью которого можно легко установить связь между подключаемыми модулями.

упомянул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 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.jsExpressрамка иredisмодули для быстрой сборки различных веб-приложений, сначала создайте новый веб-проект и установите следующие зависимости:

$ 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实战之发布订阅模式"}

Поток связи, соответствующий приведенному выше примеру, показан на следующем рисунке:

Здесь представлены сценарии применения режима публикации-подписки. Наконец, Brother Abao расскажет, как использовать 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发布订阅模式

5. Справочные ресурсы