railsとvue.jsを使用してchatアプリを作ってみた

#### 環境
- ruby 3.1.4
- rails 7.0.8.1
- vue 3.4.21

#### 事前準備

workspaceという作業環境を構築
バックエンドとフロントエンドで作業環境を分けた。

- 使用したライブラリ
 rails
 ``rack-cors(クロスドメイン対策)``
 ``ActionCable（標準用意されている。サーバ側からクライアントに直接情報）``
 vue.js
 ``axios(HTTPリクエストで使用)``
 ``vue-router（ルーティング処理）``
 ``actioncable``
 

#### バックエンド側

- データベースモデル
```ruby:schema.rb
ActiveRecord::Schema[7.0].define(version: 2024_04_17_075212) do
  create_table "messages", force: :cascade do |t|
    t.text "content"
    t.string "sender_name"
    t.integer "room_id", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["room_id"], name: "index_messages_on_room_id"
  end

  create_table "rooms", force: :cascade do |t|
    t.string "name"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  add_foreign_key "messages", "rooms"
end

```

他にも細かな設定はあるけど省略します

- チャットルームcontoroler

```ruby:rooms_controller.rb
class RoomsController < ApplicationController
    def index
        # Room モデルの全てのデータを取得して、JSON形式でレスポンスを返します。
        rooms = Room.all
        render json: rooms, status: :ok
    end

    def create
        # Room モデルに新しいデータを作成して、JSON形式でレスポンスを返します。
        room = Room.create(room_params)
        render json: room, status: :created
    end

    private

    def room_params
        params.require(:room).permit(:name)
    end
end


```

- チャットのメッセージのcontoroler
```ruby:messages_controller.rb
class MessagesController < ApplicationController
    def index
        # room_id パラメータに一致するRoomオブジェクトを取得
        room = Room.find(params[:room_id])
        messages = room.messages.limit(20) # 取得するデータ件数を20に指定

        render json: messages, status: :ok
    end
    def create
        room = Room.find(params[:room_id])
        message = room.messages.create(message_params)
        render json: message, status: :created
        # メッセージが作成されたときに指定したチャンネルにブロードキャスト(チャンネル経由でフロントエンドに送信)
        ActionCable.server.broadcast "room_channel_#{params[:room_id]}", message
    end

    private

    def message_params
        # キーの指定
        params.require(:message).permit(:content, :sender_name)
    end
end

```

- ルーティング設定
```ruby:routes.rb
Rails.application.routes.draw do
  # API設定
  get '/api/test', to: 'application#test'
  # 特定のアクションへマッピング
  resources :rooms, only: [:index, :create] do
    # チャットルームごとにメッセージを取得できるよう設定
    resources :messages, only: [:index,:create]
  end
end
```

---
#### フロント側

- チャットルームの一覧と作成

```vuejs:ChatRooms.vue
<template>
    <div>
        <h1>VueChat - チャットルーム一覧 </h1>
        <ul>
            <li v-for="room in chatRooms" :key="room.id">
            <!-- aタグみたいなもの -->
            <router-link :to="`/rooms/${room.id}`">{{ room.name }}</router-link>
            </li>
        </ul>
        <h3>チャットルーム作成</h3>
        <input type="text" v-model="newRoomName" />
        <div>
            <button @click="createRoom">作成</button>
        </div>
    </div>
</template>

<script>
import axios from 'axios';

export default {
    data(){
        return {
            chatRooms:[],
            // 新しく作成するチャットルームの名前を保持するためのデータ
            newRoomName:'',
        };
    },
    created() {
        // コンポーネントが作成されたときに、チャットルーム一覧を取得する
        this.fetchChatRooms();
    },
    methods:{
        fetchChatRooms(){
            axios
            .get(`${import.meta.env.VITE_API_URL}/rooms`)
            .then(response => {
                this.chatRooms = response.data;
            })
            .catch(error => {
                console.error(error);
            });
        },
        // newRoomNameを使用して新しいチャットルームを作成する
        createRoom(){
            // 作成フォームの入力内容をサーバーに送信
            axios.post(`${import.meta.env.VITE_API_URL}/rooms`, {
                name: this.newRoomName,
            })
            .then(response => {
                this.chatRooms.push(response.data);
                this.newRoomName = '';
            })
            .catch(error => {
                console.error(error);
            });
        },
    },
};
</script>
```

