7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Action Cable+ReactでWebSocketのアプリケーションを0から作る

Last updated at Posted at 2022-11-07

RailsのAction CableとReactの組み合わせでWebSocketのアプリケーションを0から作ってローカルで動作させました。

この記事では下記のチャットアプリの作った手順を紹介します。
下記の動画では、上下のブラウザでwebsocketでチャットしています。

ezgif-1-7afa5d7957.gif

主要ライブラリなどのバージョン

フロントエンドは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を使用します。

config/cable.yml
development:
  adapter: redis
  url: <%= ENV.fetch("REDIS_URL") { "redis://redis/1" } %>
  channel_prefix: app_development

許可されたリクエスト送信元

後にReactで作るクライアントのドメインを追加します。

config/environments/development.rb
config.action_cable.allowed_request_origins = [ "https://localhost:5173" ]

マウントパス

ActionCableにアクセスするためのパスを設定します。

config/application.rb
config.action_cable.mount_path = "/cable"

urlも設定(こちらは設定しなくても良いかも?)

config/environments/development.rb
config.action_cable.url = "wss://localhost:3020/cable"

Userモデル作成

チャットするユーザーを管理するためUserモデルを作成します。

docker compose exec api rails g model User

今回はidと名前しか使わないので、nameカラムを追加しました。

db/migrate/20221102162025_create_users.rb
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ユーザー作るようにしたので実行しておきます。

db/seeds.rb
User.insert_all((1..100).map { { name: "ham#{_1}" } })
docker conpose exec api rails db:seed  

Connectionクラスを実装

クライアントからアクセスされてきた時に認可するConnectionクラスを実装します。

本来はcookieなどを利用して、接続してきたクライアントに対応したユーザーを特定して認可するところですが、今回は動作確認したいだけなので、先ほどseedで作った100ユーザーをランダムで1名返すようにしました。

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!
      # 本来はcookieなどを使い接続してきたクラウアントのユーザーを検索するが
      # 今回は動作確認したいだけなのでランダムでユーザーを1人返す
      User.all.sample
    end
  end
end

Channelクラスを実装

クライアントとのやりとりを行うチャネルクラスを実装します。

  • サブスクライブしたらsubscribedメソッドが呼び出され、chatをサブスクライブします。
  • クライアントからメッセージをつけてchatメソッドを呼び出すと、メッセージをブロードキャストします。
app/channels/chat_channel.rb
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に関わる部分にはコメントを入れています。

src/Cable/index.tsx
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\"}"
7
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?