今回はRails5から追加されたリアルタイム通信機能「Action Cable」を用いた、ライブチャットアプリの実装手順を解説したいと思います。Ajaxの非同期通信と自動更新機能を同時に実装することでもできますが、今回はWebSocket通信を用いてLINEやSlack風のチャットアプリを作成します。
様々な通信システム
Action Cableの実装に取りかかる前に、まずは理解しておいてほしい通信システムを解説します。これらに目を通した上でAction Cableを実装することで、スムーズに理解できるのでぜひ目を通しておきましょう。
- HTTP通信
- Ajax通信
- Websocket通信←Action Cableはここに該当する
HTTP通信

HTTP通信とは、WebブラウザとWebサーバーの間でHTMLや画像ファイルなどのコンテンツの送受信に用いられる通信プロトコルです。Webページを閲覧・利用できるのは、HTTPという仕組みがあるためです。
しかし、HTTP通信ではサーバーに対してリクエスト(例えばページの更新)を送らない限り、クライアントが持っている情報が更新されることはありません。
Ajax通信

Ajax通信とは、JavaScriptを使って非同期でサーバーとやり取りをする通信のことです。ブラウザとサーバの通信とは別に、JavaScriptが通信を行うため、ページ遷移を行わずに情報の更新をすることができます。
しかし、Ajax通信は裏でリクエストを送ってJavaScriptで書き換えているだけのため、リアルタイムなやり取りをするには常に裏でやり取りをしなければなりません。
WebSocket通信

WebSocket通信とは、サーバー側とユーザー側を常時接続状態にしておき、双方向通信ができるようにする技術です。クライアントがリクエストを送信する必要がないため、リアルタイムで情報を更新することができます。
今回実装するAction CableはWebSocketのフレームワークになります。
Action Cableとは
Action Cableとは、Rails5から導入されたリアルタイム通信技術です。これにより、ユーザーはページを更新せずとも最新の情報を取得することができます。
しかしここで1つの疑問点が。
それは、リアルタイム通信技術はAjax通信でも実装できるのではないか?ということです。
結論できます。
しかし先程解説した通り、Ajax通信は裏でリクエストを送ってJavaScriptで書き換えているだけのため、リアルタイムなやり取りをするには常に裏でやり取りをしなければなりません。Ajaxの自動更新機能を実装することで、一見リアルタイムで情報の更新が行われているように見えますが、実際は通信が走っています。
一方でWebsocket通信(Action Cable)を用いるとこれら2つの機能(非同期通信機能と自動更新機能)を同時に実装できることから、簡易なリアルタイムチャットアプリの実装に用いられています。
チャットアプリの概要
今回以下のようなリアルタイムチャットアプリを実装していきます。
次にDHH氏が実装したリアルタイムチャットアプリと今回実装するアプリケーションの違いを見ていきます。異なる点は以下になるので、これらを頭に入れた上で実装に取り掛かりましょう。
- Rails5.0系で開発
- クライアントサイドの実装としてCoffeeScriptを使用
- データベースにSQLiteを使用
- Rails6.0系で開発
- クライアントサイドの実装としてJavaScriptを使用
- データベースにMySQLを使用
チャットアプリの実装手順
では早速、リアルタイムチャットアプリの実装に移ります。DHH氏は20分程で実装しているため、実際にかかる時間は20〜25分前後を想定しています。
アプリケーションの立ち上げ
まずは以下のコマンドを入力し、campfireという名前のアプリケーションを生成しましょう。「–skip-spring」オプションはDHH氏が実装している段階(リリース前のβ版)では必要ですが、現在は必要ないので打たなくても構いません。
% rails new campfire -d mysql --skip-spring
% cd campfire
% rails db:create
- アプリケーション名:campfire
- Rails:6.0.3.2
- Ruby:2.6.5
- DB:MySQL
Roomsコントローラの作成
続いてRoomsコントローラーを作成しましょう。
% rails g controller rooms show
「routes.rb」を編集し、Roomsコントローラーのshowアクションがトップページになるように定義します。
Rails.application.routes.draw do
root to: 'rooms#show'
end
「rails server」を打ち込んだ後「localhost:3000」にアクセスし、以下のようなビューが表示されるか確かめましょう。

