【Rails 6.0】Action Cable を解説しながらプログレスバーを実装する

# TL;DR
- Action Cable を使用してプッシュ通信のプログレスバーを実装した
- Action Cable について解説する
- `ProgressChannel.broadcast_to(current_user, percent: (index+1) * 100 / pictures.length)`

# 背景
旅行の思い出投稿サイトを作ったときに写真アップロードが遅くて、UX 的にアップロードの進捗状況を表すプログレスバーがあったほうが良いのではないかとなり、Action Cable の勉強も兼ねて実装しました

# モノ
![ezgif-2-3851e5ddc0e8.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531191/3cf7913e-3be2-b5d0-75c9-070605a9e45d.gif)

# Action Cable 解説
Rails に Action Cable という WebSocket と Rails のその他の部分をシームレスに統合するための機能が存在します
Rails 公式なので Rails ガイドを見るのが良いと思ったのですが、Rails ガイドの解説がとても分かりづらかったので自分でまとめ直してみました
参考になれば幸いです

## 図が一番わかり易い
Rails ガイドがわかりづらかった理由の一つが図がなかったことだと思ったので図を作りました
どこか間違ってたら教えてください
![スクリーンショット 2020-08-20 0.18.22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531191/2b18bd5c-4148-533c-f0c6-4108485ddccc.png)

## 用語解説
![スクリーンショット 2020-08-20 0.21.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/531191/86f5d490-be4c-9a9a-1043-5d66f23839e8.png)

Rails ガイドと順番が違うかもしれませんが、一番わかり易いと思う順番にしました

究極的にはパブリッシャからサブスクライバへ通信が行われる pub/sub リンクです
Rails アプリケーションの controller でブロードキャストを行い、チャネルを通して各タブ(サブスクライバ)に配信します

# プログレスバーの実装
プログレスバーを主題に実際の実装を見ていきます

## 実行環境
- ruby:2.6.5 (Docker image)
- Rails 6.0.2

まずは `rails g channel <チャネル名>` でひな形を作成します

```shell
$ rails g channel progress
```

## サーバー側

### チャネル

サーバー側のプログレスチャネルです

```ruby:app/channels/progress_channel.rb
class ProgressChannel < ApplicationCable::Channel
  def subscribed
    stream_for current_user
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end
end
```

`subscribed`と`unsubscribed`のひな形は用意されていて、それぞれサブスクリプションができた/なくなったときの挙動を書きます
一般的には、`stream_from "progress_#{params[:progress_id]}"` のような形で受け取るブロードキャストを指定します
しかし、ここでは `stream_for` を使用し、ユーザーごとに変わるブロードキャストを指定しています
これで、`progress:Z2lkOi8vbWVtb3BpYy9Vc2VyLzE` (ブロードキャスト名) のようなブロードキャストを受信することができます

### コネクション
サーバー側ではこのコネクションクラスをもとに、コネクションインスタンスを作成します

```ruby:app/channels/application_cable/progress_channel.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end

    private
      def find_verified_user
        User.find(session['warden.user.user.key'][0][0])
      rescue
        reject_unauthorized_connection
      end
  
      def session
        @session ||= cookies.encrypted[Rails.application.config.session_options[:key]]
      end
  end
end
```

`identified_by` に指定したものを参考に、サーバーはコネクションを識別できるようになります
このコードでは、`current_user` に入る User オブジェクトをもとに識別しています
アプリケーションで Devise を使用しているので、ここの実装は
https://qiita.com/masuidrive/items/fd13ab8d9410206a1b91
を参考にしました

### パブリッシャ
今回は model のところにブロードキャストするコードを書きます

```ruby:app/models/album_form.rb
pictures.each_with_index do |picture, index|
   album.pictures.new(picture_name: picture)
   ProgressChannel.broadcast_to(
      user,
      percent: (index+1) * 100 / pictures.length
   )
end
```
ここでの `user` は `current_user` に当たります(実装上、変数名が違います)
ここでクライアントにわたす data (`percent: (index+1) * 100 / pictures.length`) を指定してプログレスバーの進めるわけです
単純に枚数で進捗の%を計算しています
このブロードキャストがチャネルの `stream_for current_user` につながり、`current_user` がいるクライアントにデータが送信されます

以上で、サーバー側の実装は終わりです

## クライアント側
好きな場所にプログレスバーを置きます

```erb:app/views/albums/index.html.erb
<div class="progress-percentage text-xs-center">0%</div>
<progress class="progress progress-striped" max="100" value="0"></progress>
```

クライアント側でもコネクションを作成しないといけません

```javascript:app/javascript/channles/consumer.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.

import { createConsumer } from "@rails/actioncable"

export default createConsumer()
```


```javascript:app/javascript/channels/progress_channel.js
import consumer from "./consumer"

consumer.subscriptions.create("ProgressChannel", {
  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
    $('.progress-percentage').text(`${data.percent}%`)
    $('progress').prop('value', data.percent)
  }
});

```

`consumer.js`でコンシューマを作成し、`progress_channel.js` でコネクション(サブスクリプション)を作成しています
今回は `ProgressChannel` につながるサブスクリプションです
`connected()`にコネクションができたときの動作、`disconnected()`にコネクションがなくなったときの動作を記述します
`received()`にはブロードキャストされたデータを受け取ったときの挙動を書きます
プログレスバーの進捗状況がサブスクライバにブロードキャストされて受け取るので、ここでプログレスバーの各要素を jQuery で書き換えます

__完成！__

# まとめ
以上のコードを実装することでユーザーごとにプッシュ通信にするプログレスバーが実装できます
Qiita 初投稿で ActionCable も学びたてなので間違っているところもあるかもしれません、その部分はご指摘・ご教授いただけるとありがたいです
また、記事のこの部分がわかりづらいというコメントもお待ちしています！

# 参考記事
https://railsguides.jp/action_cable_overview.html
https://qiita.com/QUANON/items/0ba35cd0eb5d720f4237
https://qiita.com/masuidrive/items/fd13ab8d9410206a1b91

# P.S.
『[memopic](https://memopic.herokuapp.com)』 という旅行の思い出・アルバム・写真共有サービスを作っています。是非使ってみてください！
ご意見・ご要望などあれば [@memopic_bamboo](https://twitter.com/memopic_bamboo?s=20)まで DM お願いします！