はしばみあきら blog

プログラミングアウトプットするブログ。202010スタート

【RailsでLINEを作る】ActionCableで特定のユーザーとリアルタイムチャット機能を作る

ルーム機能を作ったのでいよいよ作っていきます。

色々な記事を参考にして、大量に記事を未過ぎてどの記事を参考にしたかもうわからない程になりました...

つらつらと掲載させていただきます。他にもaction cableと書いてあれば見るみたいな感じでやりました。

Action cableを実装する

自分の使用しているのはrails 5 です。

なんかrails 6 だと所々違うっぽい。

前回ルーム機能の時にモデルの記述をブログで書いてなかったです。throughを使います

前準備

user.rb

  # ユーザーは沢山のトークルームを持つ。それは他のユーザーを通してである。
  has_many :talk_room, dependent: :destroy, through: :room_users
talk_room.rb

  has_many :users,  through: :room_users

トークルームに遷移する前に、相手方のユーザーidをパラメータに持たせます。

users/show.html.slim

p = @user.name
- unless @user.id == current_user.id
  - if @is_room == true
    = link_to "チャット", talk_room_path(@room_id ,user_id: @user.id)
  - else
    = form_with model: @room, url: talk_rooms_path, local: true do |f|
      = fields_for @room_user do |ru|
        = ru.hidden_field :user_id, value: @user.id
      = f.submit 'チャット'

talk_room_controllerのshowを記述

talk_room_controller

  def show
    @user = User.find(params[:user_id])
    @room = TalkRoom.find(params[:id])
    @talks = @room.talks
  end

viewの記述

talk_rooms/show.html.slim

.talk-room-show
  h2 = "#{@user.name} さんとのトーク"
  .talk-room-show__form
    input type="text" data-behavior="room_speaker"
  .talk-room-show__talks
    #talk
      - @talks.each do |talk|
        p = talk.talk

Action cable の導入

チャネルを作成します。

$ rails g channel talk_room speak

これをターミナルで入力すると、

  • app/channels/talk_room_channel.rb
  • app/assets/javascripts/channels/talk_room.coffee

この2つのファイルができるかと思います。

channelはサーバー側の処理、coffeeはフロント側の処理を行います。

この2つを行き来してリアルタイムの動きがされます。

jQueryのメソッドをcoffee内で使うため、予めjQueryを導入しておきましょう。

次に、coffeeを書いていきます。

javascripts/channels/talk_room.coffee

App.talk_room = App.cable.subscriptions.create "TalkRoomChannel",

  # 通信が確立された時
  connected: ->
    console.log("connect");

  # 通信が切断された時
  disconnected: ->

  # 値を受け取った時
  received: (data) ->

  speak: (talk) ->
    # サーバーサイド(channel)のspeakアクションにtalkパラメータを渡す
    @perform 'speak',talk: talk

jQuery(document).on 'keypress', '[data-behavior~=room_speaker]', (event) ->
  # return(Enter)が押された時
  if event.keyCode is 13
    # channel speakへ, event.target.valueを引数に
    App.talk_room.speak event.target.value
    event.target.value = ''
    alert()
    event.preventDefault()

connected: で connect と表示するようにしています。接続が確立されていれば、ブラウザの検証ツールで、consoleにconnectと出ます。

f:id:hashibamiakira:20201019194722p:plain

jQueryで、inputに対するアクションを記述。

今の状態で、return(Enter)キーを押し、アラートが出れば正しく動いています。

f:id:hashibamiakira:20201019195053p:plain

次にchannelを記述していき、js(coffee)から送られてきたデータを保存し送り返します。

channels/talk_room_channel.rb

class TalkRoomChannel < ApplicationCable::Channel

  # 接続された時
  def subscribed
    # フロントとバックが通信している時(お互いを監視している時)に実行される
    stream_from "talk_room_channel"
  end

  # 切断された時
  def unsubscribed
  end

  def speak(talk)
    # Talkモデル内、talkカラムに、(talk)に渡されたvalue['文字列']が保存される
    Talk.create(talk: talk['talk'])
  end

end

Talkモデルにレコードを保存するために、user_idとtalk_room_idを引っ張ります。

showページのinputにパラメータを持たせます。ここではdataメソッドを使います。

talk_rooms/show.html.slim

