LoginSignup
24
6

More than 1 year has passed since last update.

【Rails6系、ActionCable、デバック】チャットappでチャットが複数送信されてしまう不具合の解消

Last updated at Posted at 2022-09-03

【 前提 】

ActionCableを使用したチャットapp
チャットルームは複数作成可能

開発環境

Rails: 6.0.3
Ruby: 3.0.1
DB: postgresql, redis
PC: Mackbook Air

デバック前コード

channels/room_channel.js
import consumer from "./consumer"

document.addEventListener('turbolinks:load', () => {
  console.log("addEventListener turbolinks:load")
  window.chatContainer = document.getElementById('chats')

  const element = document.getElementById('room-id');
  const room_id = element.getAttribute('data-room-id');
  
  if (chatContainer == null) {
    return
  }
  consumer.subscriptions.create({ channel: "RoomChannel", room_id: room_id}, {
    connected() {
      console.log(`connected to ${room_id}`)
    },

    disconnected() {
    },

    received(data) {
      console.log(data)
    }
  });
});

症状

  • ページ移動をするとチャットが複数表示される。(以下参照)
  • ページを更新すると正常な表示になる

⭕チャットルームに初めてアクセスした場合は正常に送信される
スクリーンショット 2022-09-03 18.13.31.png
❌別ページから再びチャットルームに戻ってくるとチャットデータが2つ送信される
スクリーンショット 2022-09-03 18.13.55.png
❌そしてまた戻ってくると今度はチャットデータが3つ送信される
スクリーンショット 2022-09-03 18.28.59.png

考察

  • ページを更新すると多く表示されてしまった分は消えることから、レコードは1つしか保存されていない
  • ターミナルのログのBroadcasting, Transmittingを見ても1つ分のデータしか送信されていない
    :arrow_forward: jsファイルをどうにかせなあかん
  • room_channel.js3行目に記入したconsole.log("addEventListener turbolinks:load")がページ移動のたびに検証画面のログに呼び出されている
  • そのログの数に応じてチャットデータのログが表示されている。
    :arrow_forward: room_channel.js2行目から定義されているdocument.addEventListener('turbolinks:load', () => {}がページ移動した後も実行され続けていて、ページに戻ってきた時にこの関数が追加されてしまう。

【 解決方法 】

デバック後コード

room_channel.js
import consumer from "./consumer"

document.addEventListener('turbolinks:load', () => {
  window.chatContainer = document.getElementById('chats');

  const element = document.getElementById('room-id');
  const room_id = element.getAttribute('data-room-id');

  // ここから追加
  let already_connected = false
  for (let subscription of consumer.subscriptions.subscriptions) {
    let already_connected_room_id = JSON.parse(subscription.identifier).room_id;
    if (already_connected_room_id === room_id) {
      already_connected = true
      break
    }
  }
  // ここまで追加

  // || already_connectedを追加
  
  if (chatContainer == null || already_connected) {
    return
  }
  
  consumer.subscriptions.create({ channel: "RoomChannel", room_id: room_id}, {
    connected() {
    },

    disconnected() {
    },

    received(data) {
      console.log(data);
    }
  });
});

解説

付け焼き刃的かもしれませんが、以下のように定義することで解決しました。

すでに接続されているroom_idを取得。
:arrow_forward: 現在接続されているroom_idと比較。
:arrow_forward: 過去に接続されたものと同じroom_idが1つでも存在していたらconsumer.subscriptions.create内のreceived(data)を実行しない。

ポイントはroom_channel.js1行目で読み込まれている./consumerのconsumerクラスからコードを読み解くこと。順に解説していきます。

for…of文の内容解説

room_channel.js
 // ここから追加
  let already_connected = false
  for (let subscription of consumer.subscriptions.subscriptions) {
    let already_connected_room_id = JSON.parse(subscription.identifier).room_id;
    if (already_connected_room_id === room_id) {
      already_connected = true
      break
    }
  }
  // ここまで追加

  // || already_connectedを追加
  
  if (chatContainer == null || already_connected) {
    return
  }

※for…of文の文法についてはこちら
※JSON.parse文の文法についてはこちら

consumer.subscriptions.subscriptions

consumerrails/actioncable/app/javascript/action_cable/consumer.jsの30行目で定義されているclass Cousumerのインスタンス。
subscriptions(1つ目)…rails/actioncable/app/javascript/action_cable/subscriptions.jsの15行目で定義されているclass Subscriptionsのインスタンス。
:arrow_forward: ここまでのconsumer.subscriptionは、room_channel.js17行目のconsumer.subscription.createで作成されている。
subscriptions(2つ目)…rails/actioncable/app/javascript/action_cable/subscriptions.js
19行目で定義されているインスタンスメソッド。ページ移動で新たにsubscriptionインスタンスが作成されるとそれが引数に格納される。(上記のcunsumer.subscriptionインスタンスが作成されるたびに配列に格納される)

:arrow_forward: つまり、consumer.subscriptions.subscriptionsにはページ移動のたびに作成されるconsumer.subscripstionsが格納されている。

JSON.parse(subscription.identifier).room_id

作成されて配列に格納されているsubscriptionからroom_idを取り出している。
consumer.subscriptions.subscriptions配列の中から取り出したsubscriptionに対してsubscriptionインスタンスメソッドidentifierを実行。そしてこれはstring型であるため、JSON.parseでobject型にした後、subscriptionが持っているroom_idを取り出している。

上記を解説を踏まえて改めて追記コードの解説

  1. 変数already_connectedを定義しfalseにしておく。
  2. subscriptions配列から1つづつsubscriptionを取り出す。
  3. subscriptionのroom_idを取り出し変数already_connected_room_idに格納。
  4. already_connected_room_idとroom_id(現在接続されているsubscriptionのroom_id)がもし同じであれば、変数already_connectedの値をtrueにしてfor文を抜ける
  5. if (chatContainer == null || already_connected) {return}とすることで、作成されたsubscriptionの中にすでに接続されているroom_idが存在していた場合にreceived(data)が実行されないように定義

【 最後に 】

筆者はプログラミング初学者で、転職活動に使用するPF作成中にでた不具合の解消記録です。
社内のコミュニケーションをより円滑にしたいという依頼を受けた仮定し、ニックネームで呼び合うことでその課題を解決するコミュニケーションツールを開発しました。ニックネームはアプリ登録時に自動でつくようになっていますが、後から編集も可能です。

こちらにGitHubアプリのリンクを添付いたしますので、もしよければ御覧ください。

24
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
24
6