【 前提 】
ActionCableを使用したチャットapp
チャットルームは複数作成可能
開発環境
Rails: 6.0.3
Ruby: 3.0.1
DB: postgresql, redis
PC: Mackbook Air
デバック前コード
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)
}
});
});
症状
- ページ移動をするとチャットが複数表示される。(以下参照)
- ページを更新すると正常な表示になる
⭕チャットルームに初めてアクセスした場合は正常に送信される
❌別ページから再びチャットルームに戻ってくるとチャットデータが2つ送信される
❌そしてまた戻ってくると今度はチャットデータが3つ送信される
考察
- ページを更新すると多く表示されてしまった分は消えることから、レコードは1つしか保存されていない
- ターミナルのログのBroadcasting, Transmittingを見ても1つ分のデータしか送信されていない
jsファイルをどうにかせなあかん - room_channel.js3行目に記入した
console.log("addEventListener turbolinks:load")
がページ移動のたびに検証画面のログに呼び出されている - そのログの数に応じてチャットデータのログが表示されている。
room_channel.js2行目から定義されているdocument.addEventListener('turbolinks:load', () => {}
がページ移動した後も実行され続けていて、ページに戻ってきた時にこの関数が追加されてしまう。
【 解決方法 】
デバック後コード
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を取得。
現在接続されているroom_idと比較。
過去に接続されたものと同じroom_idが1つでも存在していたらconsumer.subscriptions.create内のreceived(data)を実行しない。
ポイントはroom_channel.js1行目で読み込まれている./consumerのconsumerクラスからコードを読み解くこと。順に解説していきます。
for…of文の内容解説
// ここから追加
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
consumer…rails/actioncable/app/javascript/action_cable/consumer.jsの30行目で定義されているclass Cousumerのインスタンス。
subscriptions(1つ目)…rails/actioncable/app/javascript/action_cable/subscriptions.jsの15行目で定義されているclass Subscriptionsのインスタンス。
ここまでのconsumer.subscriptionは、room_channel.js17行目のconsumer.subscription.createで作成されている。
subscriptions(2つ目)…rails/actioncable/app/javascript/action_cable/subscriptions.jsの
19行目で定義されているインスタンスメソッド。ページ移動で新たにsubscriptionインスタンスが作成されるとそれが引数に格納される。(上記のcunsumer.subscriptionインスタンスが作成されるたびに配列に格納される)
つまり、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を取り出している。
上記を解説を踏まえて改めて追記コードの解説
- 変数already_connectedを定義しfalseにしておく。
- subscriptions配列から1つづつsubscriptionを取り出す。
- subscriptionのroom_idを取り出し変数already_connected_room_idに格納。
- already_connected_room_idとroom_id(現在接続されているsubscriptionのroom_id)がもし同じであれば、変数already_connectedの値をtrueにしてfor文を抜ける
-
if (chatContainer == null || already_connected) {return}
とすることで、作成されたsubscriptionの中にすでに接続されているroom_idが存在していた場合にreceived(data)が実行されないように定義
【 最後に 】
筆者はプログラミング初学者で、転職活動に使用するPF作成中にでた不具合の解消記録です。
社内のコミュニケーションをより円滑にしたいという依頼を受けた仮定し、ニックネームで呼び合うことでその課題を解決するコミュニケーションツールを開発しました。ニックネームはアプリ登録時に自動でつくようになっていますが、後から編集も可能です。