Messageモデルの作成
次にtext型のcontentカラムを持ったMessageモデルを作成します。作成後は「rails db:migrate」を行いましょう。
% rails g model message content:text
% rails db:migrate
ビューの編集
続いてはビューの編集です。
まずは投稿されたメッセージを一覧表示するため、Messageの配列をインスタンス変数@messagesに渡しましょう。
class RoomsController < ApplicationController
def show
@messages = Message.all
end
end
部分テンプレートとして、views以下にmessagesフォルダを配置、さらにその配下に「_message.html.erb」を作成します。
<%# views/messages/_message.html.erb %>
<div class="message">
<p><%= message.content %></p>
</div>
「views/rooms/show.html.erb」を以下のように編集し、先ほど作成した部分テンプレートが呼び出されるように記述しましょう。
<h1>Chat room</h1>
<div id ='messages'>
<%= render @messages %>
</div>
「render @messages」は「render partial: ‘messages/message’, collection: @messages」の省略形です。曖昧な方は以下の記事を参考にしてください。

テストデータの登録
次に「rails console」を用いてテストデータを登録します。
% rails c
> Message.create! content: 'Hello world!'
「rails server」でサーバーを立ち上げ「localhost:3000」にアクセスし、先ほど登録したテストデータ(Hello World!)が表示されるか確認しましょう。

フォームの作成
DHH氏の動画では後半にフォームを作成していますが、今回はここで投稿フォームを作成します。
以下のようにviews/rooms/show.html.erbを編集しましょう。
<h1>Chat room</h1>
<div id="messages">
<%= render @messages %>
</div>
<form>
<label>Say something:</label><br>
<input type="text" data-behavior="room_speaker">
</form>
このようなフォームが作成されていれば成功です。