```vuejs:ChatRoom.vue
<template>
    <div>
        <h1>チャットルーム {{ this.roomId }}</h1>

        <ul>
            <li v-for="message in messages" :key="message.id">
                <strong>{{ message.sender_name }}:</strong> {{ message.content }}
            </li>
        </ul>
        <!-- form -->
        <!-- フォームが送信されたときに sendMessage メソッドを実行 -->
        <form @submit.prevent="sendMessage">
            <div>
                <h3>名前</h3>
                <input type="text" v-model="senderName" placeholder="名前を入力" required />
            </div>
            <div>
                <h3>メッセージ</h3>
                <input type="text" v-model="newMessageContent" placeholder="メッセージを入力" required />
            </div>
            <div>
                <button type="submit">送信</button>
            </div>
        </form>
    </div>
</template>
<script>
import axios from 'axios';
import { inject } from 'vue';

export default {
    props: ['roomId'],
    data(){
        return{
            roomName: '',
            messages: [],
            // メッセージの送信者名
            senderName: '',
            // 送信するメッセージの内容
            newMessageContent: '',
        };
    },
    setup(){
        const cable = inject('cable');
        return { cable };
    },
    created() {
        this.fetchMessages();
        this.createSubscription();
    },
    methods:{
        // 特定のチャンネル（チャットルーム）に対してサブスクリプションを作成
        createSubscription(){
            // 指定したチャンネルに対して新しいサブスクリプションを作成
            this.subscription = this.cable.subscriptions.create(
                // 作成するチャンネル名と受信時の動作定義
                { channel: 'RoomChannel', room_id: this.roomId },
                {
                    // メッセージ受信時の動作
                    received: message => {
                        console.log(message);
                        this.messages.push(message);
                    },
                }
            );
        },
        fetchMessages(){
            // 特定のチャットルームのメッセージを取得
            axios
            .get(`${import.meta.env.VITE_API_URL}/rooms/${this.roomId}/messages`)
            .then((response) => {
                this.messages = response.data;
            })
            .catch((error) => {
                console.error(error);
            });
        },
        // メッセージ送信
        sendMessage(){
            axios
            // 送信先のURLは、チャットルームのIDとメッセージを組み合わせたもの
            .post(`${import.meta.env.VITE_API_URL}/rooms/${this.roomId}/messages`, {
                // 内容
                content: this.newMessageContent,
                // 送信者名
                sender_name: this.senderName
            })
            .then(() => {
                // newMessageContent を空にしてフォームをリセット（次回入力の際にメッセージを残さない）
                this.newMessageContent = ''
            })
            .catch((error) => {
                console.error(error)
            })
        }
    }
}
</script>
```

- ルーティング
```javascript:index.js
import { createRouter,createWebHistory } from "vue-router";
import ChatRooms from "../components/ChatRooms.vue";
import ChatRoom from '../components/ChatRoom.vue';

const routes = [
    { path: '/', component: ChatRooms },
    { path: '/rooms/:id', component: ChatRoom, props: route =>({ roomId: route.params.id }) },
];

const router = createRouter({
    history: createWebHistory(),
    routes
});

export default router;
```

- メイン
```javascript:main.js
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import ActionCable from 'actioncable';
// Rails側のActionCableサーバとの接続
const cable = ActionCable.createConsumer('URLが入ります');

const app = createApp(App);
app.use(router);
// アプリケーション全体から利用できるよう設定
app.provide('cable',cable);
app.mount('#app')

```

---

APIを自分で作成して、それを繋げるという作業自体はあまりやったことなかったが仕組み的なとこは理解できたと思う。
railsも前から触っていた言語で特に困ることもなく、、、

vueのactioncableや記述方法がいまいち理解が足りてないかな、、、、、


出来上がりはこんな感じ
デザインも何もしていないのでかなり殺伐としているが、、、

![スクリーンショット 2024-04-22 14.02.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2724774/02e2f1e5-e764-f153-fd3d-e21d4294f63f.png)