WebSocketsはRails、RUDX、およびRubyでRailsで動作します


注意:この投稿のコードはすべて見つかります.here(frontend) and here (backend/rails)
Rails ActionWire機能(WebSockets)を統合して、ReduxとRedux(Reduxツールキットを通して)を使っている基本的なチャット・アプリケーションで、方法を調査しましょう.私はコードの最も関連したスニペットだけを含んでいますrepo コンテキスト全体.
これは私たちが構築するものです.


バックエンド
私はAPIのエンドポイントとしてRailsを使用しているので、私は--api フラグ.これは、我々が何かを呼ぶとき、ビューが発生するのを防ぎますrails generate したがって、不要なコードを避けるコマンドです.さらに、我々は使用しますpostgresql DBとして.
rails new chat-app-backend-rails --api -database=postgresql

私たちの独立した独立したプロジェクトとして我々のフロントエンドを構築しているので、潜在的に我々のAPIより異なるサーバーに配備されて、我々はクロスドメイン呼び出しを考慮に入れる必要があります.そのためには、まずrack-corsGemfile :
gem 'rack-cors'
そして、config/initializers/cors.rb .
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    # In a prod app you'll restrict to specific origin(s).
    # for this will just allow from any.
    origins '*'

    resource '*',
             headers: :any,
             methods: %i[get post put patch delete options head]
  end
end

我々bundle install 我々は追加宝石をインストールします.
我々のアプリは、単にユーザーとメッセージがあります.そのモデルを作りましょう
rails generate model User
rails generate model Message
我々User のみusername and status これは移行の様子です.
class CreateUsers < ActiveRecord::Migration[7.0]
  def change
    create_table :users do |t|
      t.string :username
      t.string :status
      t.timestamps
    end
  end
end
Message :
class CreateMessages < ActiveRecord::Migration[7.0]
  def change
    create_table :messages do |t|
      t.string :content
      t.timestamps
    end
  end
end

我々のモデルは1-to-many 関係(1ユーザーは多くのメッセージがあります).我々はそれを追加することによってキャプチャされますhas_many :messagesUser and belongs_to on Message .
class User < ApplicationRecord
  has_many :messages, dependent: :destroy
end

class Message < ApplicationRecord
  belongs_to :user
end
最後に、リファレンスを追加するマイグレーションを追加しますuser_id ) メッセージに.
rails generate migration AddBelongToMessages
このコードで
class AddBelongToMessages < ActiveRecord::Migration[7.0]
  def change
    add_belongs_to :messages, :user
  end
end
注:我々は最初に作成したときに我々はこれを追加することができますMessage 移行
最後に、我々はmigrate コマンドrails db:migrate次に、使用するすべてのルートを追加し、ActionCable ( WebSocket )サーバーをマウントしましょう.
  resources :messages, only: %i[index]
  resources :users, only: %i[index create] do
    post 'add_message'
    post 'change_status'
  end
  mount ActionCable.server => '/cable'
これはセットアップのためです.我々は今、いくつかの機能を追加を開始する準備が整いました.の作成を開始しましょうmessages and users チャンネル.我々は、チャットの上で掲示されるメッセージのために、そして、参加しているユーザーのために、これらを使用します.
rails generate channel messages
rails generate channel users
両方の生成されたチャンネルでは、我々は単にsubscribed ストリーミングの場所を指定する方法
class MessagesChannel < ApplicationCable::Channel
  def subscribed
    stream_from 'message_channel'
  end

  def unsubscribed; end
end

class UsersChannel < ApplicationCable::Channel
  def subscribed
    stream_from 'user_channel'
  end

  def unsubscribed; end
end
今、我々はActionCable.server.broadcast() これらのチャネル上のすべての加入者にブロードキャストする方法.我々は、すべての加入者に通知したいuser_channel ユーザーがチャットに参加したとき.また、通知したいmessage_channel メッセージの送信後.両方のことをしましょうUsersController :
class UsersController < ApplicationController
  def index
    users = User.all
    render json: users
  end

  def create
    user = User.new(user_params)
    ActionCable.server.broadcast('user_channel', user) if user.save
    render json: user
  end

  def add_message
    user = User.find(params[:user_id])
    message = params[:message]
    created_message = user.messages.create(content: message)
    ActionCable.server.broadcast('message_channel', created_message) if user.save
    head :ok
  end

  def change_status; end

  def user_params
    params.require(:user).permit(:username, :status)
  end
end
完成のために、私たちもMessagesController これは、チャットに参加したユーザーのためのすべてのメッセージを返します.
class MessagesController < ApplicationController
  def index
    messages = Message.all
    render json: messages
  end
