NestJSで簡単なアプリを作ってみる(2)WebSocketでチャット機能を作る

こんばんは! 株式会社スマレジ 、開発部のmasaです。

皆さんはリモートワークでBGMかけていますか?masaはかけています。 かけるBGMはそれこそ気分次第なのですが、朝は無印良品の店内BGMをかけています笑

www.youtube.com

masaは夜になるにつれて集中力が増していくので、朝は頭が回っていないことが多いです。 それもあって登山とか、生活習慣を整えられそうなことをしてたりするのですが・・・(登山は朝早く出て、体を動かすので自然と早寝早起きになる)

意外とBGMひとつで気分も結構変わるのでバカにできないですよね。

WebSocketの練習の鉄板「チャットアプリ」を作る

WebSocketの入門で作った人も多いのではないでしょうか。今回は鉄板のチャットアプリをNestJS+WebSocketで作ります。 昔masaも作りました。

いきなりWebSocket触るわけですが、それ以前の基本については公式ドキュメントやyoutubeなどでざっくり勉強してサンプルを作って動かしています。

docs.nestjs.com

www.youtube.com

で、まず、WebSocketの公式ドキュメントを見てみます。

docs.nestjs.com

想像通りですが、エンドポイントに当たるgatewayを作って、ソケット監視する仕組みのようです。 しかし、公式で書いてあるコードが断片的で、全貌がよーわからんのです。

と思っていたら、一番下にサンプルのgithubを書いてくれていました。

github.com

一応チェックアウトすればすぐ動くみたいですが、このコードをベースに自分なりにいじっていきます。 決して今作っているプロジェクトと別にサンプルをチェックアウトするのがめんどくさいとか、そういう理由ではないのです。

で、サンプルを見ていて「あれ?」と思ったのがここ。

github.com

え、結局Socket.io使うのか。 いや使うにしても、NestJSの仕組みの中でラップしているとか、そういうんじゃないのか。

ちょっと拍子抜けでした。ちゃんとフレームワーク比較するときにちゃんと調べているわけではなかったのでただのmasaの勘違いなのですが。 (でもそれならそれで、公式ドキュメントにSocket.ioを使う想定だよって書いておいてもいいような。Node.jsのWebSocketのライブラリってもしかして他にも結構あるのかな・・・?)

ソースを見た感じ、SubscribeMessageデコレータでどのイベントにどのメソッドが紐づくのかを直感的に指定できるようになっていますね。 確かにこれは便利。それに他のサービスなどのデコレータの使い方と似ているから使いやすい。

作っていく

まずはnestコマンドでgatewayのテンプレを作成。

nest g gateway Events 

で、サンプルをもとにgatewayを実装。

import {
  MessageBody,
  SubscribeMessage,
  WebSocketGateway,
  WebSocketServer,
} from '@nestjs/websockets';
import { Server } from 'socket.io';

@WebSocketGateway({
  cors: {
    origin: '*',
  },
})
export class EventsGateway {
  @WebSocketServer()
  server: Server;
  wsClients=[];

  handleConnection(client: any) {
    this.wsClients.push(client);
  }

  @SubscribeMessage('chat')
  chat(@MessageBody() data: any) {
    console.log(data);
    this.broadcast('chat', data.message);
  }

  @SubscribeMessage('testing')
  emitLoginMessage(@MessageBody() data: any) {
    console.log(data);
    this.broadcast('login', data + 'さんがログインしました。');
  }

  private broadcast(event, message: string) {
    const broadCastMessage = message;
    for (let c of this.wsClients) {
      c.emit(event, broadCastMessage);
    }
  }

}

でクライアントhtmlも用意。

<html>
<head>
</head>

<body>
<div id="test-area">

</div>
<input type="text" id="input" />
<button onclick="sendMessage()">send</button>
<script src="https://cdn.socket.io/4.3.2/socket.io.min.js" integrity="sha384-KAZ4DtjNhLChOB/hxXuKqhMLYvx3b5MlT55xPEiNmREKRzeEm+RVPlTnAn0ajQNs" crossorigin="anonymous"></script>
<script>
    const socket = io('http://localhost:8080');
    const userId = Math.random().toString(32).substring(2);
    socket.on('connect', function() {
        console.log('Connected');
        socket.emit('testing', userId);
    });
    socket.on('login', function(data) {
        let textArea = document.getElementById("test-area");
        textArea.innerHTML += '<div style="color: blue;">' + data + '</div>';
        textArea.innerHTML += "<br />";
    });
    socket.on('chat', function(data) {
        let textArea = document.getElementById("test-area");
        textArea.innerHTML += data;
        textArea.innerHTML += "<br />";
        console.log('event', data);
    });
    socket.on('exception', function(data) {
        console.log('event', data);
    });
    socket.on('disconnect', function() {
        console.log('Disconnected');
    });

    function sendMessage() {
        let message = document.getElementById("input").value;
        console.log(message);
        socket.emit('chat', { message: userId + ": " +message });
    }
</script>
</body>
</html>

今回はブロードキャストの動作確認もするので、webstormのローカル実行機能で、ローカルでブラウザを二つ立ち上げます。

動かしてみるとこんな感じ

ブラウザにアクセスすると、ランダム文字列でuserIdが決定され、loginイベントが発火し、ログイン通知がブロードキャストされます。 その後チャット欄に文字を入れてsendボタンを押せば、そのときブラウザを開いているユーザ全員に通知がいくようになっています。

このブロードキャスト処理の実態は、接続時にwsClientsに接続情報を保存しておき、ループでクライアント全員にemitを送っているだけなので、接続数が増えると遅延なども出てきそうです。