みなさんこんにちは! この記事では、簡単な例を使用して、複数のクライアントとサーバー間でreduxアプリケーションの状態を同期する方法について説明します。これは、リアルタイムアプリケーションを開発するときに役立ちます。

アプリ
例として、次の要件に従って、任意のデバイスからリアルタイムで位置を変更できる買い物リストを作成します。
- 複数のデバイスの位置をリアルタイムで同時に変更および削除する機能
- サーバーのローカルメモリにリストを保存する機能
- 複数のリストを作成してアクセスする機能
最小限の労力で対応するために、すべてのリストにアクセスするためのUIを使用するのではなく、URL内の識別子によってそれらを区別するだけです。
このフォームのアプリケーションは、クライアントの機能とUIの観点から、 有名なtodoリストと大差ないことに気付くかもしれません。 したがって、この記事では、クライアントとサーバーの相互作用、サーバー上の状態の保存と処理について詳しく説明します。
また、次の記事を書く予定です。同じアプリケーションの例で、DynamoDBでredux状態を保存し、AWSのDockerにパッケージ化されたアプリケーションをロールアウトする方法について説明します。
はじめに
開発環境を作成するには、素晴らしいcreate-react-appツールを使用します。 彼とプロトタイプを作成するのは非常に簡単です。彼は生産的な開発に必要なものすべてを準備します:webpack cホットリロード、ファイルの初期セット、jestテスト。 このすべてを個別に構成して、アセンブリプロセスをより詳細に制御することも可能ですが、このアプリケーションではこれは重要ではありません。
アプリケーションの名前を考え出し、create-react-appに引数として渡すことで作成します。
create-react-app deal-on-meal cd deal-on-meal
プロジェクト構造
create-react-app
がプロジェクト構造を作成しましたが、実際に正しく機能するには、。 ./src/index.js
ファイル./src/index.js
エントリポイントであることが必要です。 このプロジェクトではクライアントとサーバーの両方を使用するため、初期構造を次のように変更します。
src └──client └──modules └──components └──index.js └──create-store.js └──socket-client.js └──action-emitter.js └──constants └──socket-endpoint-port.js └──server └──modules └──store └──utils └──bootstrap.js └──connection-handler.js └──server.js └──index.js └── registerServiceWorker.js
package.jsonにnode ./src/server.js
サーバーを起動するコマンドも追加します
クライアントとサーバーの相互作用
Reduxは、アプリケーションの状態を、あらゆるタイプのjavascriptオブジェクトとして、いわゆるストアに保存します。 この場合、状態の変更はリデューサーによって実行する必要があります-純粋な関数、現在の状態およびアクション(javascriptオブジェクト)が入力されます。 彼女は変更された新しい状態を返します。
ブラウザ側とnode.js環境の両方で、reducerが実装する買い物リストに顧客ロジックを使用できます。 したがって、クライアントに関係なくリストの状態を保存し、データベースに保存できます。
サーバーを操作するには、長い間socket.io
ための標準になっているsocket.ioライブラリを使用します。 リストごとに独自のルームを作成し、同じルームにいるユーザーに各アクションを送信します。 さらに、サーバー上の各部屋について、このリストの状態とともにストアを保存します。
サーバーとのクライアント同期は次のように行われます。