end
これにより、フロントエンドとの統合を必要とするAPIコールがすべてあります.
rails routes | grep users

user_add_message POST   /users/:user_id/add_message(.:format)

user_change_status POST   /users/:user_id/change_status(.:format)

users GET    /users(.:format)

POST   /users(.:format)                                                                                  users#create



rails routes | grep messages

messages GET    /messages(.:format)



フロントエンド
フロントエンドのために、私はReduxとTypesScriptで反応を使用しています.アプリを作成しましょう:
npx create-react-app chat-app-ui --template redux-typescript
このテンプレートでは、ツールキットを使用してReduxを使用するアプリケーションスケルトンを提供します.
を作成して起動します/features/users フォルダ.そこにすべてを追加しますapi and reducer 機能.そこで私はusersAPI ユーザーに関連するすべてのバックエンド呼び出しで.例えば、チャットに新しいユーザを追加する方法です.
export const addNewUser = async (user: UserType): Promise<any> => {
  const res = await fetch("http://localhost:3090/users", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(user),
  });

  return await res.json();
};
そして、これはどのように我々はメッセージを送るユーザーを扱いますか
export const sendUserMessage = async (
  data: sendUserMessageDataType
): Promise<any> => {
  const res = await fetch(
    `http://localhost:3090/users/${data.user.id}/add_message`,
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        user_id: data.user.id,
        message: data.message.content,
      }),
    }
  );

  return await res.json();
};
我々はこれらのAPIを間接的にredux thunksを介して呼び出しを使用します.
フロントエンドの非同期呼び出しで動作する場合、通常は非同期呼び出しを行い、成功した場合、アプリケーションの状態を更新します.thunksで、プロセスは同じです、しかし、すべては還元器自体で扱われます.我々は、アクションを派遣する必要があります後に実行される(例えば、成功した呼び出し)し、状態を更新します.
これは、新しいユーザを追加し、メッセージを送信するために、thunkがどのように見えるかです.
...
export const addUserAsync = createAsyncThunk(
  'users/addUser',
  async (user: UserType) => {
    const response = await addNewUser(user);
    return response;
  }
)

export const sendMessageAsync = createAsyncThunk(
  'users/sendMessage',
  async (data: sendUserMessageDataType) => {
    const response = await sendUserMessage(data);
    return response;
  }
)
...
次に、extraReducers セクションcreateSlice() .
...
  extraReducers: (builder) => {
    builder
      .addCase(sendMessageAsync.fulfilled, (state, action) => {
        let updatedUser: UserType = state.value.filter(user => user.id === action.payload.user.id)[0];
        updatedUser.messages.push(action.payload.message);
        state.value = state.value.map(user => user.id !== updatedUser.id ? user : updatedUser)
      })

      .addCase(addUserAsync.fulfilled, (state, action) => {
        state.value.push(action.payload);
        localStorage.setItem("currentUser", JSON.stringify(action.payload));
        state.userLoggedIn = true;
      })
  },
...
あなたは全体の減速を確認することができますhere .
レールを呼び出すActionCable 我々は、インストールする必要がありますactioncable パッケージ.
npm install --save actioncable

これが我々の使用方法ですactioncableMessages.tsx 新しいメッセージを購読するには、
import { useAppDispatch, useAppSelector } from "../app/hooks";
import { addMessage, selectMessages } from "../features/messages/messagesSlice";
import { MessageType } from "../types";
import Message from "./Message";
import ActionCable from "actioncable";
import { useEffect } from "react";

function Messages() {
  const messages: MessageType[] = useAppSelector(selectMessages);
  const cable = ActionCable.createConsumer("ws://localhost:3090/cable");
  const dispatch = useAppDispatch();

  const createSubscription = () => {
    cable.subscriptions.create(
      { channel: "MessagesChannel" },
      { received: (message) => handleReceivedMessage(message) }
    );
  };

  const handleReceivedMessage = (message: any) => {
    dispatch(addMessage(message));
  };

  useEffect(() => {
    createSubscription();
  }, []);

  return (
    <div className="">
      {messages.map((message) => (
        <Message key={message.id} message={message} />
      ))}
    </div>
  );
}

export default Messages;
我々は、同じアプローチを使用しますUsers.tsx チャットに参加する新しいユーザーを購読する.
すべての構成とスタイルで、これは全体のチャットアプリケーションのようになります:

それを使用すると、我々は、反応、redux、およびRailsとWebSocketsを使用してアプリケーションがあります.