1. rhiroe

    No comment

    rhiroe
Changes in body
Source | HTML | Preview
@@ -1,462 +1,470 @@
# 前置き
今回この記事を書こうと思った理由は3つ。
1つ目は、ActionCableを使ったリアルタイムチャット作成の記事が少ないこと、
2つ目は、既存する記事のRailsや諸々のバージョンが古いこと、
3つ目は、その多くがユーザー認証にDeviseを使うことを前提としていなかったこと。
この記事を投稿して10ヶ月ほど経ちましたが、ありがたいことに見てくださる方がいるので、ちょうどGW10連休なのもあってRuby, Railsもアップデートするついでにちょっと内容を刷新しました。
具体的にはアクション名を適切な名前にしたり、ルーティングに`resources`を使ったり、あとは雑だった説明をもう少し丁寧に...。
自分で書いた記事見ながらもう一度作ってみて、わかりにくいところ加筆したり修正したりって感じですね。
# 本題
今回作成するアプリはユーザー認証にDevise、リアルタイムチャットを実現するためにActionCableを使用します。
作成順序は大まかに以下の通りです。
1. 全てのユーザーが接続できるオープンなチャットアプリを作成
2. グループを作成してそこに参加するメンバーのみでチャットができるよう改良する
## 開発環境
Ruby 2.6.3
Rails 5.2.3
## 参考にした記事
・[Action Cableでリアルタイムチャットアプリの作成方法 (Rails 5.1.4にて)(その1) herokuで動かす!](https://qiita.com/Hijiri-K/items/c3774c72a2cb68e1a720)
・[Rails 5 Action Cable メッセージとルームを紐付ける。](https://qiita.com/kohei1228/items/7aed5aad9c63e834c0e1)
## 完成形イメージ
下のような同じルーム内でのみリアルタイムで投稿内容が表示されるシンプルなアプリです。
![名称未設定.mov.gif](https://qiita-image-store.s3.amazonaws.com/0/262029/f8963451-bba3-493c-8e1e-2a1d0942b7e5.gif)
## ソースコード
https://github.com/eRy-sk/chat_app
## アプリの作成
今回はchat_appという名前でアプリケーションを作成します。
まずは全てのユーザーが接続できるオープンなチャットアプリを作成していきます。
```bash:bash
$ rails new chat_app
```
必要なgemをインストールします。
```:Gemfile
gem 'devise'
gem 'jquery-rails'
```
```bash:bash
$ bundle install
```
jQueryを読み込む
```js:app/assets/javascripts/application.js
//= require jquery
//= require rails-ujs
```
## ユーザー認証
続いてDeviseでサクッとユーザー認証を実現します。
Deviseについての細かな説明は割愛します。
少し検索すればとても参考になる記事がいくつも出てくると思うのでそちらを参考にしてください。
```bash:bash
$ rails g devise:install
$ rails g devise user
$ rails db:migrate
```
サーバーを起動して[http://localhost:3000/users/sign_in](http://localhost:3000/users/sign_in)にアクセスし、ログイン機能を確認。
## チャット機能の作成
### Messageモデルを作成
```bash:bash
# text型のcontentカラムを持つMessageモデルを作成
$ rails g model message content:text
```
### Roomsコントローラの作成
```bash:bash
# showアクションを持つroomsコントローラを作成
$ rails g controller rooms show
```
```rb:routes.rb
Rails.application.routes.draw do
devise_for :users
get 'rooms/show'
end
```
### showアクションの中身を追加
```rb:app/controllers/rooms_controller.rb
class RoomsController < ApplicationController
before_action :authenticate_user! # Deviseのログイン確認
def show
# メッセージ一覧を取得
@messages = Message.all
end
end
```
### rooms#showに対応するviewを作成
```erb:app/views/rooms/show.html.erb
<h1>Chat room</h1>
<div id="messages">
<%= render @messages %>
</div>
```
### message1つ1つを表示するパーシャルを追加
`render @messages`部分に`messages/_message.html.erb`という名前のパーシャルが自動で適用されるようになる
```erb:app/views/messages/_message.html.erb
<div class="messages">
<p><%= message.content %></p>
</div>
```
パーシャルファイル名と置く場所は「変数名」ではなく「テーブル名」に依存するので勘違いなきよう。
`app/views/<テーブル名>/_<テーブル名(単数形)>.html.erb`
```ruby
@messages.table_name
#=> "messages"
```
### 投稿データの登録
```bash:bash
$ rails c
```
```rb:console
Message.create! content: 'Hello'
```
[http://localhost:3000/rooms/show](http://localhost:3000/rooms/show)にアクセスし、投稿したデータが表示されるか確認
### Roomチャネルの作成
```bash:bash
$ rails g channel room speak
```
ブラウザ側のコンソール(デベロッパーツール)を開いて確認
```console:console
App.room.speak()
=> true
```
`app/channel/room_channel.rb`のsubscribedメソッドのコメントアウトを外し、"room_channel"をstreamする。
そしてspeakアクションに引数を用意し、中身を追加する
```rb:app/channel/room_channel.rb
class RoomChannel < ApplicationCable::Channel
def subscribed
stream_from "room_channel"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def speak(data)
# coffeeで実行されたspeakのmessageをmessageとしてroom_channelのreceivedにブロードキャストする
ActionCable.server.broadcast 'room_channel', message: data['message']
end
end
```
channelのspeakを実行させるためにjsでspeakアクションを定義する。
```coffee:app/assets/javascripts/channels/room.coffee
# これが実行されるとコンシューマになったRoomChannelのspeakアクションが引数`{ message: message }`で実行される
speak: (message) ->
@perform 'speak', message: message
```
データを受け取った際のアクション(アラート)を定義して動作確認
```coffee:app/assets/javascripts/channels/room.coffee
# room_channel.rbでブロードキャストされたものがここに届く
received: (data) ->
alert data['message']
```
サーバー起動、ブラウザ側コンソールを開いて確認
```rb:console
App.room.speak("test")
#アラートが表示されればOK
```
### フォームの作成
```erb:app/views/rooms/show.html.erb
<h1>Chat room</h1>
<div id="messages">
<%= render @messages %>
</div>
<%= label nil, nil, 'Say something:' %>
<%= text_field nil, nil, 'data-behavior': 'room_speaker' %>
```
フォーム内でエンターキーを押した時の動作を定義
```coffee:app/assets/javascripts/channels/room.coffee
...
# data-behaviorがroom_speakerである場所で...
$(document).on 'keypress', '[data-behavior~=room_speaker]', (event) ->
# keyCodeが13のキー(エンターキー)を押した時...
if event.keyCode is 13 # return = send
# これまでアラートで確認していたようなものがここで実行される
App.room.speak event.target.value
# フォームの中身を空にする
event.target.value = ''
event.preventDefault()
```
サーバー起動、テキストボックスに文字を入力してエンターを押してアラートが出ればOK
### 入力したものが保存されるように改善
speakアクションの書き換え
```rb:app/channels/room_channel.rb
def speak(data)
# ActionCable.server.broadcast 'room_channel', message: data['message']
Message.create! content: data['message']
end
```
### Broadcastのjobを作成
```bash:bash
$ rails g job MessageBroadcast
```
作成されたjobファイルを編集し、ブロードキャスト処理を追加
```rb:app/jobs/message_broadcast_job.rb
def perform(message)
- ActionCable.server.broadcast "room_channel_#{message.room_id}", message: render_message(message)
+ ActionCable.server.broadcast 'room_channel', message: render_message(message)
end
private
def render_message(message)
ApplicationController.renderer.render partial: 'messages/message', locals: { message: message }
end
```
###messageモデルを編集する
バリデーションとデータ作成後にジョブを実行させるよう追記する
```rb:app/models/message.rb
class Message < ApplicationRecord
validates :content, presence: true
# createの後にコミットする { MessageBroadcastJobのperformを遅延実行 引数はself }
after_create_commit { MessageBroadcastJob.perform_later self }
end
```
サーバー起動、文字入力後エンターを押し、入力した文字入りHTMLがアラートされればOK
### 非同期でアラートの代わりに画面に文字を表示させる
```coffee:app/assets/javascripts/channels/room.coffee
received: (data) ->
$('#messages').append data['message']
```
実際にメッセージを送信して確認
これでチャット機能は実装されました。
## 複数のルームとメッセージの紐付け
ここからは先ほど作ったアプリを改良して、複数ルームのそれぞれのルームの接続者のみにチャットが見えるようにします。
### Roomモデルの作成
```bash:bash
$ rails g model room
```
###モデルの紐付け
Messageモデルに紐付けのためのカラムを追加します。
```bash:bash
$ rails g migration AddColumnsToMessages user:references room:referencese
$ rails db:migrate
```
UserモデルとRoomモデルに紐付けのためのhas_manyを追加
```rb:app/models/user.rb
class Room < ActiveRecord::Base
has_many :messages
...
end
```
```rb:app/models/room.rb
class Room < ActiveRecord::Base
has_many :messages
end
```
##接続ルームのメッセージのみが表示されるようControllerとViewを変更
### Controller
```rb:app/controllers/rooms_controller.rb
class RoomsController < ApplicationController
# ついでにRoom一覧アクションも追加しとこう
def index
@rooms = Room.all.order(:id)
end
def show
@room = Room.find(params[:id])
@messages = @room.messages
end
end
```
```rb:routes.rb
Rails.application.routes.draw do
devise_for :users
# rootはRoom一覧画面にでもしておく
root 'rooms#index'
# resourcesを使うとRESTfulなURLを自動生成できる
resources :rooms, only: %i[show]
end
```
### View
紐づいたデータを表示させるよう修正する。
接続ルームに紐づいたメッセージを全て表示させるためのview
```erb:app/views/rooms/show.html.erb
<div id="messages" data-room_id="<%= @room.id %>">
<%= render @messages %>
</div>
<%= label nil, nil, 'Say something:' %>
<%= text_field nil, nil, 'data-behavior': 'room_speaker' %>
```
接続ルームに紐づいたメッセージ1つ1つを表示するパーシャル
```erb:app/views/messages/_message.html.erb
<div class="message">
<p><%= "#{message.user.email}: #{message.content}" %></p>
</div>
```
ついでにRoom一覧画面も追加しておこう
```erb:app/views/rooms/index.html.erb
<div>
<ul>
<% @rooms.each do |room| %>
<li><%= link_to "ROOM#{room.id}", room_path(room) %></li>
<% end %>
</ul>
</div>
```
## Room.idを受け取り、監視する場所を分ける
```coffee:app/assets/javascripts/channels/room.coffee
document.addEventListener 'turbolinks:load', ->
App.room = App.cable.subscriptions.create { channel: "RoomChannel", room_id: $('#messages').data('room_id') },
connected: ->
disconnected: ->
received: (data) ->
$('#messages').append data['message']
speak: (message)->
@perform 'speak', message: message
$(document).on 'keypress', '[data-behavior~=room_speaker]', (event) ->
if event.keyCode is 13 # return = send
App.room.speak event.target.value
event.target.value = ''
event.preventDefault()
```
```rb:app/channels/room_channel.rb
class RoomChannel < ApplicationCable::Channel
def subscribed
stream_from "room_channel_#{params['room_id']}"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def speak(data)
Message.create!(content: data['message'], user_id: current_user.id, room_id: params['room_id'])
end
end
```
+ブロードキャストする場所もルームごとに分ける
+
+```rb:app/jobs/message_broadcast_job.rb
+ def perform(message)
+ ActionCable.server.broadcast "room_channel_#{message.room_id}", message: render_message(message)
+ end
+```
+
## current_user情報を取得する
Deviseを使っている場合、ログインユーザーのIDはクッキーの以下に格納されている
```rb
cookies.encrypted[Rails.application.config.session_options[:key]][‘warden.user.user.key’][0][0]
```
これを使ってcurrent_userをWebsocket側で使うためのコードを書く
```rb:app/channels/application_cable/connection.rb
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :current_user
def connect
self.current_user = find_verified_user
end
private
def find_verified_user
session_key = cookies.encrypted[Rails.application.config.session_options[:key]]
verified_id = session_key['warden.user.user.key'][0][0]
verified_user = User.find_by(id: verified_id)
return reject_unauthorized_connection unless verified_user
verified_user
end
end
end
```
##完成
あとはRoomの新規作成機能を付け加えたり、メッセージ画面をおしゃれな感じにしたり、Roomの閲覧制限をかけたりこの辺りは簡単にできると思う。
わかんないこととかあったら気軽にコメントかTwitterの方に連絡してくれれば責任を持って説明します。
あと、記事の内容を書き換えたので(一応確認はしたけど)うまく動かないとかがもしあれば教えてください。