Edited at

ActionCableによるチャットで画像も送る

タイトルの通りです。ActionCableによるチャット作成の解説はよくありますが、

画像も送信する解説が日本のサイトでは見つからなかったのでまとめておきます。

下記サイトを参考にコードを自分なりに分かりやすく若干アレンジしてます。

参考サイト:

1. Action Cableでリアルタイムチャットアプリの作成方法 (Rails 5.1.4にて)(その1) herokuで動かす!

2. Uploading Files With Rails and ActionCable

チャット部分は1を、画像アップロード部分は2を参考にしています。


  • 環境


    • Rails 5.1.6.1

    • ruby 2.4.5p335 (2018-10-18 revision 65137) [x86_64-linux]



※下記解説でerbはslim、JavaScriptはCoffeeScriptで書いています。

 slim→erbへの変換はslim gemを入れれば使えるようになるslimrbコマンドでできます。

 slimrb -e show.html.slim show.html.erb

 slimについてはこちら→速習テンプレートSlim(HTML作成編)

 CoffeeScriptはブラウザでJavaScriptへ変換できるツールがありますので、そちらをご確認ください。

 js2coffee ※右側にCoffeeScriptを貼り付けると左側にJavaScriptが表示されます。


必要なもの


  • jquery

  • CarrierWave


    • 画像アップロード用




  • CarrierWave::DataUri


    • DataURIスキームで表された画像データをCarrierWaveで保存するためのプラグイン



後述の手順の中で入れていきます。


1. アプリ作成と初期設定

$ rails new ac_image_test

$ cd ac_image_test

Gemfileに追記して必要なものをbundle installします。


Gemfile

gem 'jquery-rails', '4.3.1'

gem 'carrierwave', '1.2.2'
gem 'carrierwave-data-uri'

$ bin/bundle install

jquery用の設定もします。下記の2行目を追加。


app/assets/javascripts/application.js

//= require rails-ujs

//= require jquery
//= require turbolinks
//= require_tree .


2. チャットルーム用のコントローラ作成

$ bin/rails g controller rooms show


app/controllers/rooms_controller.rb

class RoomsController < ApplicationController

def show
@messages = Message.all
end
end


3. 画像アップローダ作成

$ bin/rails g uploader Picture


4. メッセージ保存用作成

$ bin/rails g model message content:text picture:string

$ bin/rails db:migrate


app/models/message.rb

class Message < ApplicationRecord

# 後述(9. ブロードキャストジョブの作成)
after_create_commit { MessageBroadcastJob.perform_later self }

# CarrierWave用の設定
# Messageモデルのpicture属性とPictureUploaderクラスを紐付ける
# PictureUploaderクラスは前述の3.画像アップローダ作成で作られている
mount_uploader :picture, PictureUploader

# チャット画面でメッセージを上から最新順に表示するため
# デフォルトで作成日の降順でデータを取得する設定
default_scope { order(created_at: :desc) }
end



5. チャット表示用ルームビューとパーシャル作成


app/views/rooms/show.html.slim

h1

| Chat Room

form id="test_form"
input type="text" id="content"
input type="file" id="picture"

/ render @messagesはパーシャル用ファイルの
/ _message.html.slimに展開されます。
#messages
= render @messages


パーシャルを入れるディレクトリを作っておきます。

$ mkdir app/views/messages


app/views/messages/_message.html.slim

.message

p
= message.content
- if message.picture?
= image_tag message.picture.url


6. ルームチャンネル作成とテスト

$ bin/rails g channel room speak

これでapp/assets/javascripts/channels/room.coffeeが作成され、

その中にApp.room.speakという関数が定義されます。

その関数がちゃんと動くかここでテストしておきます。

railsサーバを起動します。

$ bin/rails s

chromeなどで http://localhost:3000/rooms/show へアクセスして

デベロッパーツールのコンソールからApp.room.speak()コマンドを

実行してtrueが返ってくればOKです。

test.gif


7. 送信・受信時の処理作成


app/assets/javascripts/channels/room.coffee

App.room = App.cable.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) ->
$('#messages').prepend data['message_html']

# 送信時の処理
speak: (content, data_uri, file_name) ->
# @perform 'speak' で 後述のapp/channels/room_channel.rb に
# 定義したspeakアクションが実行されます。
@perform 'speak', { content: content, data_uri: data_uri, file_name: file_name }
clear_form '#test_form'

