RailsのAction CableとReactの組み合わせでWebSocketのアプリケーションを0から作ってローカルで動作させました。
この記事では下記のチャットアプリの作った手順を紹介します。
下記の動画では、上下のブラウザでwebsocketでチャットしています。
主要ライブラリなどのバージョン
フロントエンドはReact、バックエンドはRailsを使っています。
主要なライブラリのバージョンは下記の通り。
バックエンド
ライブラリ | バージョン |
---|---|
Ruby | 3.1.2 |
Rails | 7.0.4 |
Redis | 7.0.5 |
Redis(Gem) | 5.0.5 |
全ソースを確認したい場合、GitHubに完成したときのタグ[0.0.1]を残しているのでご覧ください。
フロントエンド
ライブラリ | バージョン |
---|---|
React | 18.2.0 |
Vite | 3.2.2 |
actioncable | 5.2.8 |
※ 2022/11/14追記 actioncable
の最新版は @rails/actioncable
のようです。
全ソースを確認したい場合、GitHubに完成したときのタグ[0.0.1]を残しているのでご覧ください。
バックエンド
まずはAction Cableのサーバーサイドを構築します。
Rails New
バックエンドはDockerで動かすため、事前にRails(api)とMySQL(db)とRedis(redis)が動作するコンテナを作ります(括弧内は今回設定したコンテナ名)。
Docker環境の構築は本題からずれるので割愛します。
コンテナができたら早速Rails Newします。今回は下記のコマンドを使いました。使わないサービスはOFFにしています。
docker compose run api rails new . --skip-git --skip-action-mailer --skip-action-mailbox --database=mysql --skip-action-text --skip-active-storage --skip-asset-pipeline --skip-javascript --skip-hotwire --skip-jbuilder --skip-test --skip-system-test --skip-bootsnap --api --force
ローカルをhttpsで動くようにする
今はhttpsで動かさないことはないと思うので、ローカルでもhttpsで動くようにします。
下記の記事通りやればサクッとできました。httpsで開発環境に接続できれば動作確認OKです。
Action Cableの設定
各種設定を行います。
サブスクリプションアダプタ
サブスクリプションを管理するアダプタを設定します。
デフォルトではdevelopmentにasync
が設定されていますが、プロダクション環境に近づけるためにredisを使用します。
development:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://redis/1" } %>
channel_prefix: app_development
許可されたリクエスト送信元
後にReactで作るクライアントのドメインを追加します。
config.action_cable.allowed_request_origins = [ "https://localhost:5173" ]
マウントパス
ActionCableにアクセスするためのパスを設定します。
config.action_cable.mount_path = "/cable"
urlも設定(こちらは設定しなくても良いかも?)
config.action_cable.url = "wss://localhost:3020/cable"
Userモデル作成
チャットするユーザーを管理するためUserモデルを作成します。
docker compose exec api rails g model User
今回はidと名前しか使わないので、nameカラムを追加しました。
class CreateUsers < ActiveRecord::Migration[7.0]
def change
create_table :users do |t|
t.string :name, null: false
t.timestamps
end
end
end
テストデータを作成するため、seedで雑に100ユーザー作るようにしたので実行しておきます。
User.insert_all((1..100).map { { name: "ham#{_1}" } })
docker conpose exec api rails db:seed
Connectionクラスを実装
クライアントからアクセスされてきた時に認可するConnectionクラスを実装します。
本来はcookieなどを利用して、接続してきたクライアントに対応したユーザーを特定して認可するところですが、今回は動作確認したいだけなので、先ほどseedで作った100ユーザーをランダムで1名返すようにしました。
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!
# 本来はcookieなどを使い接続してきたクラウアントのユーザーを検索するが
# 今回は動作確認したいだけなのでランダムでユーザーを1人返す
User.all.sample
end
end
end
Channelクラスを実装
クライアントとのやりとりを行うチャネルクラスを実装します。
- サブスクライブしたら
subscribed
メソッドが呼び出され、chat
をサブスクライブします。 - クライアントからメッセージをつけて
chat
メソッドを呼び出すと、メッセージをブロードキャストします。
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from 'chat'
end
def unsubscribed
end
def chat(data)
ActionCable.server.broadcast('chat', { sender: current_user.name, body: data['body'] })
end
end
テスト
今回はテストを書かなかったですが、Railsガイドにテストのやり方も記載されていたのでリンクを載せておきます。
フロントエンド
次にフロントエンドを構築します。
Vite
Reactの環境をゼロから作るため、Viteを使いました。
初めて使いましたが、npm create vite@latest
を実行するだけでサクッと構築できてめちゃくちゃ楽でした!
ローカルをhttpsで動くようにする
クライアントもhttpsで動くようにします。
下記の記事を参考に設定しました。httpsで開発環境に接続できれば動作確認OKです。
actioncable
Railsから提供されているactioncableを追加します。
Typescriptを使っているので、@types/actioncableも追加します。
クライアントの実装
最後にクライアントを実装します。
Action Cableに関わる部分にはコメントを入れています。
import { useState, useMemo, useEffect } from 'react';
import ActionCable from 'actioncable';
type Message = {
sender: string;
body: string;
};
export default function Cable() {
const [receivedMessage, setReceivedMessage] = useState<Message>();
const [text, setText] = useState('');
const [input, setInput] = useState('');
const [subscription, setSubscription] = useState<ActionCable.Channel>();
// Action Cableに接続
const cable = useMemo(() => ActionCable.createConsumer('wss://localhost:3020/cable'), []);
useEffect(() => {
// ChatChannelをサブスクライブ
// receivedにメッセージを受信した時のメソッドを設定します。
// 今回はreceivedMessageにメッセージをセットします。
const sub = cable.subscriptions.create({ channel: "ChatChannel" }, {
received: (msg) => setReceivedMessage(msg)
});
setSubscription(sub);
}, [cable]);
const handleSend = () => {
// inputをサーバーに送信
subscription?.perform('chat', { body: input });
setInput('');
};
useEffect(() => {
if (!receivedMessage) return;
const { sender, body } = receivedMessage;
setText(text.concat("\n", `${sender}: ${body}`));
}, [receivedMessage]);
useEffect(() => {
const history = document.getElementById('history');
history?.scrollTo(0, history.scrollHeight);
}, [text]);
const onChangeInput = (e) => {
setInput(e.currentTarget.value);
};
return (
<div>
<div>
<textarea id="history" readOnly style={{ width: "500px", height: "200px" }} value={text} />
</div>
<div>
<input
type="text"
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleSend();
}
}}
style={{ width: "400px", marginRight: "10px" }}
onChange={onChangeInput}
value={input}
/>
<button onClick={handleSend} disabled={input === ''}>
send
</button>
</div>
</div >
);
}
動作確認
一通り実装は終わったので最後に動作確認です。
クライアント
ChromeのDevToolを使い、コンソールログや通信状況を確認します。
通信状況はDevTool > Netwookで表示できます。WSでフィルターすると見やすいです。
サーバー
Dockerのログをdocker compose logs -f
で確認したり、Railsのログlog/development.log
などを確認します。
うまく動かない場合はログを追加したりdebuggerで止めるなどして検証しましょう。
今回はRedisを使っているので、Redisのログも確認します。
Redisサーバーにアクセスしてredis-cli
を実行し、monitor
コマンドで確認できます。
% x redis bash
root@8cfed9bcac8e:/data# redis-cli
127.0.0.1:6379> monitor
OK
1667539510.712099 [1 192.168.208.4:37452] "publish" "app_development:chat" "{\"body\":\"sent: 87\"}"