つまり、アクションが発生するたびに、次のようになります。
- 顧客はそれを自分の店に通します
- ミドルウェアを介してサーバーに転送されます
- ページURLによるサーバーは、アクションがどの部屋から来たかを理解し、対応するストアを通過します
- また、このアクションをこの部屋のすべてのクライアントにブロードキャストします
基本的に、reduxの世界では、サーバーはhttpを介してredux-thunkやredux-sagaなどのライブラリを介してやり取りします。 サーバーとのこの種の接続は必要ありませんが、クライアントでredux-saga
も使用します。ただし、1つのタスクのみです。URLに識別子がない場合は、作成したばかりのリストにリダイレクトします。
クライアントコードを書く
reduxが機能するために必要な初期化には焦点を当てません。 公式のreduxドキュメントに詳しく説明されています。2つのミドルウェアを登録する必要があるとしか言えません:前述のredux-saga
パッケージとemitterMiddleware
すでに述べたように、最初のものはリダイレクトに必要で、最後のものはsocket.io-client
介してアクションをサーバーと同期させるために書きsocket.io-client
。
クライアントとサーバー間の状態の同期
ファイル./src/client/action-emitter.js
を作成します。このファイルには、言及されているemitterMiddleware
実装が含まれます。
export const syncSocketClientWithStore = (socket, store) => { socket.on('action', action => store.dispatch({ ...action, emitterExternal: true })); }; export const createEmitterMiddleware = socket => store => next => action => { if(!action.emitterExternal) { socket.emit('action', action); } return next(action); };
createEmitterMiddleware
はミドルウェアのファクトリーであり、外部に渡されるソケットへのリンクを保存するために必要です。 また、もう1つ微妙な違いがあります。外部からのアクションをサーバーに送信する必要はありません。 これを行うには、それらをマークすることをお勧めします(この場合は、 emitterExternal
フィールドを使用します)。このようなアクションの場合、ミドルウェアは何もすべきではありません。 アクションデコレータを使用することは可能ですが、この必要性はわかりません。syncSocketClientWithStore
完全にシンプルです。ソケットでaction
メッセージをリッスンし、受信したアクションをストアに渡すだけで、既に説明したフラグでフラグを立てます。
リストの初期状態を取得する
既に述べたように、クライアントでredux-saga
を使用するため、最初の呼び出しでクライアントの場所が作成されたばかりのリストにredux-saga
されます。 ./src/client/modules/products-list/saga/index.js
方法で、。 ./src/client/modules/products-list/saga/index.js
saga ./src/client/modules/products-list/saga/index.js
で、製品のリストとクライアントが置かれている部屋に応答するサガを記述します。
import { call, takeLatest } from 'redux-saga/effects' import actionTypes from '../action-types'; export function* onSuccessGenerator(action) { yield call(window.history.replaceState.bind(window.history), {}, '', `/${action.roomId}`); } export default function* () { yield takeLatest(actionTypes.FETCH_PRODUCTS_SUCCESS, onSuccessGenerator); }
サーバー
サーバーのエントリポイントがpackage.json ./src/server.js
スクリプトに追加されます。
require('babel-register')({ presets: ['env', 'react'], plugins: ['transform-object-rest-spread', 'transform-regenerator'] }); require('babel-polyfill'); const port = require('./constants/socket-endpoint-port').default; const clientReducer = require('./client').rootReducer; require('./server/bootstrap').start({ clientReducer, port });
サーバーの開始時にクライアントレデューサーがサーバーに転送されることに注意してください。これは、サーバーがリストの現在の状態を維持し、状態全体ではなくアクションのみを受信できるようにするために必要です。 ./src/server/bootstrap.js
見て./src/server/bootstrap.js
:
import createSocketServer from 'socket.io'; import connectionHandler from './connection-handler'; import createStore from './store'; export const start = ({ clientReducer, port }) => { const socketServer = createSocketServer(port); const store = createStore({ socketNamespace: socketServer.of('/'), clientReducer }); socketServer.on('connection', connectionHandler(store)); console.log('listening on:', port); }
サーバーロジック
サーバー固有のロジックに進み、サポートする必要のあるアクションを説明しましょう。
- ルームへのユーザーの追加
- そのようなルームがない場合にユーザーを追加するときにルームとそれに対応するストアを作成する
- ルームからユーザーを削除する
- ユーザーが残っていない場合の保管室の削除
- comeアクションによって部屋の状態を変更します
- 部屋のすべてのユーザーにアクションを中継
これらすべてのアクションについてもreduxで説明し、そのために対応するサガとリデューサーを含むモジュール./src/server/modules/room-service
を作成することを提案します。 そこで、屋内ストア./src/server/modules/room-service/data/in-memory.js
最も単純なストレージを作成します。
export default class InMemoryStorage { constructor() { this.innerStorage = {}; } getRoom(roomId) { return this.innerStorage[roomId]; } saveRoom(roomId, state) { this.innerStorage[roomId] = state; } deleteRoom(roomId) { delete this.innerStorage[roomId]; } }
サーバーとクライアントの状態の同期
ソケットサーバーイベントでは、サーバーストアのroom-service
モジュールからの適切なアクションでディスパッチを行います。 これを./src/server/connection-handler.js
説明し./src/server/connection-handler.js
。
import { actions as roomActions } from './modules/room-service'; import templateParseUrl from './utils/template-parse-url'; const getRoomId = socket => templateParseUrl('/list/{roomId}', socket.handshake.headers.referer).roomId.toString() || socket.id.toString().slice(1, 6); export default store => socket => { const roomId = getRoomId(socket); store.dispatch(roomActions.userJoin({ roomId, socketId: socket.id })); socket.on('action', action => store.dispatch(roomActions.dispatchClientAction({ roomId, clientAction: action, socketId: socket.id }))); socket.on('disconnect', () => store.dispatch(roomActions.userLeft({ roomId }))); };
userJoin
とuserLeft
は、リポジトリを調べるのが面倒ではなかった好奇心reader userLeft
な読者の良心に任せましょう。 userJoin
、 userLeft
方法を見ていきます。 覚えているとおり、2つのアクションが必要です。
- 対応するストアの状態を変更する
- ルーム内のすべてのクライアントにこのアクションを送信します
ジェネレーター./src/server/modules/room-service/saga/dispatch-to-room.js
は最初のものを担当します。
import { call, put } from 'redux-saga/effects'; import actions from '../actions'; import storage from '../data'; const getRoom = storage.getRoom.bind(storage); export default function* ({ socketServer, clientReducer }, action) { const storage = yield call(getRoom, action.roomId); yield call(storage.store.dispatch.bind(storage.store), action.clientAction); yield put(actions.emitClientAction({ roomId: action.roomId, clientAction: action.clientAction, socketId: action.socketId })); };
room-service
モジュールの次のアクションであるemitClientAction
、./ emitClientAction
room-service
/ emitClientAction
room-service
./src/server/modules/room-service/saga/emit-action.js
反応します:
import { call, select } from 'redux-saga/effects'; export default function* ({ socketNamespace }, action) { const socket = socketNamespace.connected[action.socketId]; const roomEmitter = yield call(socket.to.bind(socket), action.roomId); yield call(roomEmitter.emit.bind(roomEmitter), 'action', action.clientAction); };
このように簡単な方法で、アクションは部屋の残りの顧客に届きます。 私の意見では、フルスタックリデュースのシンプルさはシンプルさ、およびクライアントロジックを再利用してサーバーや他のクライアントの状態を再現する力にあります。
おわりに
少し面倒ですが、私の話は終わりに近づいています。 私の意見では、すでに多くの記事やレッスンが存在すること(いずれにせよ、完全なアプリケーションコードはリポジトリで表示できます )には焦点を当てませんでしたが、より少ない情報に焦点を合わせました。 質問がある場合はコメントしてください。