ActionCableで通信するホワイトボードをherokuにデプロイする
9/26/2019

現在参画している案件では、業務委託含めエンジニア全員がローテーションで2週に1回ペースで技術系発表会(通称:エンジニア会)を行っています。
業務時間外で作成した資料なので、本ブログにも残しておくことにします。
公開URL
https://sticky-whiteboard.herokuapp.com/
(非公開中)
github
https://github.com/kazukifujii/whiteboard
概要
テキストエリアに何らかの項目を入力して送信をすると付箋が作成される。ブラウザを開いている人全員にリアルタイム送信される。
作成意図
・node.jsを触っていてwebsocketに興味が出てきた。
・javascriptとjqueryをあまり触ってこなかったことで開発時スムーズに行かないことが多い。(特にデバッグ)
・Railsの知らない技術がまだまだたくさんある
・Railsの知らない機能を触りつつ必然的にjavascriptも書くのでいい題材そう。
ActionCable概要
フロントのWebSocket - バックエンドのRails を統合するフレームワーク。
WebSocketは、Web上の双方向の通信をHTTP等よりも軽量で行うことが出来る。
Pub/Subモデルで送信側(Publisher)が個別の受信者(Subscriber)を指定せずbroadcastでメッセージを送信する。
これにより低コストでリアルタイムな通信が可能となる。
詳しくはRailsガイドを参照。
https://railsguides.jp/action_cable_overview.html
HTTPとの違い
HTTPはブラウザから何らかのリクエストをしてサーバからレスポンスが返る一問一答型のプロトコル。
基本的にpush通知のようなことは出来ないが、ブラウザからサーバに定期的な問い合わせを行うポーリングという手法を使うとHTTPでも実装可能。
当然負荷は高い。
その他以下のような仕組みがある。
ロングポーリング
基本ポーリングと同じだが、問い合わせを受けたサーバは新着情報がなければレスポンスを保留して、新着情報が発生すれば即座にレスポンスを返す方法。
SSE(Server-Sent Events)
基本ロングポーリングと同じだが、情報が発生するたびにレスポンスは返さず、長さ不定のレスポンスをだらだら返し続ける(情報が発生するたびにレスポンスの続きが来る)方式。
HTTPではレスポンスボディの長さを不定にできるというルールがあるので、これにより下り情報が発生するたびにリクエスト・レスポンスが必要なロングポーリングのオーバーヘッドが解消される。
WebSocket
HTTPのオーバーヘッド無しに双方向通信するための規格。
HTTP(S)リクエストにUpgradeヘッダを付けることですぐにHTTPの枠外に飛び出し、一問一答というルールにとらわれず最短2バイトのヘッダを付けてやりとりする。
plantuml
app/channels/room_channel.rb
class RoomChannel < ApplicationCable::Channel
def subscribed
stream_from "room_channel"
end
def unsubscribed
end
def speak(data)
StickyNote.create! content: data['sticky_note']
end
def remove(data)
StickyNote.destroy data['sticky_note']
end
end
app/assets/javascripts/channels/room.coffee
app/assets/javascripts/channels/room.coffee
App.room = App.cable.subscriptions.create "RoomChannel",
connected: () ->
disconnected: () ->
received: (data) ->
if data['sticky_note'] != undefined
sticky_note = $(data['sticky_note'])
sticky_note.attr('id', 'note')
$('#sticky_notes_index').append(sticky_note)
else
# !isNaNで動いてほしい。。
if isNaN(data['sticky_note'])
$('.sticky_note_' + data['id']).remove()
speak: (sticky_note) ->
@perform 'speak', sticky_note: sticky_note
remove: (sticky_note) ->
@perform 'remove', sticky_note: sticky_note
# 送信クリック - 付箋作成
$(document).on 'click', '[data-behavior~=room_speak]', (e) ->
App.room.speak $('.text').val();
$('.text').val('');
e.preventDefault()
# 削除クリック - 付箋削除
$(document).on 'click', '.remove-icon', (e) ->
if !confirm('本当に削除しますか?')
return false;
else
id = $(e.target).data('sticky_note-id')
App.room.remove id
e.preventDefault()
# 付箋の色変更
$ ->
$('.note').children('.color-button').click ->
main_color = $(this).data('main-color')
sub_color = $(this).data('sub-color')
shadow_color = $(this).data('shadow-color')
shadow = $(this).data('shadow')
$(this).parents('.note').css 'background', "linear-gradient(to right, #{shadow} 0%, #{shadow_color} 0.5%, #{sub_color} 13%, #{main_color} 16%)"
# 付箋ホールド - ドラッグ
$ ->
$('.note').draggable()
# 付箋ダブルクリック - 編集
# $ ->
# $('.note').dblclick ->
# $(this).wrapInner('<textarea class="text" name="text" cols="23" rows="8"></textarea>').find('textarea').focus().select().blur ->
# $(this).parent().html $(this).val()
app/jobs/sticky_note_broadcast_job.rb
class StickyNoteBroadcastJob < ApplicationJob
queue_as :default
def perform(sticky_note)
if sticky_note.is_a?(ActiveRecord::Base)
ActionCable.server.broadcast 'room_channel', sticky_note: render_sticky_note(sticky_note)
else
ActionCable.server.broadcast 'room_channel', id: sticky_note
end
end
private
def render_sticky_note(sticky_note)
ApplicationController.renderer.render partial: 'sticky_notes/sticky_note', locals: { sticky_note: sticky_note }
end
end
詰まりポイント
特定の受信者に対してメッセージを送ることは出来ない
roomは並列化出来るので、例えばdeviseでユーザー機能を作って部屋ごとに異なるユーザーが入室していて、そのroomの入室者だけにbroadcastするみたいなことは可能。
roomに対してのメッセージは全体送信になる。
サーバからデータを受け取るJSの関数が一つしかない
サーバーからデータを受け取ってブラウザにリアルタイムで返す際にJS側で received を使うが 、 received は固定かつ一箇所しか使えなかったので、付箋データの作成と削除処理の共存に苦労した。
現状 receivedで受け取ったデータ型に合わせて if/elseで 作成するか削除するかの処理を分けている。
(無理やり感がある上にbroadcastしたい処理を追加していくとreceivedが肥大化しそう)
herokuのエラー
There was an exception - Gem::LoadError(Error loading the 'redis' Action Cable pubsub adapter. Missing a gem it depends on? redis is not part of the bundle. Add it to your Gemfile.)
普通にdeployするとredisのエラーを吐かれる。
サイト自体は表示されるがリアルタイム通信が出来ない。
対処法
production.rb
config.action_cable.allowed_request_origins = [ /http:\/\/.*/ ]
config/cable.yml
production:
# adapter: redis
# url: redis://localhost:6379/1
# channel_prefix: ac_test_production
adapter: async
上記で一旦直るがherokuは無料でredisを使えるので、redisを使うように設定した方が無難。
本番でのasync設定は非推奨。
フリープランもクレジットカード登録しなければいけなかったので今回はスキップ。
8.1.1.1 Asyncアダプタ
asyncアダプタはdevelopment環境やtest環境での利用を意図したものであり、>production環境で使うべきではありません。
https://railsguides.jp/action_cable_overview.html#利用できるアダプタ設定
今後やりたい
・付箋の位置を追従したい
・色情報を保存したい
・ダブルクリックで後からコンテンツの内容を編集できるようにしたい
・色変更の●が増えていくと集合体恐怖症が発症しそうなのでなんとかしたい
・株価のチャート、ルーム分けされたチャットなども作ってみたい

©️ 2020 ふじい Dev-Remo-Work.