めるノート

一児の母 兼 へっぽこWebエンジニアの内省ノート

RailsのAction CableとWebpackerとVue.jsを使ってチャットを作成してみる

もう2ヶ月くらい前になりますが、Action Cableを用いたチャットの実装にチャレンジする機会がありました。
このときには、Railsの中でVue.jsをどう扱うのかについての一例を見ることもできたので、記念(?)にざっくりしたコードを残しておきます。

1. rails newと、webpackerでVueの初期化

rails new したあと、Gemfileに gem "webpacker" を追記し、bundle install した後、以下のコマンドを実行します。

$ bundle exec rails webpacker:install
$ bundle exec rails webpacker:install:vue

2. モデルを作る

$ bundle exec rails db:create
$ bundle exec rails g model message content:text user_id:integer
$ bundle exec rails g model user name:string email:string
$ bundle exec rails db:migrate
class User < ApplicationRecord
  has_many :messages, dependent: :restrict_with_error
end
class Message < ApplicationRecord
  belongs_to :user

  validates :content, presence: true

  after_create_commit { MessageBroadcastJob.perform_later self }
end

3. コントローラの作成

class HomeController < ApplicationController
  def index
    @messages = Message.all
  end
end

4. Actioncableのインストール

$ yarn add actioncable

5. Viewを作る(Vueコンポーネントの実装)

app/javascript/packs/application.js

import Vue from 'vue/dist/vue.esm'
import UserChat from '../components/user-chat.vue'
import ActionCable from 'actioncable';

const cable = ActionCable.createConsumer('ws:localhost:3000/cable');
Vue.prototype.$cable = cable;

document.addEventListener('DOMContentLoaded', () => {
  const app = new Vue({
    el: '#main-container',
    data: {},
    components: { UserChat }
  })
})

app/javascript/packs/components/user-chat.vue

<template>
<div>
  <div>
      <div v-for="item in messages" :key="item.message.id">
        <div>
          <div>{{ item.message.content }}</div>
      </div>
    </div>
  </div>
  <div>
    <div>
      <input type="text" v-model="message" placeholder="入力してください ...">
      <span>
        <button type="button" v-if="userMessageChannel" @click="speak">Send</button>
      </span>
    </div>
  </div>
</div>
</template>

<script>
export default {
  data() {
    return {
      message: "",
      messages: [],
      userMessageChannel: null,
    };
  },
  props: ['userId'],
  created() {
    this.userMessageChannel = this.$cable.subscriptions.create( "UserMessageChannel", {
      received: (data) => {
        this.messages.push(data)
        this.message = ''
      },
    })
  },
  methods: {
    speak() {
      this.userMessageChannel.perform('speak', { 
        message: this.message,
        user_id: this.userId,
      });
    },
    // 以下、今回のViewでは使っていませんが、LINEのように自分からのメッセージと他ユーザーからのメッセージでスタイルを分けるときなどに使います
    messageClass (user_id) {
      return {
        "right": user_id === Number(this.userId)
      }
    },
    dataClass (user_id) {
      return {
        "float-right": user_id === Number(this.userId)
      }
    }
  },
};
</script>

app/views/layouts/application.html.erb

<!DOCTYPE html>
<html>
  <head>
    <title>VueActioncable</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag 'application', media: 'all' %>
    <%= javascript_pack_tag 'application' %>
  </head>

  <body>
    <div id="main-container">
      <%= yield %>
    </div>
  </body>
</html>

app/views/home/index.html.erb

<% @messages.each do |message| %>
  <div>
    <%= message.content %>
  </div>
<% end %>
<!-- current_userはdevise gemを入れるなどでログイン実装することによって使えます -->
<user-chat user-id="current_user.id"></user-chat>

6. チャンネルを作る

$ bundle exec rails g channel user_message speak
class UserMessageChannel < ApplicationCable::Channel
  def subscribed
    stream_from "user_message_channel"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end
  
  def speak(data)
    Message.create!(
      user_id: data['user_id'],
      content: data['message']
    )
  end
end

7. Jobの作成

$ bundle exec rails g job MessageBroadcast
class MessageBroadcastJob < ApplicationJob
  queue_as :default

  def perform(message)
    ActionCable.server.broadcast "user_message_channel", message: message
  end
end

8. ルーティングを追加する

Rails.application.routes.draw do
  root to: 'home#index'

  mount ActionCable.server => '/cable'
end

以上です。

参考にさせていただいた情報

qiita.com

qiita.com

qiita.com