.talk-room-show
  h2 = "#{@user.name} さんとのトーク"
  .talk-room-show__form
    input[type="text"
          data-behavior="room_speaker"
          data-user="#{current_user.id}"
          data-room="#{@room.id}"]
  .talk-room-show__talks
    #talk
      - @talks.each do |talk|
        p = talk.talk

この data をcoffeeの方で受け取り、配列として channel のアクションに渡します。

javascripts/channels/talk_room.coffee

App.talk_room = App.cable.subscriptions.create "TalkRoomChannel",

  # 通信が確立された時
  connected: ->

  # 通信が切断された時
  disconnected: ->

  # 値を受け取った時
  received: (data) ->

  speak: (talk) ->
    # サーバーサイド(channel)のspeakアクションにtalkパラメータを渡す
    @perform 'speak', talk: talk

jQuery(document).on 'keypress', '[data-behavior~=room_speaker]', (event) ->
  # return(Enter)が押された時
  if event.keyCode is 13
    # channel speakへ, event.target.valueを引数に
    App.talk_room.speak [event.target.value, $('[data-user]').attr('data-user'), $('[data-room]').attr('data-room')]
    event.target.value = ''
    event.preventDefault()

js(coffee)から送られてきたデータを、channel内のspeakアクションに渡し、それを元にTalkを保存します。

channels/talk_room_channel.rb

class TalkRoomChannel < ApplicationCable::Channel

  # 接続された時
  def subscribed
    # フロントとバックが通信している時(お互いを監視している時)に実行される
    stream_from "talk_room_channel"
  end

  # 切断された時
  def unsubscribed
  end

  def speak(talk)
    # Talkモデル内、talkカラムに、(talk)に渡されたvalue['文字列']が保存される
    talk = Talk.new(talk: talk['talk'][0],
                    user_id: talk['talk'][1].to_i,
                    talk_room_id: talk['talk'][2].to_i)
    talk.save
    # 同じchannel名の全てにインプットタグに入力された文字を配信する
    ActionCable.server.broadcast 'talk_room_channel', talk: talk['talk'][0]
  end

end

ここで一度、speakアクションのすぐ下にbinding.pryをつけて値を見てみます。

f:id:hashibamiakira:20201019203329p:plain

talk内に、内容とユーザーid、トークルームidが入っていることが確認できました。

これらをTalk.newで、それぞれのカラムに入れていくことができます。

他の方の記事を参考にしながら、ホェーーそんなことができるか...すげぇ...と思わず声に出しながらコードを書いていたのを思い出します...

話が逸れました。

ActionCable.server.broadcast 'talk_room_channel', talk: talk['talk'][0]で、channelに接続しているcoffeeにtalkの内容が帰ります。

coffeeで受け取った内容を、viewのidをターゲットにして、pタグで追加します。

javascripts/channels/talk_room.coffee

App.talk_room = App.cable.subscriptions.create "TalkRoomChannel",

  # 通信が確立された時
  connected: ->

  # 通信が切断された時
  disconnected: ->

  # 値を受け取った時
  received: (data) ->
    # サーバーサイドから値を受け取りviewに追加する
    $('#talk').append("<p>"+data["talk"]+"</p>");

  speak: (talk) ->
    # サーバーサイド(channel)のspeakアクションにtalkパラメータを渡す
    @perform 'speak', talk: talk

jQuery(document).on 'keypress', '[data-behavior~=room_speaker]', (event) ->
  # return(Enter)が押された時
  if event.keyCode is 13
    # channel speakへ, event.target.valueを引数に
    App.talk_room.speak [event.target.value, $('[data-user]').attr('data-user'), $('[data-room]').attr('data-room')]
    event.target.value = ''
    event.preventDefault()

もし自分の記述に矛盾がなければ、これで一旦 TalkRoomChannel に接続されているviewに対しては、リアルタイムのチャット機能を付けれるようになっているはずです。

実際の動きはこちらです。

タブを複製して、複数ブラウザで試してみても全てのブラウザで入力された内容が反映されているはずです。

ただし、今のままだと他の人とのトークルームにも、自分の入力した内容が反映されてしまいます。(TalkRoomというチャネルが1つしかないため)

次回はこのTalkRoomチャネルとTalkRoomのidを紐付けて、他のユーザーのルームに内容を反映しないようにしていきます。

ここまで書くのすごく大変だった...こんなに書くのは初めてです。

はてなブログなのでコードが白黒でみづらい点もあるかと思いますが。同じような初学者の方に参考になれば幸いです。

またボチボチ更新していきます。