最短で作るActionCableチャットアプリ

# 目次

- 初めに
- WebSocket と ActionCable
- Pub/Sub モデルの説明
- ActionCable の構成要素
- チャットアプリの作成
- 終わりに

# 初めに

ActionCable を説明した記事や閲覧できるソースコードは色々ありますが、だいたい要件に合わせて機能が追加されていたり他の機能も取り込んでいたりして、必要最小限の実装のみ解説している記事がなかなか見つかりませんでした。この記事は ActionCable に必要な最小限の内容に注目しているので応用が効きます。多分これが最短だと思います。

# WebSocket と ActionCable

WebSocketとはHTTP上で双方向リアルタイム通信を実現するためのプロトコルです。これとRailsをシームレスに接続するための技術がActionCableであり、 Rails 5.0 から Rails の一部に含まれます。 ActionCable を利用することで、リアルタイムにメッセージをやりとりするアプリが実装できます。

# Pub / Sub モデル

WebSocket は "Pub / Sub" と呼ばれるメッセージプロトコルに従っています。アプリを作成する上でこの概念を理解しておくと、書くべきコードの理解が進みます。

メールなど一般のメッセージが「送信者」と「受信者」から構成されるのに対し、 WebSocket は「出版者 (publisher)」と「購読者 (subscriber)」及び「チャネル」から構成されます。

- 出版者は特に宛先を指定せず、メッセージをチャネルに配信する。
- 購読者はチャネルを購読する。
- チャネルにメッセージが送信(**ブロードキャスト**)されると、購読者はメッセージを取得できる。

ここで、1つのチャネルは複数の購読者が購読できます。購読者は「チャネルにメッセージがブロードキャストされた時」の処理を定義できるため、リアルタイムな処理を実装てきます。

# ActionCableの構成要素

典型的な ActionCable アプリケーションに必要な実装です。

## コネクション

ファイル: app/channels/application_cable/connection.rb

クライアント (フロント) とサーバーの接続自体を表現します。セッションの値を取得できるので、アカウント認証に使用されます。

## チャネル

リクエストを処理し、レスポンスを返すことを受け持ちます。通常の Rails アプリケーションでいう Controller に対応しますが、 ActionCable では上記 Pub/Sub モデルに対応した記法を使用します。

ファイル: app/channels/application_cable/channel.rb

基底クラスです。このクラスを継承することで ActionCable における Channel として振る舞うことができます。何も追記しないことが多いですが、チャネルが複数あり、共通の処理を実装したい場合はここに書けます。

