開発するチャットアプリ(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を新たに追加しています。
