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

nabeliwo note

5万人に1人になる。

Node.jsとWebSocketでチャットを実装するときの話

モチベーション


なんかこうMMO的なゲームをブラウザで作りたいなあなんていう気持ちがありまして。
そんなわけでWebGLのお勉強とかもしてるんですよ最近。そして同時にグラフィック的な部分以外の要素もしっかりやっていこうという思いがあるわけです。
ということで今アメーバピグのクローン的なものを作ってるわけです。

その名もアオミドロピグ。名前を言うのは恥ずかしい。
リポジトリはここです。

AomidroPigg

今チャットを実装しているのですが、実装に関して若干迷うなあーって部分があってそこがある程度自分の中でまとまってきたのでここに書いとこうということで今回の記事になります。

環境


package.jsonのdependenciesはこんな感じです。
全然関係ないのも混ざってますけど察してください。

"dependencies": {
  "body-parser": "~1.13.2",
  "config": "^1.20.1",
  "connect-flash": "^0.1.1",
  "cookie-parser": "~1.3.5",
  "crypto": "0.0.3",
  "debug": "~2.2.0",
  "ect": "^0.5.9",
  "events": "^1.1.0",
  "express": "~4.13.1",
  "express-session": "^1.13.0",
  "jsonwebtoken": "^6.2.0",
  "moment": "^2.13.0",
  "morgan": "~1.6.1",
  "mysql": "^2.10.2",
  "passport": "^0.3.2",
  "passport-local": "^1.0.0",
  "react": "^15.0.2",
  "react-dom": "^15.0.2",
  "serve-favicon": "~2.3.0",
  "socket.io": "^1.4.6",
  "socket.io-client": "^1.4.6",
  "socketio-jwt": "^4.3.4"
},
"devDependencies": {
  "babel-cli": "^6.8.0",
  "babel-eslint": "^6.0.4",
  "babel-plugin-syntax-async-functions": "^6.8.0",
  "babel-plugin-transform-regenerator": "^6.8.0",
  "babel-polyfill": "^6.8.0",
  "babel-preset-es2015": "^6.6.0",
  "babel-preset-react": "^6.5.0",
  "babelify": "^7.3.0",
  "browserify": "^13.0.1",
  "cssnano": "^3.5.2",
  "eslint": "^2.9.0",
  "eslint-config-airbnb": "^9.0.1",
  "eslint-plugin-import": "^1.7.0",
  "eslint-plugin-jsx-a11y": "^1.2.0",
  "eslint-plugin-react": "^5.1.1",
  "postcss-cli": "^2.5.2",
  "postcss-import": "^8.1.2",
  "postcss-nested": "^1.0.0",
  "postcss-simple-vars": "^1.2.0",
  "uglifyify": "^3.0.1",
  "watchify": "^3.7.0"
}

仕様


  • viewはReactを使う
  • 送信したメッセージは全てDBに保存された上で他の端末に表示される
  • メッセージ入力中は「入力中…」の文字を表示させる

悩んだところ


WebSocketの処理とDBの処理のフローどうするか問題

NodeとWebSocketを使ってチャットを作るっていうのは以前にもやったことがあるのですが、そのチャットのメッセージの永続化を今回はやってみたかったのでメッセージをDBに入れる処理が必要になりました。
そうなると、チャット入力欄をsubmitしたときに、DBに投げる処理とWebSocketとして投げる処理の2つの処理が必要になります。
この2つの処理を並列で同時に走らせるべきなのか、それともWebSocketでサーバーに投げてサーバー側でDBに入れてコールバックでクライアントにWebSocketを返すべきなのか、みたいな選択肢で悩みました。
DBの内容と表示されるチャットの内容が整合性保たれないとだめだから2つの処理は繋がりがないといけない気がするけれど、WebSocketはそのリアルタイム性が大事であることを考えるとDBに入れる処理を毎回挟むのはどうなのかなあとか悩んでました。
が、これは結局以下の問題を解決するときに必然的にやり方が決まりました。

まず、チャットの中身はReact Componentの中で、stateとして渡されたmessegeという配列の中身をmapで回して表示しています。
このように回すと、jsxの中でkeyというattributeをつけないと怒られてしまいます。しかし、keyはuniqueでなければならないため、配列を回したときのindexをあてるのは間違っている、ということみたいです。

React.jsの地味だけど重要なkeyについて

そうなってくるとこれはもうkeyはDBに入れたときのメッセージのIDを使う必要があるということになります。
そしてその前提を元に考えるとDBに入れる処理とWebSocketの処理を別にしてしまうとメッセージのIDを取るのが難しくなってしまうため、必然的にWebSocketの処理とDBに入れる処理を直列で繋ぐ必要があることがわかりました。

流れとしては、まずAさんがチャットのメッセージ入力欄からメッセージをsubmitします。
メッセージはWebSocketを使ってサーバーに渡されます。サーバー側でその値をDBに入れて今入れたメッセージのIDを取得しつつコールバックでWebSocketを使ってそのメッセージをBさんに渡します。
Bさんはメッセージを受け取ったらReact Componentの中で配列をmapで回して描画するわけですが、その際のkeyはメッセージのIDをあてます。

これで、悩んでいた部分の1つは解決することができました。

入力中を表示する処理はどうするか問題

facebookメッセンジャー等で見られる、チャットのメッセージ入力欄に文字を入れているときに相手には「入力中…」と表示される処理を実装したいってときに、上で述べたようなDB接続を挟む処理と一緒にやってしまうと、入力中情報はDBとかとは関係なく実装したいので上手くいかないなーってなってました。
なんか上手くやる方法ないかなあーって考えた結果、結局入力中情報を渡す処理と送信したメッセージを渡す処理は別の処理で実装することにしました。

入力欄に入力したときにWebSocketで入力中であることを送信します。
入力中情報に関してはサーバー側は何もせず、そのまま他のクライアントに渡します。受け取ったクライアントは「〇〇が入力中…」というメッセージを表示します。
メッセージをsubmitしたとき、もしくは入力をやめたとき、入力をやめたことを送信します。受け取ったクライアントは「〇〇が入力中…」というメッセージを非表示にします。
入力中の情報表示に関してはこんな感じ?

ということで、入力中ということを表示させる処理と、実際にメッセージを送る処理は別で分けて作っています。

入力中のイベントって一体どうやってハンドルするんだ

「入力中である」なんていうイベントの取り方がいまいち良い方法が思いつかなかった。
focus?いや、focusしてる間は入力中という判断にしてしまうとfocusだけされてる状態で放置してる状況のときも入力中扱いになってしまう…
keydownされたときに発火させる?うーん、keydownイベントは複数起きまくるしなあ…そしてkeydownにした場合に、途中で入力をやめて送信せずに消した場合はどうやってイベントを取るのか…
考えれば考えるほど想定しなければならないことが多いなあと実感する。

結論として、入力欄にfocusしている状態でのkeydownイベントを取得してinputのvalueが存在していたら入力中という情報を送り、2回目以降のkeydownイベントではinputのvalueが空にならない限りは何もしないでスルーする。空になった場合は入力中を非表示にする情報を送り、もしくはsubmitされた場合も入力中を非表示にする情報を送る。あとはblurのときも入力中を非表示にさせる。

こんな感じでカバーできるかな?とは思うけれど、いかがでしょうか。誰か何か意見あったら教えていただけるとありがたいです。

まとめ


こんな感じで悩みに悩みつつただ今Node.jsとWebSocketでチャットを実装している。まだ終わってはいないけれど。
上にあげたリポジトリを見てもらえれば現在の進捗がわかるのでもし興味がある人がいたら見てみてね。

こちらからは以上です。