Rails7とNuxt3でActionCableを使ったチャットアプリを作る
はじめに
Rails7とNuxt3でチャットアプリを作成したので、残しておきます。
Nuxt3はRC版です。安定版で動かない場合は教えていただけると助かります。
間違っている箇所があればご指摘お願いいたします。
各バージョン
- Rails: 7.0.3.1
- Nuxt: 3.0.0-rc.9
用語について
ActionCableではあまり見慣れない用語(主にPub/Sub関連の用語)が出てきますが、Railsガイドを読めばなんとなくわかると思います。
事前準備
APIモードでRailsアプリを作成
$ rails new /app/backend --api --minimal
Nuxt3アプリを作成
npx nuxi init /app/frontend
ActionCableで通信してみる
railsの設定を変更して、ActionCableを有効化する
action_cableをrequireしている箇所のコメントアウトを解除する
※rails new
時に--minimal
オプションをつけていない場合は必要ないかと思います。
# ...
require "action_cable/engine"
# ...
サブスクリプションアダプタを設定
Railsガイドの設定をそのままコピーしています。
development:
adapter: async
test:
adapter: test
production:
adapter: redis
url: redis://10.10.3.153:6381
channel_prefix: appname_production
開発環境では、すべてのオリジンから接続できるようにする
# ...
config.action_cable.disable_request_forgery_protection = true
# ...
chatチャンネルを作成する
$ rails g channel chat
購読・配信処理を書く
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from("chat:#{params[:room]}")
end
def test
ActionCable.server.broadcast("chat:#{params[:room]}", { body: "「#{params[:room]}」を購読中です" })
end
end
subscribed
は、購読開始時に処理されるメソッドです。
stream_from
で、購読を設定しています。
これで、ActionCable.server.broadcast
メソッドでチャットルームにメッセージを配信できるようになります。
test
メソッドが呼ばれた場合、コンシューマーへ購読中のルーム名を配信するようにしています。
nuxtにactioncableのパッケージを追加
$ yarn add actioncable
nuxtの設定を変更する
SSRを有効にしていると、「window is not defined」エラーが出てしまうため、無効化しています。
import { defineNuxtConfig } from 'nuxt'
export default defineNuxtConfig({
ssr: false,
})
フロント側の処理
<template>
<div>
<button @click="test">テスト</button>
</div>
</template>
<script setup lang="ts">
import ActionCable from 'actioncable'
// 接続を生成(引数は、'ws://[Railsの接続URL]/cable')
const cable = ActionCable.createConsumer('ws://localhost:3000/cable')
// chatチャンネルのサブスクリプションを作成
const chatChannel = cable.subscriptions.create(
{ channel: 'ChatChannel', room: 'チャットルーム1' },
{
// 配信された時のメソッド
received(response) {
console.log(response)
},
}
)
// 接続確認
const test = () => {
chatChannel.perform('test')
}
</script>
chatChannel.perform('test')
で、サーバー側のtest
メソッドを呼び出しています。
サーバー側からメッセージが配信された場合、received
メソッドが呼ばれます。
今回は、配信された内容をそのままコンソールに出力しています。
確認
複数のウィンドウでページを開き、「テスト」ボタンをクリックします。
すると、すべてのウィンドウのコンソールにルーム名が表示されます。
チャットができるようにする
ActionCableでの通信はできたので、チャットができるように整えていきます。
特別なことはしていないので、ソースだけ載せておきます。
rails
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from("chat:#{params[:room]}")
end
# チャットルームにメッセージを配信
def speak(data)
content = {
type: 'speak',
body: {
name: data['name'],
message: data['message'],
spoke_at: Time.zone.now,
},
}
ActionCable.server.broadcast("chat:#{params[:room]}", content)
end
end
nuxt3
<template>
<div>
<!-- メッセージ一覧 -->
<div v-for="message in messages">
{{ message.name }}:{{ message.message }}
<small>{{ message.spoke_at }}</small>
</div>
<!-- 入力 -->
<h1>名前</h1>
<input type="text" v-model="name" />
<h1>内容</h1>
<input type="text" v-model="input_message" />
<button @click="speak">発言</button>
</div>
</template>
<script setup lang="ts">
import ActionCable from 'actioncable'
const cable = ActionCable.createConsumer('ws://localhost:3000/cable')
const chatChannel = cable.subscriptions.create(
{ channel: 'ChatChannel', room: 'チャットルーム' },
{
received({ type, body }) {
switch (type) {
case 'speak':
messages.value.push(body)
break
}
},
}
)
const speak = () => {
// performの第二引数でサーバー側の関数の引数を設定できる
chatChannel.perform('speak', {
message: input_message.value,
name: name.value,
})
input_message.value = ''
}
const name = ref('')
const input_message = ref('')
const messages = ref([])
</script>
確認
複数ウィンドウで開いて名前とメッセージを送信すると、すべてのウィンドウにメッセージが表示されることを確認できると思います。
これで、簡単なチャットアプリができました。
補足
ちなみに、部屋を複数作るにはstream_from
の第一引数の値を変えてやればいいので、今回の場合はNuxt側で接続ルーム名を変更できるようにすると実現できます。
// ...
const room = ref('チャットルーム1')
const chatChannel = cable.subscriptions.create(
{ channel: 'ChatChannel', room: room.value },
{
received({ type, body }) {
switch (type) {
case 'speak':
messages.value.push(body)
break
}
},
}
)
// ...
参考
Discussion