Злоупотребление Redux: Блог 1

Если бы вы использовали одно и то же хранилище Redux на обеих сторонах стека, могли бы вы получить распределенное состояние бесплатно? Это вопрос, на который я собирался ответить. TL; DR; да. Да, можно, и это красиво.

Предыстория: безумная идея

Однажды я написал прототип приложения на React. Это было переписывание того, что я написал год назад. У меня был только прототип интерфейса, но если бы я хотел, чтобы он работал, я бы использовал распределенное состояние в реальном времени (например, приложение для чата). Моя мотивация к сторонним проектам имеет тенденцию быстро угасать, поэтому я хотел что-то, что было бы быстро и легко.

Тогда у меня возникла безумная идея: что, если я просто возьму уже созданное хранилище Redux и использую его на сервере? Тогда я смогу просто передавать действия через сокет, и все должно синхронизироваться!

Конечно, это было немного глупо (Redux на сервере?) И, может быть, немного безумно (бесплатная синхронизация состояния?), Но оказалось фантастическим. Я выполнил работу по подключению одного действия, но вместо того, чтобы работать только с этим действием, все мое приложение начало распространяться по сокетам среди любого количества пользователей, которое я хотел.

Простой. Sync’d. Красивый.

Предварительные требования

Прежде чем читать это, вы захотите познакомиться с Redux и Websockets. Я рекомендую Повышение уровня с Redux Брэда Вестфолла для Redux и Введение в веб-сокеты поверх HTML 5 Rocks для некоторого контекста веб-сокетов.

Цель

Создайте работающее приложение чата, используя React / Redux, сервер узлов и веб-сокеты.

Настройка

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



Обязательно используйте ветку the-beginning, если хотите следовать с самого начала.

Мы начнем с того, что уже создано для нас:

  • Прототип чат-приложения (только для React / Redux).
  • Сервер узла (веб-сервер, использующий Express)
  • Настройка веб-сокетов на клиенте и сервере (мы будем использовать Primus)

План

Мой ленивый и гениальный план? Заставьте Redux делать всю работу за меня, и просто волшебным образом пусть все заработает! Хорошо, я был достаточно глуп, чтобы принять это за чистую монету, но вы, вероятно, не так. Давайте поговорим о том, как мы это сделаем:

FrontEnd :

  • Создать действие (только данные, чистый стиль Redux)
  • Отправьте его через розетку

BackEnd:

  • Получите действие
  • Отправьте в магазин
  • Отправьте его обратно в переднюю часть

FrontEnd:

  • Отправьте действие в магазин переднего плана

Если план сработает, внешний магазин теперь должен быть точно таким же, как и внутренний, и все, что нам нужно было сделать, это отправить действие через сокет, вся тяжелая работа была сделана за нас!

Кажется подозрительным…

У нас есть идея, как отправлять данные, но можем ли мы действительно доверять тому, что магазины будут синхронизироваться друг с другом? Оказывается, хорошо продуманные магазины Redux действительно могут это гарантировать. Вот почему:

If:

  • Каждое действие содержит только данные и
  • Каждый редуктор - это чистая функция старого состояния и действия.

Затем:

  • Продукт старого состояния и одно и то же действие должны приводить к одному и тому же состоянию.

Математически это выглядит так: f (state, action) = newState

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

Исполнение

Хорошо, давайте перейдем к сути этой штуки! Мой подход к этому во внешнем интерфейсе был немного неортодоксальным. Вместо того, чтобы писать промежуточное ПО, я написал оболочку вокруг моего сокета, которая предоставляет один метод: dispatch

Эта функция отправки имеет такую ​​же сигнатуру, что и Redux, за исключением того, что она отправляет действие по сети в хранилище сервера, а не во внешнее хранилище.

/** ui/js/services/socket.js **/
/** dispatch() **/
const dispatch = action => {
  roomConnection.write(action);
};

Вот и все. Три строчки кода. Получение так же просто: мы предпримем любое действие, которое нам было отправлено, и отправим его в магазин переднего плана.

/** ui/js/services/socket.js **/
/** connectToRoom() **/
connection.on('data', action => {
  return store.dispatch(action);
});

Теперь мы можем заменить старую отправку на socketDispatch, и ваш интерфейс будет готов к работе.

