SNSと掲示板の違い
エンジニアなら誰しも一回はLINEのようなSNSアプリを作りたいと思ったことはあるはずです
SNSもどきの例
問題点
- リアルタイムで更新されていないためコミュニケーションが図れない!!!
Twitterのタイムラインで手動で更新する形、掲示板でスーパーリロードをかけて更新する場合とは大きく性質が異なる。画面開いたままのユーザーもいるため画面遷移時にAPIを叩いていたのではまともな会話にならない。
今回使うもの
フロント用のテンプレート
React Typescript
APIのテンプレート
Docker Ruby On Rails
他の人のパクってエラーでた箇所修正しました
よかったら使ってください
Branch切って番号降ってるので進捗度合いもわかると思います
簡易な解決方法 (多分ダメ、最悪)
setIntervalを使って毎秒ごとに更新をかける
setInterval(() => {
connectTest();
}, 1000);
const connectTest = async () => {
await axios
.get('http://localhost:3000/test/tests/index')
.then((res) => {
setMessages(res.data.messages);
})
.catch((e) => console.log(e));
};
[結果]取り合えずリアルタイム更新は実現した
問題点
- メッセージが更新されていなくてもAPIを叩く羽目になる。
- 早期リターンしたとしても負荷がかかる
- 再レンダリングがかかってたりする
- 大規模で同時接続数が多いアプリでこの方法は多分成り立たない
- 多少とはいえラグありそう
APIを不必要に何回も叩くのは絶対良くない。これは初学者の僕でもわかる
AJAXポーリングというらしい
本題 WebSocketを使おう
WebSocketとは
HTTP通信は通常、ブラウザなどからの要求にサーバーが応答を返すという形でしか通信できない。
webSocketは、必要に応じてサーバーからクライアントに対して通信を行える。双方向通信と呼ばれる通信方法を実現するシステムのことで、リアルタイムチャットのチャット機能を作るにはとても便利。
こういうの見た方が早い
ちなみに自分でまとめたものもあるのでよかったら見てください
Action Cable
Action Cableは、Railsのアプリケーションと同様の記述で、WebSocket通信という双方向の通信によるリアルタイム更新機能を実装できるフレームワークで、Rails5から実装されました。
Action Cableを利用することで、たとえばリアルタイムで更新されるチャット機能を実装することができます。
他の記事頼みの説明
実践
API(Rails)
環境構築
config/environments/development.rb
ActionCableにあらゆるオリジンからの通信を許可するため、以下のコメントアウトを解除します
config.action_cable.disable_request_forgery_protection = true
config/environments/production.rb
development同様disable_request_forgery_protectionをtrueにします。
それに加えて、通信を許可するオリジンをallowed_request_originsに配列型式で追加します
config.action_cable.allowed_request_origins = ['https://your-staging-domain', 'https://your-production-domain']
config.action_cable.disable_request_forgery_protection = true
/cableにサーバーをマウントするよう設定します。このパスがWebSocketのコネクションを開始するためのルートパスになります
mount ActionCable.server => '/cable'
コネクションの確立
app/channels/application_cable/connection.rb
本来ならここでコネクションを制限したりする? uniqな値を振り分ける。おそらく解錠の時に必要だから
module ApplicationCable
class Connection < ActionCable::Connection::Base
identified_by :chat_token
def connect
self.chat_token = Time.now.to_i
end
end
end
チャネルの作成
$ docker-compose run web rails g channel Chat
app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
# チャネルがフロント側で作成された時に走るメソッド
def subscribed
stream_from "chat_channel"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
# フロントから送られてきたデータを使ってテーブルに保存している。
# その後ブロードキャストをして(不特定多数のsubscriber)に対して保存したデータを送信している
def speak(data)
createdMessage = Message.create!({ body: data["message"] })
ActionCable.server.broadcast("chat_channel", { message: createdMessage })
end
end
フロント(React)
src/App.tsx
import React, { ChangeEvent, useEffect, useState } from 'react';
import './App.css';
import axios from 'axios';
import ActionCable from 'actioncable';
interface Message {
id: number;
body: string;
created_at: string;
updated_at: string;
}
function App() {
const [messages, setMessages] = useState<Message[]>([]);
const [inputText, setInputText] = useState('');
const cable = ActionCable.createConsumer('ws://localhost:3000/cable');
const channel = cable.subscriptions.create('ChatChannel', {
// chanelとコネクトした時に走るメソッド
connected: () => {
console.log('コネクト成功');
},
// broadcastで不特定多数に送られたものを受け取る
received: (data) => {
// idが同じものだったら即リターンする
const existingMessages = messages.filter((message) => {
message.id === data.message.id;
});
if (existingMessages.length !== 0) return;
console.log(data);
setMessages([...messages, data.message]);
setInputText('');
},
});
useEffect(() => {
fetchMessages();
}, []);
const fetchMessages = async () => {
await axios
.get('http://localhost:3000/test/tests/index')
.then((res) => {
setMessages(res.data.messages);
})
.catch((e) => console.log(e));
};
const changeInputText = (e: any) => {
setInputText(e.target.value);
};
const clickSendMessage = async () => {
// ここでchanelのメソッドを呼び出している。これによってspeakメソッドが発火する
channel.perform('speak', {
message: inputText,
});
};
return (
<div className="App">
<h1>ChatRoom</h1>
<ul>{messages !== [] ? messages.map((message) => <li key={message.id}>{message.body}</li>) : null}</ul>
<label htmlFor="">Say Something</label>
<input type="text" value={inputText} onChange={changeInputText} />
<button onClick={clickSendMessage}>送信</button>
</div>
);
}
export default App;
完成
最後に
websocketを使っての双方向の通信ができるようになった。
しかし、聞きなれない単語が多くどうしても理解が浅い部分は多い。安全にデータを取り扱ったり、気持ちの良いコードとはいえないのでもっと学習してより深く学んでいきたい
参考
この記事を作るにあたって以下の記事を参考にしました!
https://lamila.hatenablog.com/entry/2021/10/11/114521
https://railsguides.jp/action_cable_overview.html