Rails

【Rails6.0】ActionCableを使用したライブチャットアプリを実装する手順を解説

Rails

 

今回はRails5から追加されたリアルタイム通信機能「Action Cable」を用いた、ライブチャットアプリの実装手順を解説したいと思います。Ajaxの非同期通信と自動更新機能を同時に実装することでもできますが、今回はWebSocket通信を用いてLINEやSlack風のチャットアプリを作成します。

これから実装するライブチャットアプリは、Rails生みの親DHH氏が実際に作成したものです。この記事はDHH氏の実装を翻訳+Rails6.0系にアレンジしたものになります。元ネタは以下のYouTube動画をご覧ください。

様々な通信システム

Action Cableの実装に取りかかる前に、まずは理解しておいてほしい通信システムを解説します。これらに目を通した上でAction Cableを実装することで、スムーズに理解できるのでぜひ目を通しておきましょう。

  1. HTTP通信
  2. Ajax通信
  3. Websocket通信←Action Cableはここに該当する

HTTP通信

HTTP通信

HTTP通信とは、WebブラウザとWebサーバーの間でHTMLや画像ファイルなどのコンテンツの送受信に用いられる通信プロトコルです。Webページを閲覧・利用できるのは、HTTPという仕組みがあるためです。

しかし、HTTP通信ではサーバーに対してリクエスト(例えばページの更新)を送らない限り、クライアントが持っている情報が更新されることはありません。

Ajax通信

ajax通信

Ajax通信とは、JavaScriptを使って非同期でサーバーとやり取りをする通信のことです。ブラウザとサーバの通信とは別に、JavaScriptが通信を行うため、ページ遷移を行わずに情報の更新をすることができます。

しかし、Ajax通信は裏でリクエストを送ってJavaScriptで書き換えているだけのため、リアルタイムなやり取りをするには常に裏でやり取りをしなければなりません。

WebSocket通信

WebSocket通信

WebSocket通信とは、サーバー側とユーザー側を常時接続状態にしておき、双方向通信ができるようにする技術です。クライアントがリクエストを送信する必要がないため、リアルタイムで情報を更新することができます。

今回実装するAction CableはWebSocketのフレームワークになります。

Action Cableとは

Action Cableとは、Rails5から導入されたリアルタイム通信技術です。これにより、ユーザーはページを更新せずとも最新の情報を取得することができます。

しかしここで1つの疑問点が。

それは、リアルタイム通信技術はAjax通信でも実装できるのではないか?ということです。

結論できます。

しかし先程解説した通り、Ajax通信は裏でリクエストを送ってJavaScriptで書き換えているだけのため、リアルタイムなやり取りをするには常に裏でやり取りをしなければなりません。Ajaxの自動更新機能を実装することで、一見リアルタイムで情報の更新が行われているように見えますが、実際は通信が走っています。

 

一方でWebsocket通信(Action Cable)を用いるとこれら2つの機能(非同期通信機能と自動更新機能)を同時に実装できることから、簡易なリアルタイムチャットアプリの実装に用いられています。

チャットアプリの概要

今回以下のようなリアルタイムチャットアプリを実装していきます。

Image from Gyazo
上記GIFをクリックすると別タブで見ることができます。

 

次にDHH氏が実装したリアルタイムチャットアプリと今回実装するアプリケーションの違いを見ていきます。異なる点は以下になるので、これらを頭に入れた上で実装に取り掛かりましょう。

DHH氏
  • Rails5.0系で開発
  • クライアントサイドの実装としてCoffeeScriptを使用
  • データベースにSQLiteを使用
今回の実装
  • Rails6.0系で開発
  • クライアントサイドの実装としてJavaScriptを使用
  • データベースにMySQLを使用

チャットアプリの実装手順

では早速、リアルタイムチャットアプリの実装に移ります。DHH氏は20分程で実装しているため、実際にかかる時間は20〜25分前後を想定しています。