/** ui/js/components/chat.jsx **/
import {dispatch as socketDispatch} from '../services/socket';
const mapDispatchToProps = (dispatch) => ({
  sendMessage: (newMessage, userName) => socketDispatch(sendMessage(newMessage, userName)),
  addUser: userName => socketDispatch(addParticipant({id: userName, name: userName})),
  setUserId: userId => dispatch(joinChat({userId})),
});

Примечание: не изменяйте отправку для setUserId. Для этого необходимо выполнить однократную отправку только переднего плана, чтобы установить ваш идентификатор пользователя в магазине переднего плана. Есть способ лучше решить эту проблему, но мы рассмотрим его в следующем блоге.

Получение на стороне обслуживания

Хорошо, теперь на сервере мы должны получать действия через диспетчеризацию сокетов, так что нам с ними делать? Во-первых, нам понадобится хранилище на стороне сервера. Мы собираемся использовать тот же метод createStore, который мы использовали в интерфейсе пользователя.

/** server/room.js **/
/** constructor() **/
import {createStore} from '../shared/store/store';
this.store = createStore();

Видеть? Ничего особенного, мы добавим сюда немного промежуточного программного обеспечения через минуту, но пока выглядит неплохо! Теперь давайте добавим метод отправки в магазин комнаты.

/** server/room.js **/
/** dispatch() **/
dispatch(action){
  this.store.dispatch(action);
}

Наконец, давайте обработаем действия, отправленные нам через веб-сокет, отправив их в комнату.

/** server/room.js **/
/** addUser() **/
addUser(spark){
    this.addSpark(spark);
    spark.on('data', action => {
      this.dispatch(action);
    });
 }

Каждый раз, когда мы получаем данные через сокет, мы просто предполагаем, что это действие, и отправляем его в наш магазин. Если вам это кажется знакомым, то причина в том, что вы написали почти такой же код во внешнем интерфейсе.

Последняя часть

Все, что осталось сейчас, - это взять все, что было отправлено в наш серверный магазин, и перебросить обратно во внешний интерфейс. В этом случае промежуточное ПО является сильным паттерном, потому что мы просто слепо отправляем действие после каждой успешной отправки.

/** server/room.js **/
/** constructor() **/
const sendActionsMiddlware = () => next => action => {
 const result = next(action);
 this.sparks.forEach(spark => spark.write(result));
 return result;
};

Вот разбивка того, что происходит на случай, если вы не знакомы с промежуточным программным обеспечением Redux:

  1. Пусть действие перейдет в магазин next(action)
  2. Отправить действие по сети на каждое соединение this.sparks.forEach(spark.write(result))
  3. Продолжайте с остальной частью промежуточного программного обеспечения return result

Последнее, что нам нужно сделать, это добавить это промежуточное ПО в хранилище нашего сервера. Давайте изменим создание магазина, чтобы оно выглядело так:

/** server/room.js **/
/** constructor() **/
import {applyMiddleware} from 'redux';
this.store = createStore(applyMiddleware(sendActionsMiddlware));

Почувствуйте волшебство

Вот и все! Теперь вы должны иметь возможность открывать несколько вкладок и видеть обновления сообщений в режиме реального времени.

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

Если вы не следовали инструкциям, вы можете увидеть, как выглядит конечный продукт, проверив basic-distributed ветку репозитория github здесь:



Пропущенный шаг

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

Но… но…

О нет, здесь есть множество вопросов, которые мы не рассмотрели! Если бы это был настоящий чат, разве у нас не должно быть более одной комнаты? Разве моему серверу не нужно проводить какую-то проверку? Что, если мне абсолютно необходимо иметь логику, специфичную для сервера или внешнего интерфейса? Почему второй человек не видит людей, уже присутствующих на собрании?

На эти и другие вопросы мы ответим в оставшейся части серии Abusing Redux, так что следите за обновлениями.

Спасибо, что нашли время, чтобы прочитать это, надеюсь, вы нашли это крутым и / или полезным!

Примечания:

Изоморфное мышление

Ключ к тому, чтобы этот шаблон работал, - это действительно рассматривать магазин как изоморфную структуру. Было бы легко создать действия REQUEST и RESPONSE, но кто запрашивает, а кто отвечает? Ну, это зависит от того, передний это магазин или бэкэнд. Мы настолько привыкли добавлять поведение, которое является только серверным или только интерфейсным, что кажется естественным желание сделать это. Это займет некоторое время и много рефакторинга, когда вы поймете, что сделали плохой поступок, просто продолжайте это делать, и вы сможете пожинать плоды.