ファイル: app/channels/application_cable/*_channel.rb

ここにそれぞれの処理を実装します。

## クライアント

ファイル: app/assets/javascripts/cable.js

クライアントに ActionCable ライブラリを読み込ませるためのファイルです。このファイルで window.App というオブジェクトが定義され、クライアントのどこからでも ActionCable のメソッドを使用することができます。最初に生成されたあとは基本的に編集しません。こんな内容です。

```app/assets/javascripts/cable.js
// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the `rails generate channel` command.
//
//= require action_cable
//= require_self
//= require_tree ./channels

(function() {
  this.App || (this.App = {});

  App.cable = ActionCable.createConsumer();

}).call(this);
```

ファイル: app/assets/javascripts/channels/*.js

チャネル関係の「接続した時」「受信した時」「接続を拒否された時」及び「チャネルにブロードキャストする」処理をここに実装します。

# チャットアプリの作成

## ベースになるプロジェクト

実際にRailsアプリケーションを作成しながらActionCable
最初にRailsプロジェクトを新規作成します。

```
$ rails new cable_chat_tmp
```

個人的な趣味ですが、Slimが好きでCoffeeScriptは嫌いなのでそうします。 jQueryは評価が分かれるところですが入れておきます。

```Gemfile
gem 'slim-rails'
gem 'jquery-rails'
gem 'coffee-rails', '~> 4.2' # 削除
```

```app/assets/javascripts/application.js
//= require rails-ujs
//= require turbolinks
//= require jquery
//= require jquery_ujs
//= require_tree .
```

```
$ bundle install
```

ルーティングを定義します。

```config/routes.rb
Rails.application.routes.draw do
  root to: 'top#show'
end
```

```
$ rails g controller top
```

```app/controllers/top_controller.rb
class TopController < ApplicationController
  def show
  end
end
```

ビューを作成します。これで最低限ページを表示できるので、 `rails server` を立ち上げてブラウザで確認します。ここまでは通常のRailsアプリケーションです。

```app/views/top/show.html.slim
h1 Chat
```

チャネルを生成します。専用のコマンドがあるので利用します。

```
$ rails g channel chat post
```

以下のファイルができます。

```app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
  def subscribed
    # stream_from "some_channel"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def post
  end
end
```

```app/assets/javascripts/channels/chat.js
App.chat = App.cable.subscriptions.create("ChatChannel", {
  connected: function() {
    // Called when the subscription is ready for use on the server
  },

  disconnected: function() {
    // Called when the subscription has been terminated by the server
  },

  received: function(data) {
    // Called when there's incoming data on the websocket for this channel
  },

  post: function() {
    return this.perform('post');
  }
});
```

この時点でJavaScriptからActionCableの機能を呼び出すインスタンス `App` が使えます。rails server を起動し、ブラウザで localhost を開き、開発者ツールのコンソールで確認します。

```
> App
<- Object {cable: Consumer, chat: Subscription}
> App.cable
<- Consumer {url: "ws://localhost:3000/cable", subscriptions: Subscriptions, connection: Connection}
```

## 入力の受け取り

ここからは実際に入力を受け取ってコンテンツに反映する処理を実装していきます。
チャットの入力フォームを作成します。この時 data-behavior 属性を必ず指定します。(Railsの `form_tag` メソッドは色々余計なものが付いてくるので使いません)

```app/views/top/show.html.slim
h1 Chat

form
  input#chat-form type="text" data-behavior="chat_post"
```

フォームの入力を受け取る部分を実装します。動作確認のため、処理はアラートを表示するだけにします。

```app/assets/javascripts/channels/chat.js
App.chat = App.cable.subscriptions.create("ChatChannel", {

  // 中略

  post: function(message) {
    window.alert(message);
    return this.perform('post');
  }
}, $(document).on('keypress', '[data-behavior~=chat_post]', function(event) {
  if (event.keyCode === 13) {
    var chatForm = $('#chat-form');
    App.chat.post(chatForm.val());
    return chatForm.val('');
  }
}));

```

`, $(document)...` から先(形式上は `App.cable.subscriptions.create` メソッドの第3引数)がフロントのイベントを受け取る処理です。 `'[data-behavior~=chat_post]'` という見慣れない形式の文字列がありますが、これでイベントを取得するHTML要素を指定します。

ここまでで、フォームに何かしら入力してエンターキーを押すとその内容のアラートが表示されます。

![actioncable_alert.png](https://qiita-image-store.s3.amazonaws.com/0/172883/01e7bb7b-ba00-56d3-e277-dcb9d540d732.png)

## サーバー側実装

入力内容をサーバーに送信する処理を実装します。先ほどのアラートを表示する処理は消して以下の通り書き換えます。

```app/assets/javascripts/channels/chat.js
App.chat = App.cable.subscriptions.create("ChatChannel", {
  // 中略

  post: function(message) {
    return this.perform('post', { message: message });
  }

  // 中略
});
```

`perform` メソッドにより、フロントのデータをサーバーに送信することができます。第1引数にサーバー側のメソッド名、第2引数に送信するデータを指定します。

サーバー側を実装します。メソッド `post` 内でデバッガを使用すると、フォームの内容が取得できていることが分かります。今回は機能がシンプルなため遠回りしている印象を受けますが、実際はここでDBと接続したり外部アプリと連携したりできます。

```app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_from 'chat_channel'
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def post(data)
    ActionCable.server.broadcast('chat_channel', data)
  end
end
```

## ビューに反映

JavaScriptを使用して受け取ったメッセージをビューに反映します。まずは表示部分を作成します。

```app/views/top/show.html.slim
h1 Chat

ul#chat-index
  li Posts

form
  input#chat-form type="text" data-behavior="chat_post"
```

メッセージを受け取った時の処理を実装します。今回は単に `append` するだけです。

```app/assets/javascripts/channels/chat.js
App.chat = App.cable.subscriptions.create("ChatChannel", {
  // 中略

    received: function(data) {
    return $('#chat-index').append('<li>' + data['message'] + '</li>');
  },

  // 中略
});
```

以上で完成です! 動作確認してみましょう。今回DBを経由していないので送信側はリロードが走ってしまいメッセージが消えてしまいますが、複数ウィンドウを開くと想定した動作になっていることが確認できます。

![actioncable_work.png](https://qiita-image-store.s3.amazonaws.com/0/172883/5ac4ca06-017d-5484-46dc-e66018af02da.png)

# 終わりに

今回はActionCableの説明に注目したため、要素を削って説明しました。このためDBとの連携やフロント実装の深いところなどの内容はありませんが、応用は効かせられると思います。

また、今回作成したソースコードはこちらです→ [strviola/cable_chat_tmp](https://github.com/strviola/cable_chat_tmp)