Roomチャネルの作成
ここからAction Cableの設定に入っていきます。
まずは以下のコマンドを打ち込み、speakメソッドを持つRoomチャネルを作成しましょう。以下のような、チャネルの関連ファイルが複数生成されるはずです。
% rails g channel room speak
invoke test_unit
create test/channels/room_channel_test.rb
create app/channels/room_channel.rb
identical app/javascript/channels/index.js
identical app/javascript/channels/consumer.js
create app/javascript/channels/room_channel.js
生成されたファイルの内、「room_channel.rb」がサーバーサイドの処理を担うチャネル、「room_channel.js」がクライアントサイドの処理を担うチャネルになります。
上記2つのファイル以外は編集しないので、無視していただいて構いません。
作成された「room_channel.rb」と「room_channel.js」の中身をそれぞれ見てみると、以下のようになっています。
# app/channels/room_channel.rb
class RoomChannel < ApplicationCable::Channel
def subscribed
# stream_from "some_channel"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def speak
end
end
// app/javascript/channels/room_channel.js
import consumer from "./consumer"
consumer.subscriptions.create("RoomChannel", {
connected() {
// Called when the subscription is ready for use on the server
},
disconnected() {
// Called when the subscription has been terminated by the server
},
received(data) {
// Called when there's incoming data on the websocket for this channel
},
speak: function() {
return this.perform('speak');
}
});
Action Cableの有効化
Action Cableを有効化するため、routes.rbに「mount ActionCable.server => ‘/cable’」を追記しましょう。
Rails.application.routes.draw do
mount ActionCable.server => '/cable'
root to: 'rooms#show'
end
room_channel.rbの編集
続いて、サーバーサイドの処理を受け持つapp/channels/room_channel.rbを編集します。
subscribedメソッドの中に「stream_from “room_channel”」を、speakメソッドに引数「data」と「ActionCable.server.broadcast ‘room_channel’, message: data[‘message’]」の記述を追記しましょう。
class RoomChannel < ApplicationCable::Channel
def subscribed
stream_from "room_channel"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def speak(data)
ActionCable.server.broadcast 'room_channel', message: data['message']
end
end
順番に解説していきます。 まずは以下の記述です。
subscribedは直訳すると「購読する」という意味で、クライアントがサーバーに接続したと同時に実行されるメソッドです。unsubscribedでは反対に、クライアントの接続が解除されたと同時に実行されるメソッドです。
stream_fromメソッドはAction Cableが保持しているメソッドの1つで、データの送受信を行うことができます。今回の場合で言うと、room_channelを宣言することによって、room_channel間(room_channel.rbとroom_channel.js)でデータを送受信することができます。
def subscribed
stream_from "room_channel"
end
次は以下の記述です。
これはroom_channel.jsで実行されたspeakのメッセージを受け取り、room_channel.jsのreceivedメソッドにブロードキャスト(送信)する記述です。
端的に言うと、room_channel.jsのreceivedメソッドにdata[‘message’]を送信しています。
def speak(data)
ActionCable.server.broadcast 'room_channel', message: data['message']
end
room_channel.jsの編集
次にクライアントサイド側の処理を受け持つapp/javascripts/channels/room_channel.jsを編集しましょう。
今回はテキストボックスに文字が入力されエンターキーが押された際、アラートが表示されるように実装します。
mport consumer from "./consumer"
// 「const appRoom =」を追記
const appRoom = consumer.subscriptions.create("RoomChannel", {
// 省略
received(data) {
return alert(data['message']);
},
speak: function(message) {
return this.perform('speak', {message: message});
}
});
window.addEventListener("keypress", function(e) {
if (e.keyCode === 13) {
appRoom.speak(e.target.value);
e.target.value = '';
e.preventDefault();
}
})
まずは以下の記述です。
「e.keyCode === 13」はEnterキーが押下されたことを表し、「appRoom.speak(e.target.value)」でroom_channel.jsのspeakアクションを発火させています。room_channel.jsのspeakアクション発火後、「e.target.value = ”;」「e.preventDefault();」が作動します。
window.addEventListener("keypress", function(e) {
if (e.keyCode === 13) {
appRoom.speak(e.target.value);
e.target.value = '';
e.preventDefault();
}
})
「e.target.value」でテキストボックスに打ち込んだ文字列を取得することができます。実際に取得できているかどうかは、「console.log(e.target.value)」を記述し、コンソール上で確かめてみましょう。
「e.preventDefault」によって、ブラウザが持っているデフォルトの動作を妨げてくれます。詳しくはこちらの記事を参考にしてください。
JavaScriptのpreventDefault()って難しくない?preventDefault()を使うための前提知識
「event.keyCode」で押したキーのキーコードを取得することができます。取得できるイベントはkeyup、keydown、keypressの3つです。キーコード一覧は以下のサイトを参考にしてください。
続いて以下の記述です。
function(message)のmessageが仮引数、実引数は先ほど定義した「e.target.value」になります。そして、room_channel.rbのspeakアクションを動かすために、中でspeak関数を定義しています。加えて、引数messageとして「e.target.value」をroom_channel.rbのspeakアクションに送信しています。
speak: function(message) {
return this.perform('speak', {message: message});
}
最後は以下の記述です。
room_channel.rbでブロードキャスト(送信)されたデータがreceivedに届き、アラート表示を実行しています。アラート表示する内容は「data([‘message’])」ですが、これは「e.target.value」で取得したデータと同じになっています。
received(data) {
return alert(data['message']);
},
挙動確認1
ここで一度挙動を確認しておきます。文字を入力しエンターキーを押すと、アラート表示が出てくるのか確認しましょう。
ここまでのチャネルの流れを図解
どのような流れでアラート表示まで行われているのか、改めて確認しましょう。

