Rails7とNuxt3でActionCableを使ったチャットアプリを作る

2022/09/12に公開

はじめに

Rails7とNuxt3でチャットアプリを作成したので、残しておきます。
Nuxt3はRC版です。安定版で動かない場合は教えていただけると助かります。

間違っている箇所があればご指摘お願いいたします。

各バージョン

  • Rails: 7.0.3.1
  • Nuxt: 3.0.0-rc.9

用語について

ActionCableではあまり見慣れない用語(主にPub/Sub関連の用語)が出てきますが、Railsガイドを読めばなんとなくわかると思います。
https://railsguides.jp/action_cable_overview.html

事前準備

APIモードでRailsアプリを作成

$ rails new /app/backend --api --minimal

Nuxt3アプリを作成

npx nuxi init /app/frontend

ActionCableで通信してみる

railsの設定を変更して、ActionCableを有効化する

action_cableをrequireしている箇所のコメントアウトを解除する

rails new時に--minimalオプションをつけていない場合は必要ないかと思います。

config/application.rb
# ...
require "action_cable/engine"
# ...

サブスクリプションアダプタを設定

Railsガイドの設定をそのままコピーしています。

config/cable.yml
development:
  adapter: async

test:
  adapter: test

production:
  adapter: redis
  url: redis://10.10.3.153:6381
  channel_prefix: appname_production

開発環境では、すべてのオリジンから接続できるようにする

config/environments/development.rb
# ...
config.action_cable.disable_request_forgery_protection = true
# ...

chatチャンネルを作成する

$ rails g channel chat

購読・配信処理を書く

app/channels/chat_channel.rb
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」エラーが出てしまうため、無効化しています。

nuxt.config.ts
import { defineNuxtConfig } from 'nuxt'

export default defineNuxtConfig({
  ssr: false,
})

フロント側の処理

app.vue
<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

app/channels/chat_channel.rb
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

app.vue
<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
      }
    },
  }
)
// ...

参考

https://railsguides.jp/action_cable_overview.html
https://api.rubyonrails.org/v7.0.3/classes/ActionCable/Channel/Streams.html
https://qiita.com/daitasu/items/d018fba8d3daa51ecf51
https://qiita.com/gimKondo/items/5f4790bbbc6beaea520e

Discussion