1. rhiroe

    ファイル名を修正

    rhiroe
Changes in body
Source | HTML | Preview
@@ -1,566 +1,566 @@
# 前置き
そこそこRailsが扱えるようになりました。
ありがたいことに色々コメントをいただく機会もあり、改めて見直してみたところ、ちょっとうまく動かないところとかもあり、Rails自体のバージョンも上がっているので最新版の6.0で書き直すことにしました。
# 本題
今回作成するアプリはユーザー認証にDevise、リアルタイムチャットを実現するためにActionCableを使用します。
作成順序は大まかに以下の通りです。
1. 全てのユーザーが接続できるオープンなチャットアプリを作成
2. グループを作成してそこに参加するメンバーのみでチャットができるよう改良する
## 開発環境
Ruby 2.6.3
Rails 6.0.0
## 参考にした記事
・[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/erysk/actioncable
https://github.com/erysk/chat_app (古いバージョンでLINE風にして遊んだみたもの)
## アプリの作成
今回はchat_appという名前でアプリケーションを作成します。
まずは全てのユーザーが接続できるオープンなチャットアプリを作成していきます。
テストは作成しません。
DBはデフォルトのSQLiteを使います。
まずはrailsアプリケーションを作成します。
```bash:bash
$ mkdir chat_app
$ cd chat_app
$ bundle init
```
```rb:Gemfile
# frozen_string_literal: true
source "https://rubygems.org"
git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
gem 'rails', '6.0.0'
```
```bash:bash
$ bundle install
$ rails _6.0.0_ new . -T
```
この`-T`というオプションはテストを作成しませんよというものです。
`--skip-test`の略です。Minitest関連のファイルが自動生成されなくなります。
`Gemfile`を上書きしますか?と聞かれるので上書きしてください。
Rails6ではデフォルトでwebpackerを使うようになっていますので、
初めての場合`yarn`をインストールする必要があったりするかと思います。
だいたいは`brew install yarn`とかで解決すると思いますが、調べてみてください。
エラーなどでyarnを新しくインストールした場合は再度`rails webpacker:install`を実行してください。
Gemfileに必要なgemを追加し、インストールします。
```rb:Gemfile
gem 'devise'
```
```bash:bash
$ bundle install
```
## ユーザー認証
続いてDeviseでサクッとユーザー認証を実現します。
Deviseについての細かな説明は割愛します。
少し検索すればとても参考になる記事がいくつも出てくると思うのでそちらを参考にしてください。
```bash:bash
$ rails g devise:install
$ rails g devise user
$ rails db:migrate
```
たまに`rails g devise:install`で固まることがあります。
その場合は`spring stop`でspringを止めてから再度試してください。
サーバーを起動して[http://localhost:3000/users/sign_in](http://localhost:3000/users/sign_in)にアクセスし、ログイン機能を確認。
## チャット機能の作成
### Messageモデルを作成
```bash:bash
# text型のcontentカラムを持つMessageモデルを作成
$ rails g model message content:text
$ rails db:migrate
```
### 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>
```
`render @messages`とは`render partial: 'messages/message', collection: @messages`の略です。
メッセージ毎に`messages/_message.html.erb`というパーシャルが適用されるようになります。
### message1つ1つを表示するパーシャルを追加
```erb:app/views/messages/_message.html.erb
<div class='message'>
<p><%= message.content %></p>
</div>
```
この中の`message`変数の中に`@messages`のそれぞれのレコードが代入されています。
### 投稿データの登録
```bash:bash
$ rails c
```
```rb:ruby
Message.create! content: 'Hello'
```
[http://localhost:3000/rooms/show](http://localhost:3000/rooms/show)にアクセスし、投稿したデータが表示されるか確認
### Roomチャネルの作成
ActionCableの機能を使うためにチャネルを作成します。
その前にjQueryを使えるようにしておきましょう。
```bash:bash
$ yarn add jquery
```
```js:config/webpack/environment.js
const { environment } = require('@rails/webpacker');
const webpack = require('webpack');
environment.plugins.append('Provide',
new webpack.ProvidePlugin({
$: 'jquery/src/jquery',
jQuery: 'jquery/src/jquery'
})
);
module.exports = environment;
```
```js:app/javascript/packs/application.js
// 追加
require('jquery');
```
デベロッパーツールのコンソールでバージョンが表示されればOK。
```js:console
console.log($.fn.jquery);
// 3.4.1
```
`speak`メソッドを持つ`room`チャネルを作成します。
```bash:bash
$ rails g channel room speak
```
`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)
# jsで実行されたspeakのmessageを受け取り、room_channelのreceivedにブロードキャストする
ActionCable.server.broadcast 'room_channel', message: data['message']
end
end
```
channelのspeakを実行させるためにjsでspeak関数を定義する。
```js:app/javascript/channels/room_channel.js
consumer.subscriptions.create("RoomChannel", {
// ...
// これが実行されるとコンシューマになったRoomChannel#speak({ message: message })が呼ばれる
speak: function(message) {
return this.perform('speak', {
message: message
});
}
});
```
データを受け取った際のアクションを定義する。
```js:app/javascript/channels/room_channel.js
consumer.subscriptions.create("RoomChannel", {
// ...
// room_channel.rbでブロードキャストされたものがここに届く
received: function(data) {
return alert(data['message']);
},
// ...
});
```
### フォームの作成
```erb:app/views/rooms/show.html.erb
<h1>Chat room</h1>
<div id='messages'>
<%= render @messages %>
</div>
<%= label_tag :content, 'Say something:' %>
<%= text_field_tag :content, nil, data: { behavior: 'room_speaker' } %>
```
フォーム内でエンターキーを押した時の動作を定義
```js:app/javascript/channels/room_channel.js
const chatChannel = consumer.subscriptions.create("RoomChannel", {
// ...
});
$(document).on('keypress', '[data-behavior~=room_speaker]', function(event) {
if (event.keyCode === 13) {
chatChannel.speak(event.target.value);
event.target.value = '';
return 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: 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
### 非同期でアラートの代わりに画面に文字を表示させる
```js:app/javascript/channels/room_channel.js
received: function(data) {
return $('#messages').append(data['message']);
}
```
実際にメッセージを送信して確認
これでチャット機能は実装されました。
## 複数のルームとメッセージの紐付け
ここからは先ほど作ったアプリを改良して、複数ルームのそれぞれのルームの接続者のみにチャットが見えるようにします。
### Roomモデルの作成
```bash:bash
$ rails g model room
$ rails db:migrate
```
###モデルの紐付け
Messageモデルに紐付けのためのカラムを追加します。
```bash:bash
$ rails g migration AddUserRefAndRoomRefToMessages user:references room:references
```
うまいこと自動生成されてなかった場合は手直しする。
-```rb:_add_columns_to_messages.rb
-class AddColumnsToMessages < ActiveRecord::Migration[6.0]
+```rb:add_user_ref_and_room_ref_to_messages.rb
+class AddUserRefAndRoomRefToMessages < ActiveRecord::Migration[6.0]
def change
add_reference :messages, :user, null: false, foreign_key: true
add_reference :messages, :room, null: false, foreign_key: true
end
end
```
デフォルト値の無いNotNullなカラムをいきなり追加しようとするとエラーになるので小細工する。
-```rb:_add_columns_to_messages.rb
+```rb:add_user_ref_and_room_ref_to_messages.rb
class AddUserRefAndRoomRefToMessages < ActiveRecord::Migration[6.0]
def change
add_reference :messages, :user, foreign_key: true
add_reference :messages, :room, foreign_key: true
change_column_null :messages, :user_id, false
change_column_null :messages, :room_id, false
end
end
```
```bash:bash
$ rails db:migrate
```
UserモデルとRoomモデルに紐付けのためのhas_manyとbelongs_toを追加
```rb:app/models/user.rb
class User < ApplicationRecord
# ...
has_many :messages
end
```
```rb:app/models/room.rb
class Room < ApplicationRecord
has_many :messages
end
```
```rb:app/models/message.rb
class Message < ApplicationRecord
# ...
belongs_to :user
belongs_to :room
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
<%# ここの data-room_id を使ってjs側で部屋を見分ける %>
<div id='messages' data-room_id="<%= @room.id %>">
<%= render @messages %>
</div>
<%= label_tag :content, 'Say something:' %>
<%= text_field_tag :content, 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を受け取り、監視する場所を分ける
```js:app/javascript/channels/room_channel.js
import consumer from './consumer'
// $(function() {}; で囲むことでレンダリング後に実行される
// レンダリング前に実行されると $('#messages').data('room_id') が取得できない
$(function() {
const chatChannel = consumer.subscriptions.create({ channel: 'RoomChannel', room: $('#messages').data('room_id') }, {
connected() {
// Called when the subscription is ready for use on the server
},
disconnected() {
// Called when the subscription has been terminated by the server
},
received: function(data) {
return $('#messages').append(data['message']);
},
speak: function(message) {
return this.perform('speak', {
message: message
});
}
});
$(document).on('keypress', '[data-behavior~=room_speaker]', function(event) {
if (event.keyCode === 13) {
chatChannel.speak(event.target.value);
event.target.value = '';
return event.preventDefault();
}
});
});
```
```rb:app/channels/room_channel.rb
class RoomChannel < ApplicationCable::Channel
def subscribed
stream_from "room_channel_#{params['room']}"
end
# ...
def speak(data)
Message.create! content: data['message'], user_id: current_user.id, room_id: params['room']
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)
# こんなややこしくしなくても↓で取れました。
verified_user = User.find_by(id: env['warden'].user.id)
return reject_unauthorized_connection unless verified_user
verified_user
end
end
end
```
##完成
あとはログインしないとチャットできないようにしたり、Roomの新規作成機能を付け加えたり、メッセージ画面をおしゃれな感じにしたり、Roomの閲覧制限をかけたりこの辺りは簡単にできると思う。
わかんないこととかあったら気軽にコメントかTwitterの方に連絡してくれれば責任を持って説明します。
あと、記事の内容を書き換えたので(一応確認はしたけど)うまく動かないとかがもしあれば教えてください。