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
以上です。