2
1

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.

メッセージ系のSNSを作ろうとしたのに掲示板になっていない?

Last updated at Posted at 2022-08-05

SNSと掲示板の違い

エンジニアなら誰しも一回はLINEのようなSNSアプリを作りたいと思ったことはあるはずです

SNSもどきの例

u8amv-lvx1w.gif

問題点

  • リアルタイムで更新されていないためコミュニケーションが図れない!!!

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;

完成

gif.gif

最後に

websocketを使っての双方向の通信ができるようになった。
しかし、聞きなれない単語が多くどうしても理解が浅い部分は多い。安全にデータを取り扱ったり、気持ちの良いコードとはいえないのでもっと学習してより深く学んでいきたい

参考

この記事を作るにあたって以下の記事を参考にしました!

https://lamila.hatenablog.com/entry/2021/10/11/114521
https://railsguides.jp/action_cable_overview.html

2
1
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
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?