1. rhiroe

    No comment

    rhiroe
Changes in body
Source | HTML | Preview
@@ -1,446 +1,463 @@
-#編集履歴
-'18.10.12 修正
-`find_verified_user`メソッド内のif文が必ず`false`になってしまっていたので修正
-
-'18.6.27 追記
-`cookies.encrypted[Rails.application.config.session_options[:key]][‘warden.user.user.key’][0][0]`にログインしているユーザーのID情報を発見。記事を修正しました。
-
-#前置き
+# 前置き
今回この記事を書こうと思った理由は3つ。
1つ目は、ActionCableを使ったリアルタイムチャット作成の記事が少ないこと、
2つ目は、既存する記事のRailsや諸々のバージョンが古いこと、
3つ目は、その多くがユーザー認証にDeviseを使うことを前提としていなかったこと。
-などと偉そうなとを言っている私は、実はまだ未経験で入社して2ヶ月目のひよっこですので、もしおかしなところや説明不足なところがございましたら教えてください🙏
-
-#本題
-今回作成するアプリはユーザー認証にDevise、リアルタイムチャットを実現するためにActionCableを使用します。順序としては全てのユーザーが接続できるオープンなチャットアプリを作成した後、グループを作成してそこに参加するメンバーのみでチャットができるよう改良します。
+の記事を投稿して10ヶ月ほど経ちましたが、ありがたいことに見てくださる方がいるので、ちょうどGW10連休なのもあってRuby, Railsもアップデートするついでにちょっと内容を刷新しました。
+具体的にはアクション名を適切な名前にしたり、ルーティングに`resources`を使ったり、あとは雑だった説明をもう少し丁寧に...。
+自分で書いた記事見ながらもう一度作ってみて、わかりにくいところ加筆したり修正したりって感じですね。
+
+# 本題
+今回作成するアプリはユーザー認証にDevise、リアルタイムチャットを実現するためにActionCableを使用します。
+作成順序は大まかに以下の通りです。
+1. 全てのユーザーが接続できるオープンなチャットアプリを作成
+2. グループを作成してそこに参加するメンバーのみでチャットができるよう改良する
+
+## 開発環境
+Ruby 2.6.3
+Rails 5.2.3
-##開発環境
-Ruby 2.5
-Rails 5.2
-
-##参考にした記事
+## 参考にした記事
・[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)
-##ソースコード
-必要ないかもしれませんがGitHubのURLを貼っておきます。
-
+## ソースコード
https://github.com/eRy-sk/chat_app
-##アプリの作成
+## アプリの作成
今回はchat_appという名前でアプリケーションを作成します。
+まずは全てのユーザーが接続できるオープンなチャットアプリを作成していきます。
-```:bash
+```bash:bash
$ rails new chat_app
```
必要なgemをインストールします。
```:Gemfile
gem 'devise'
gem 'jquery-rails'
```
-```:bash
+```bash:bash
$ bundle install
```
jQueryを読み込む
```js:app/assets/javascripts/application.js
//= require jquery
//= require rails-ujs
```
-##ユーザー認証
+## ユーザー認証
続いてDeviseでサクッとユーザー認証を実現します。
+Deviseについての細かな説明は割愛します。
+少し検索すればとても参考になる記事がいくつも出てくると思うのでそちらを参考にしてください。
-```:bash
+```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)にアクセスし、実装されたか確認します
+サーバーを起動して[http://localhost:3000/users/sign_in](http://localhost:3000/users/sign_in)にアクセスし、ログイン機能を確認
-##チャット機能の作成
-###ControllerとModelを作成
+## チャット機能の作成
+### Messageモデルを作成
-```:bash
-$ rails g controller rooms show
+```bash:bash
+# text型のcontentカラムを持つMessageモデルを作成
$ rails g model message content:text
```
-###showアクションの追加
+### 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
```
-###message一覧を表示させるviewを作成
+### rooms#showに対応するviewを作成
-```html:app/views/rooms/show.html.erb
+```erb:app/views/rooms/show.html.erb
<h1>Chat room</h1>
<div id="messages">
<%= render @messages %>
</div>
```
-###message1つ1つを表示するパーシャルを追加
+### message1つ1つを表示するパーシャルを追加
+`render @messages`部分に`messages/_message.html.erb`という名前のパーシャルが自動で適用されるようになる
+
-```html:app/views/messages/_message.html.erb
+```erb:app/views/rooms/_message.html.erb
<div class="messages">
<p><%= message.content %></p>
</div>
```
-###投稿データの登録
+パーシャルファイル名と置く場所は「変数名」ではなく「テーブル名」に依存するので勘違いなきよう。
+`app/views/<テーブル名>/_<テーブル名(単数形)>.html.erb`
+
+```ruby
+@messages.table_name
+#=> "messages"
+```
+
+### 投稿データの登録
-```:bash
+```bash:bash
$ rails c
```
```rb:console
- Message.create! content: "Hello"
+ Message.create! content: 'Hello'
```
[http://localhost:3000/rooms/show](http://localhost:3000/rooms/show)にアクセスし、投稿したデータが表示されるか確認
-###Roomチャネルの作成
+### Roomチャネルの作成
-```:bash
+```bash:bash
$ rails g channel room speak
```
-ブラウザ側のコンソールを開いて確認(デベロッパー ツール)
+ブラウザ側のコンソール(デベロッパーツール)を開いて確認
-```rb:console
+```console:console
App.room.speak()
-=>true
-```
-
-speakアクションを定義する
-
-```coffee:app/assets/javascripts/channels/room.coffee
- speak: (message) ->
- @perform 'speak', message: message
-
+=> true
```
-subscribedアクションのコメントアウトを外し、room_channelをstreamする
+`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) #サーバーサイドのspeakアクションの定義
+ 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
+```
+
データを受け取った際のアクション(アラート)を定義して動作確認
-```coffeescript:app/assets/javascripts/channels/room.coffee
+```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>
+
-```html:app/views/rooms/show.html.erb
-<form>
- <label>Say something:</label><br>
- <input type="text" data-behavior="room_speaker">
-</form>
+<%= 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を作成
+### Broadcastのjobを作成
-```:bash
+```bash:bash
$ rails g job MessageBroadcast
```
作成されたjobファイルを編集し、ブロードキャスト処理を追加
```rb:app/jobs/message_broadcast_job.rb
def perform(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})
+ ApplicationController.renderer.render partial: 'messages/message', locals: { message: message }
end
```
###messageモデルを編集する
+バリデーションとデータ作成後にジョブを実行させるよう追記する
+
```rb:app/models/message.rb
class Message < ApplicationRecord
- validates :content, presence: true #これにより無記入投稿とエンター長押しの連続投稿の2つが同時に防げる
- after_create_commit {MessageBroadcastJob.perform_later self}
+ validates :content, presence: true
+ # createの後にコミットする { MessageBroadcastJobのperformを遅延実行 引数はself }
+ after_create_commit { MessageBroadcastJob.perform_later self }
end
```
-サーバー起動、文字入力後エンターを押し、入力した文字入りアラートが表示されればOK
+サーバー起動、文字入力後エンターを押し、入力した文字入りHTMLがアラートされればOK
-###非同期で画面に入力された文字を表示させる
+### 非同期でアラートの代わりに画面に文字を表示させる
```coffee:app/assets/javascripts/channels/room.coffee
received: (data) ->
$('#messages').append data['message']
```
実際にメッセージを送信して確認
これでチャット機能は実装されました。
-##複数のルームとメッセージの紐付け
+## 複数のルームとメッセージの紐付け
ここからは先ほど作ったアプリを改良して、複数ルームのそれぞれのルームの接続者のみにチャットが見えるようにします。
-###Roomモデルの作成
+### Roomモデルの作成
-```:bash
+```bash:bash
$ rails g model room
```
###モデルの紐付け
Messageモデルに紐付けのためのカラムを追加します。
-```:bash
+```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
+### 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
```
-###View
+```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
-```html:app/views/rooms/show.html.erb
-<div id="messages", data-room_id="<%= @room.id %>">
+```erb:app/views/rooms/show.html.erb
+<div id="messages" data-room_id="<%= @room.id %>">
<%= render @messages %>
</div>
-<div class="input">
- <form>
- <label>post</label>
- <input type="text" data-behavior="room_speaker">
- </form>
-</div>
+
+<%= label nil, nil, 'Say something:' %>
+<%= text_field nil, nil, 'data-behavior': 'room_speaker' %>
```
接続ルームに紐づいたメッセージ1つ1つを表示するパーシャル
-```html:app/views/messages/_message.html.erb
+```erb:app/views/messages/_message.html.erb
<div class="message">
<p><%= "#{message.user.email}: #{message.content}" %></p>
</div>
```
-##Room.idを受け取り、監視する場所を分ける
+ついでに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
```
-##current_user使えない問題
-
-Websocket側はモデルのインスタンスを参照できないからcurrent_userが使えない(だったかな)。
+## current_user情報を取得する
-そこでcookieの`cookies.signed["user.id"]`からログイン中のユーザーのIDを抜き出してcurrent_userを作ろうって話。
+Deviseを使っている場合、ログインユーザーのIDはクッキーの以下に格納されている
-###connection.rbに色々書いて設定
-
-```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
- verified_user = User.find_by(id: cookies.signed["user.id"])
- return verified_user if verified_user && cookies.signed['user.expires_at'] > Time.now
- reject_unauthorized_connection
- end
- end
-end
+```rb
+cookies.encrypted[Rails.application.config.session_options[:key]][‘warden.user.user.key’][0][0]
```
-##それでもcurrent_userが使えない問題
-
-Deviseを使ってると`cookies.signed["user.id"]`にログイン中のユーザーのIDが格納されないらしい。詳しくはWardenを学ぼう。
-
----
-[追記 '18.6.27]
-どこにあるかというと
-
-`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
```
-に変更することでログインユーザーのID情報をもとにcurrent_userを定義できます。
-
-もしくは↓でも可能。
-
-
-仕方ないので`cookies.signed["user.id"]`にログイン中のユーザーのIDが格納されるようinitializers/warden_hooks.rbを追加して設定。
-
-```rb:config/initializers/warden_hooks.rb
-Warden::Manager.after_set_user do |user,auth,opts|
- scope = opts[:scope]
- auth.cookies.signed["#{scope}.id"] = user.id
- auth.cookies.signed["#{scope}.expires_at"] = 30.minutes.from_now
-end
-
-Warden::Manager.before_logout do |user, auth, opts|
- scope = opts[:scope]
- auth.cookies.signed["#{scope}.id"] = nil
- auth.cookies.signed["#{scope}.expires_at"] = nil
-end
-```
-
##完成
-やったね。
-あとは必要に応じてルームを作成するアクションやviewを作ってルーティング設定して、実際に動かして確認してみてね
+あとはRoomの新規作成機能を付け加えたり、メッセージ画面をおしゃれな感じにしたり、Roomの閲覧制限をかけたりこの辺りは簡単にできると思う
-この記事はアプリ作ってしばらくして、思い出しながら書いたので間違いがあったらごめんなさい。
-うまくいかない場合報告していただければ…
+わかんないこととかあったら気軽にコメントかTwitterの方に連絡してくれれば責任を持って説明します
-おわり
+あと、記事の内容を書き換えたので(一応確認はしたけど)うまく動かないとかがもしあれば教えてください