フルスタックのreduxで買い物に行く

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







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ライブラリを使用します。 リストごとに独自のルームを作成し、同じルームにいるユーザーに各アクションを送信します。 さらに、サーバー上の各部屋について、このリストの状態とともにストアを保存します。







サーバーとのクライアント同期は次のように行われます。













つまり、アクションが発生するたびに、次のようになります。









基本的に、reduxの世界では、サーバーはhttpを介してredux-thunkredux-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); };
      
      







リストの初期状態を取得する



既に述べたように、クライアントで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); }
      
      





サーバーロジック



サーバー固有のロジックに進み、サポートする必要のあるアクションを説明しましょう。









これらすべてのアクションについても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); };
      
      





このように簡単な方法で、アクションは部屋の残りの顧客に届きます。 私の意見では、フルスタックリデュースのシンプルさはシンプルさ、およびクライアントロジックを再利用してサーバーや他のクライアントの状態を再現する力にあります。







おわりに



少し面倒ですが、私の話は終わりに近づいています。 私の意見では、すでに多くの記事やレッスンが存在すること(いずれにせよ、完全なアプリケーションコードはリポジトリで表示できます )には焦点を当てませんでしたが、より少ない情報に焦点を合わせました。 質問がある場合はコメントしてください。








All Articles