改めてですが、DHH氏が実装しているチャットアプリとこれから実装するチャットアプリの仕様が異なるため、実装手順に若干の違いが生じている点に注意してください。

アプリケーションの立ち上げ

まずは以下のコマンドを入力し、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>

テストデータの登録

次に「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
チャネルはAction Cable専用のファイルと思っていただいて構いません。チャネル自体はMVCモデルのコントローラーと役割が似ています。

生成されたファイルの内、「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つです。キーコード一覧は以下のサイトを参考にしてください。

JavaScript逆引き辞典

 

続いて以下の記述です。

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

ここで一度挙動を確認しておきます。文字を入力しエンターキーを押すと、アラート表示が出てくるのか確認しましょう。

Image from Gyazo

ここまでのチャネルの流れを図解

どのような流れでアラート表示まで行われているのか、改めて確認しましょう。

アラート表示までの流れ

 

  1. Enterキーを押下することにより、appRoomのspeakアクションが発火
  2. room_channel.rbのspeakメソッドへデータを送信
  3. room_channel.jsへデータをブロードキャスト
  4. データを受け取った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
「after_create_commit { MessageBroadcastJob.perform_later self }」を直訳すると、「データ保存した後にコミットする {MessageBroadcastJobのperformを遅延実行する 引数はself}」となるね。

ブロードキャスト処理の追加

先ほど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
Active JobはRails4.2から追加された機能で、メール送信や請求書発行などをバックグラウンドで実行することが可能になります。ActiveJobを利用することで、重たい処理(=時間のかかる処理)を非同期的に実行することができるようになります。

 

作成した「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>"}
「ApplicationController.renderer」を使用することで、コントローラーのアクションの制約を受けずに、任意のビューファイルをレンダリングできます。

ブラウザ内の記述を書き換える

いよいよ最後の実装です。

最後は打ち込んだテキストが、そのままビューに反映されるように実装します。room_channel.jsのreceivedの記述を以下のように編集しましょう。

received(data) {
  const messages = document.getElementById('messages');
  messages.insertAdjacentHTML('beforeend', data['message']);
},

 

引数dataの中身を「console.log(data)」で確認すると、以下のようになっています。ここからmessageのみを取得し、insertAdjacentHTMLメソッドを使用することで、打ち込んだテキストがテキストボックス上に積み重なるように表示させます。

console.log(data)の出力結果

挙動確認2

ここまでで実装が全て終了しました。最後に挙動を確認し、以下と同じになることを確かめましょう。

Image from Gyazo

総合まとめ

最後にまとめとして、ビューが表示されるまでのコードの流れを図解しました。初めは複雑で理解するのに苦労するとは思いますが、何度もアプリケーションを作り直し、理解に落とし込んでいきましょう。

ActionCableまとめ
ActionCableまとめ
ActionCableまとめ

 

  1. ユーザーがサーバーを立ち上げ、localhost:3000にアクセスするとsubscribeアクションが発火する。また、stream_fromメソッドによりクライアントサイド側のroom_channel.jsとサーバーサイド側のroom_channel.rbでデータの送受信ができるようになります。
  2. テキストを入力し、エンターキーを押下するとイベントが発火する。
  3. 同ファイルのspeakアクションが呼び出される。
  4. room_channel.rbのspeakメソッドを呼び出し、入力したテキストをデータベースに保存する。
  5. after_create_commitメソッドにより、データベースに保存後、message_broadcast_job.rbのperformアクションが呼び出される。
  6. render_message(message)の記述により、private以下の記述が呼び出される。
  7. 部分テンプレートの記述を呼び出す。
  8. broadcastを介して、room_channel.jsのreceivedにデータが渡される。
  9. 渡されたデータのテキスト情報だけを抽出し、ブラウザに表示させる。

参考文献

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」を用いて、ライブチャットアプリを実装してみました。正直簡単とは言えない実装ですが、何度も作り直してみることで大まかな流れは理解できると思います。ぜひチャレンジしてみてください。

僕の場合はこのアプリケーションを3回作り直してやっと理解できました(笑)