# フォームリセット
clear_form = (selector) ->
$(selector).find(":text, :file").val("")

# 入力時の処理
$(document).on 'keypress', '#content', (event) ->
# エンターキーを押した時に処理
if event.which is 13
content = $.trim($("#content").val())
has_content = if content.length > 0 then true else false

picture = $('#picture')
has_picture = if picture.get(0).files.length > 0 then true else false

if has_content or has_picture
if has_picture
file_name = picture.get(0).files[0].name
# ファイル読み込み用のreader生成
reader = new FileReader()
# Data URI Scheme文字列を取得するためのファイル読み込み
reader.readAsDataURL picture.get(0).files[0]
# readerが画像を読み込んだ後の処理
reader.addEventListener "loadend", ->
# reader.result部分が、読み込んだ画像のData URI Scheme文字列
App.room.speak content, reader.result, file_name
else
App.room.speak content

# 後続のイベントキャンセル
# これが無いとエンターを押したあとに画面が一番上まで
# スクロールしたりします。
event.preventDefault()


reader = new FileReader()付近の処理が画像読み込みの肝です。

AcitionCableで画像を送るために、通常のformのfileフィールドによる

POSTではなく、画像をDATA URI Scheme文字列として取得して、それを

App.room.speak関数でrapp/channels/room_channel.rbで定義した

speakメソッドへ受け渡します。なお、文字列で画像を送るため、大きなサイズの

画像ファイル送信には向かないようです。

FileReaderについて

DATA URI Scheme文字列について


8. 送信されたデータの保存処理


app/channels/room_channel.rb

class RoomChannel < ApplicationCable::Channel

def subscribed
stream_from "room_channel"
end

def unsubscribed
# Any cleanup needed when channel is unsubscribed
end

# app/assets/javascripts/channels/room.coffeeで定義した
# speak関数によって、下記のspeakアクションが呼ばれます。
def speak(data)
message = Message.new(
content: data['content'],
picture_data_uri: data['data_uri']
)
message.save
end
end



9. ブロードキャストジョブの作成

$ bin/rails g job MessageBroadcast


app/jobs/message_broadcast_job.rb

class MessageBroadcastJob < ApplicationJob

queue_as :default

def perform(message)
# room_channelに対して生成したデータをブロードキャストします。
# ブロードキャストされたデータは 7. 送信・受信時の処理作成 の
# received関数に渡されます。
ActionCable.server.broadcast('room_channel', data(message))
end

private
# ブロードキャストするデータを生成
def data(message)
{
message_html:
ApplicationController.renderer.render(
partial: 'messages/message',
locals: { message: message }
)
}
end
end


上記のpeformアクションは、前述したMessageモデルに記載した

after_create_commitコールバックから実行されます。


app/models/message.rb

# 再掲

class Message < ApplicationRecord
# messageがcreateされ、commitまでされたら、上記の
# MessageBroadcastJobのperformアクションが実行されます。
after_create_commit { MessageBroadcastJob.perform_later self }
# 省略
end

実装は以上です。


10. 動作確認

railsサーバを起動します。

$ bin/rails s

ブラウザを2つ起動して両方とも http://localhost:3000/rooms/show へアクセスします。

以下のように動けば完成です。

test.gif


処理の流れ

ざっくり以下のような感じです。

app/views/rooms/show.html.slimのinputフォームに入力してエンター



app/assets/javascripts/channels/room.coffeeで定義している

$(document).on 'keypress'イベントに設定した処理でinputフォームのデータを受け取る。

そこから同ファイルで定義しているspeak関数を呼び出す。



speak関数からapp/channels/room_channel.rbのspeakメソッドが呼ばれる。



speakメソッド内のmessage.saveにより、app/models/message.rb

after_create_commitコールバックが呼ばれ、app/jobs/message_broadcast_job.rb

peformメソッドが呼ばれる。



peformメソッドによりspeak関数から引き継がれてきたフォームデータが

ブロードキャストされる。



ブロードキャストされたフォームデータをapp/assets/javascripts/channels/room.coffee

定義したreceived関数が受け取り、app/views/rooms/show.html.slimへ出力する。


ソース

上記のソースをGitHubに上げました。ご参考に。

https://github.com/xojan0120/ac_image_test