- Enterキーを押下することにより、appRoomのspeakアクションが発火
- room_channel.rbのspeakメソッドへデータを送信
- room_channel.jsへデータをブロードキャスト
- データを受け取ったreceivedメソッドでアラート表示を実行
データ保存と保存後の処理の追加
実装の続きに戻ります。
次は、入力したテキストがデータベースに保存されるように実装します。room_channel.rbの記述を以下のように編集しましょう。
def speak(data)
ActionCable.server.broadcast 'room_channel', message: data['message']
end
↓ 以下のように編集
def speak(data)
Message.create! content: data['message']
end
続いて、保存後の処理の記述を追記します。
Messageモデルに以下の記述を追記しましょう。この記述があることで、データ保存後の処理を指定することができます。今回は、データ保存後にMessageBroadcastJobのperformメソッドを実行するように記述します。
class Message < ApplicationRecord
after_create_commit { MessageBroadcastJob.perform_later self }
end
ブロードキャスト処理の追加
先ほどspeakメソッドのブロードキャスト処理を削除してしまったため、別ファイルで定義し直さなければなりません。加えて、データ保存後はMessageBroadcastJobを呼び出すことから、MessageBroadcastJobを新たに作成しなければなりません。
以下のコマンドを打ち込み、ブロードキャストのjobを作成しましょう。作成されたファイルの内、今回使用するのはapp/jobs/message_broadcast_job.rbのみです。
% rails g job MessageBroadcast
invoke test_unit
create test/jobs/message_broadcast_job_test.rb
create app/jobs/message_broadcast_job.rb
作成した「app/jobs/message_broadcast_job.rb」を以下のように編集しましょう。
class MessageBroadcastJob < ApplicationJob
queue_as :default
def perform(message)
ActionCable.server.broadcast 'room_channel', message: render_message(message)
end
private
def render_message(message)
ApplicationController.renderer.render(partial: 'messages/message', locals: { message: message })
end
end
順番に見ていきましょう。
以下の記述でブロードキャスト処理を定義しています。また、render_message(message)を呼び出しています。
def perform(message)
ActionCable.server.broadcast 'room_channel', message: render_message(message)
end
次は以下の記述です。
def render_message(message)
ApplicationController.renderer.render(partial: 'messages/message', locals: { message: message })
end
ここでは部分テンプレートである「app/views/messages/_message.html.erb」を呼び出しています。「app/views/messages/_message.html.erb」の記述は、以下のような形でレンダリングされています。
{message: "<div class='message'><p>投稿したテキスト</p></div>"}
ブラウザ内の記述を書き換える
いよいよ最後の実装です。
最後は打ち込んだテキストが、そのままビューに反映されるように実装します。room_channel.jsのreceivedの記述を以下のように編集しましょう。
received(data) {
const messages = document.getElementById('messages');
messages.insertAdjacentHTML('beforeend', data['message']);
},
引数dataの中身を「console.log(data)」で確認すると、以下のようになっています。ここからmessageのみを取得し、insertAdjacentHTMLメソッドを使用することで、打ち込んだテキストがテキストボックス上に積み重なるように表示させます。

挙動確認2
ここまでで実装が全て終了しました。最後に挙動を確認し、以下と同じになることを確かめましょう。
総合まとめ
最後にまとめとして、ビューが表示されるまでのコードの流れを図解しました。初めは複雑で理解するのに苦労するとは思いますが、何度もアプリケーションを作り直し、理解に落とし込んでいきましょう。



- ユーザーがサーバーを立ち上げ、localhost:3000にアクセスするとsubscribeアクションが発火する。また、stream_fromメソッドによりクライアントサイド側のroom_channel.jsとサーバーサイド側のroom_channel.rbでデータの送受信ができるようになります。
- テキストを入力し、エンターキーを押下するとイベントが発火する。
- 同ファイルのspeakアクションが呼び出される。
- room_channel.rbのspeakメソッドを呼び出し、入力したテキストをデータベースに保存する。
- after_create_commitメソッドにより、データベースに保存後、message_broadcast_job.rbのperformアクションが呼び出される。
- render_message(message)の記述により、private以下の記述が呼び出される。
- 部分テンプレートの記述を呼び出す。
- broadcastを介して、room_channel.jsのreceivedにデータが渡される。
- 渡されたデータのテキスト情報だけを抽出し、ブラウザに表示させる。
参考文献
Railsガイド(Action Cable)
https://railsguides.jp/action_cable_overview.html
Railsガイド(Active Job)
https://railsguides.jp/active_job_basics.html
ActionController::Renderer
https://devdocs.io/rails~5.0/actioncontroller/renderer
DHH氏のYouTube
https://youtu.be/n0WUjGkDFS0
今回はRails5から追加されたリアルタイム通信機能「Action Cable」を用いて、ライブチャットアプリを実装してみました。正直簡単とは言えない実装ですが、何度も作り直してみることで大まかな流れは理解できると思います。ぜひチャレンジしてみてください。