読者です 読者をやめる 読者になる 読者になる

Reduxのテストファースト開発(第四回 Socket.io サーバーのセットアップ)

React.js Redux テストファースト

前回

uraway.hatenablog.com

Setting Up a Socket.io Server

このアプリケーションは別のブラウザアプリのサーバーとして働く。そのため、クライアントがサーバーと相互通信する方法が必要となる。

自身のあるいは他人のアクションがすぐに反映されるのを見るのは面白く、これはリアルタイム・コミュニケーションの利点と言える。そのため、相互通信にはWebSocketsを使用するが、具体的には、Socket.ioライブラリを用いる。WebSocketsに対応していないクライアントでも、 通信が可能である。

さて、Socket.ioをプロジェクトにインストールしよう。

npm install --save socket.io

次に、server.jsファイルを作成し、Socket.ioサーバーを立てる関数をexportする。

src/server.js

import Server from 'socket.io';

export default function startServer() {
  const io = new Server().attach(8090);
}

ポートの選択は任意だが、後で設定するクライアントからの接続ポートと一致している必要がある。

index.jsファイルでこの関数を呼出すことで、アプリ起動時にサーバーが開始される。

index.js

import makeStore from './src/store';
import startServer from './src/server';

export const store = makeStore();
startServer();

start コマンドをpackage.jsonに追加する。babel-nodeコマンドは以前にインストールした babel-cli パッケージに入っており、NodeコードをBabelトランスパイルし、実行する。

package.json

"scripts": {
  "start": "babel-node index.js",
  "test": "mocha --compilers js:babel-core/register  --require ./test/test_helper.js  --recursive",
  "test:watch": "npm run test -- --watch"
},

これで次のコマンドを打つだけで、サーバーを開始(また、Redux storeを作成)することができる。

npm run start
Broadcasting State from A Redux Listener

Socket.ioサーバーとReudx stateを統合しよう。

サーバーはクライアントに対して現在のstateの状態を教える必要がある。これは、state変更時にSocket.io イベントを発火することで可能になる。

では、どうやってstateの変更を感知するのか?Redux storeをサブスクライブ(subscribe)すれば良い。actionが適応されて、stateが変更された時に、storeが毎回呼び出す関数を提供する。この関数はstore内でstateの変更が完結していることを示すコールバック関数である。

まずはstartServerに対してRedux storeを与える。

index.js

import makeStore from './src/store';
import {startServer} from './src/server';

export const store = makeStore();
startServer(store);

listenerをstoreに対して登録(subscribe)する。listenerはactionが発送された時に呼びだされるコールバック関数であり、stateの変更を管理するために使う。storeによって取得されたstateをプレーンなJavaScriptオブジェクトに変換し、Socket.ioサーバーのstateイベントとして発火する。

src/server.js

import Server from 'socket.io';

export function startServer(store) {
  const io = new Server().attach(8090);

  store.subscribe(
    () => io.emit('state', store.getState().toJS())
  );
}

state変更時にstateのスナップショットを送信するようにした。これに加えて、クライアントがアプリケーションに接続したらすぐに現在のstateを取得できるようにしよう。クライアントサイドのstateと最新のサーバーサイドのstateを同期させる。

Socket.ioサーバーのconnectionイベントをリッスンして、クライアントの接続を受信する。このイベントリスナーにおいて、現在のstateを発する。

src/server.js

import Server from 'socket.io';

export function startServer(store) {
  const io = new Server().attach(8090);

  store.subscribe(
    () => io.emit('state', store.getState().toJS())
  );

  io.on('connection', (socket) => {
    socket.emit('state', store.getState().toJS());
  });

}
Receiving Remote Redux Actions

stateをクライアントに送信するようにした。加えて、クライアントからの更新を受信できるようにしよう。投票者は投票に割り当てられる。ホストはNEXTアクションによって、投票ステージを進める。

これを実施するのは、極めて簡単だ。クライアントがactionイベントをRedux storeに対して発すれば良い。

src/server.js

import Server from 'socket.io';

export function startServer(store) {
  const io = new Server().attach(8090);

  store.subscribe(
    () => io.emit('state', store.getState().toJS())
  );

  io.on('connection', (socket) => {
    socket.emit('state', store.getState().toJS());
    socket.on('action', store.dispatch.bind(store));
  });

}

ここでは、Socket.ioに接続したすべてのクライントがactionを発送することができる。実際のアプリではここで認証メカニズムを導入するべきだろう。

サーバーは現在、基本的には次の操作を扱う。

  • クライアントがサーバーに対してactionを送信する。
  • サーバーがRedux storeに対してそのactionを渡す。
  • storeがreducerを呼び出して、reducerはactionに対応したロジックを実行する。
  • storeがreducerの戻り値に基づいたstateを更新する。
  • storeがサーバーに購読されたリスナー関数を実行する。
  • サーバーがstateイベントを発する。
  • 接続されたすべてのクライアントが更新されたstateを受信する。

サーバーについて終える前に、エントリーを読み込ませてテストしよう。entries.jsonファイルを追加し、Danny Boyleの映画リストをリストする。もちろん好きなモノをリストして良い。

entries.json

[
  "Shallow Grave",
  "Trainspotting",
  "A Life Less Ordinary",
  "The Beach",
  "28 Days Later",
  "Millions",
  "Sunshine",
  "Slumdog Millionaire",
  "127 Hours",
  "Trance",
  "Steve Jobs"
]

index.jsから読み込んで、NEXT actionを発送して、投票を開始する。

index.js

import makeStore from './src/store';
import {startServer} from './src/server';

export const store = makeStore();
startServer(store);

store.dispatch({
  type: 'SET_ENTRIES',
  entries: require('./entries.json')
});
store.dispatch({type: 'NEXT'});

さて、これでクライアントアプリケーションを作成に集中する準備は整った。

次回: 第五回 クライアント・アプリケーション

uraway.hatenablog.com