開発するチャットアプリ(2)
チャット用チャネルの作成
いよいよチャネルの作成です。Rails 5より新たに追加されたジェネレートコマンドである、rails g channel
を使って作成します。第1引数には、チャネル名を指定します。ここでは、コントローラー・モデル名と合わせてchat_message
を指定します。第2引数には、チャネルに自動で追加したいメソッドを指定します。ここでは発言する意味のspeak
メソッドとするため、speak
を指定します。
bin/rails g channel chat_message speak
↓
Running via Spring preloader in process 18493 create app/channels/chat_message_channel.rb identical app/assets/javascripts/cable.js create app/assets/javascripts/channels/chat_message.coffee
実行結果から分かる通り、サーバー側のチャネルファイルとクライアント側のJavaScript、CoffeeScriptが自動生成されます。まずは自動生成されたクライアント側のJavaScriptコードを確認しましょう。
…(中略)… //= require action_cable //= require_self //= require_tree ./channels (function() { this.App || (this.App = {}); App.cable = ActionCable.createConsumer(); }).call(this);
「cable.js」は、クライアントからサーバーに対してWebSocket接続しているコードです。App.cable = ActionCable.createConsumer();
でWebSocket通信を確立しています。
次に、自動生成されたCoffeeScriptコードにチャットで「発言する」動作を定義するspeak
メソッドを、リスト2の通りに修正します。
App.chat_message = App.cable.subscriptions.create "ChatMessageChannel", …(中略)… speak: (message) -> @perform 'speak', message: message
App.chat_message
の定義の1行目で、Action Cableのサーバー側のチャネルをcreate
しています。create
の引数にChatMessageChannel
が指定されているので、「app/channels/chat_message_channel.rb」で指定されるサーバー側のチャネルにクライアント側から接続します。
デフォルトのspeak
メソッドを書き換え、引数に指定されたmessage
をサーバー側のチャネルのspeak
メソッドに引数として渡すようにします。@perform 'speak', message: message
と記述すると、ChatMessageChannel
のspeak
メソッドを呼び出すことができます。このCoffeeScriptコードを定義したspeak
メソッドをクライアントから呼び出すには、App.chat_message.speak
(発言メッセージ)とします。これでクライアント側から発言メッセージをサーバー側に送ることができます。
今度はサーバー側のChatMessageChannel
にspeak
メソッドの動作定義を入れます。リスト3の通りにspeak
メソッドを修正しましょう。
…(中略)… class ChatMessageChannel < ApplicationCable::Channel def subscribed stream_from 'chat_message_channel' end …(中略)… def speak(data) ActionCable.server.broadcast 'chat_message_channel', message: data['message'] end end
speak
メソッドはクライアント側で、発言メッセージを指定したキーワード引数を取るように定義しました。サーバー側では引数に指定したdata
経由で、data['message']
とすることで発言メッセージを取り出すことができます。
speak
メソッドのActionCable.server.broadcast
では第1引数にチャネル名を、第2引数に発言メッセージを指定することで、サーバー側からChatMessageChannel
にWebSocketで接続している全クライアントに対して発言メッセージを配信することができます。
なおsubscribed
メソッドは、各クライアントに配信する内容をどこに配信するかを定義しています。この機能はストリームと呼ばれ、Railsが提供するstream_from
メソッドを通じて、発言メッセージを'chat_message_channel'
に接続したクライアントに配信できるようになります。
実はたったこれだけで、/chat_messages/index
にアクセスしてWebSocketを使う最低限の準備が整います。書いたコードは数行なので、Action CableによるWebSocket通信の実現が簡単なことが実感できるのではないでしょうか。この時点でアプリケーションログを確認すると、WebSocket通信が開始されている様子が分かります。
$ cat log/development.log
↓
Started GET "/chat_messages/index" for ::1 at 2017-03-22 18:16:38 +0900 Processing by ChatMessagesController#index as HTML Rendering chat_messages/index.html.erb within layouts/application Rendered chat_messages/index.html.erb within layouts/application (1.3ms) Completed 200 OK in 254ms (Views: 242.2ms | ActiveRecord: 0.0ms) Finished "/cable/" [WebSocket] for ::1 at 2017-03-22 18:16:38 +0900 ChatMessageChannel stopped streaming from chat_message_channel Started GET "/cable" for ::1 at 2017-03-22 18:16:38 +0900 Started GET "/cable/" [WebSocket] for ::1 at 2017-03-22 18:16:38 +0900 Successfully upgraded to WebSocket (REQUEST_METHOD: GET, HTTP_CONNECTION: Upgrade, HTTP_UPGRADE: websocket) ChatMessageChannel is transmitting the subscription confirmation ChatMessageChannel is streaming from chat_message_channel
チャネルクラスであるChatMessageChannel
の上記のようなログが出力されていることが確認できれば、WebSocket通信が成功しています。ただ、現段階で/chat_messages/index
にアクセスしても、発言メッセージを投稿する入力フォームがありませんので作成しましょう。
入力フォームの作成
発言を入力するフォームをリスト4の通りに追加します。
<div id='chat_messages'> </div> <br/> <form> <label> 発言する: <input type="text" data-behavior="speak_chat_messages"> </label> </form>
div
要素のid
がchat_messages
となっている部分には、発言した内容が入ります。現状、まだ発言メッセージをDBに保存していないので空のままにしておきます。また、エンターキーを押下すると発言メッセージがサーバー側に送信される動作定義を後で入れるため、data-behavior
属性を定義しておきます。なお、data-behavior
属性はhtml5の独自データ属性で、アプリケーションの動作とCSSの分離のために使います。
次に、発言する入力フォームでエンターキーが入力されたらサーバー側に発言メッセージを送信するCoffeeScriptのコードをリスト5の通りに記述します。
…(中略)… speak: (message) -> @perform 'speak', message: message $(document).on 'keypress', '[data-behavior~=speak_chat_messages]', (event) -> if event.keyCode is 13 App.chat_message.speak event.target.value event.target.value = '' event.preventDefault()
keypress
イベントは、ブラウザ上でキーボードからの入力を拾います。data-behavior
属性がspeak_chat_messages
の場合としているので、先ほど指定した入力フォームでのキーボード入力イベントを拾っています。
引数に指定されたevent
を使ってevent.keyCode
プロパティにアクセスすることで、キーボードの入力文字に対応した値を取得できます。keyCode
が13
(エンターキー)の場合に、サーバー側に発言メッセージを送信する処理を呼び出します。発言メッセージを送信した後は入力フォームを空にし、最後にpreventDefault()
でイベントをキャンセルすることで、エンターキーを入力してもフォームが送信されないようにしておきます。
最後に、サーバー側チャネルのspeak
メソッドでブロードキャスト(broadcast
)された発言メッセージを、クライアント側で受け取ってブラウザ上に表示する実装を追加します。
App.chat_message = App.cable.subscriptions.create "ChatMessageChannel", …(中略)… received: (data) -> $('#chat_messages').append '<div>' + data['message'] + '</div>' …(中略)…
サーバー側のチャネルからブロードキャストされた発言メッセージをクライアント側で受け取る処理は、received
メソッドに記述します。サーバー側から送られてきたデータを引数data
で受け取ります。発言メッセージはdata['message']
で取り出すことができます。事前にビューファイルで指定したid=chat_messages
のdiv
要素に、発言メッセージのdiv
を新たに追加しています。