【Elysia/Bun】ElysiaJSでWebSocketチャットを作ってみる

<b>※修正：2023/12/20</b>  元記事では、HTTPサーバーを .listen(3000) に立てて、WebSocketサーバーを .listen(9000)　に立ててましたが、.listen(3000) 1本だけに修正しました。WebSocketサーバーは .ws('/ws', ... パスにアクセスすればつながります。

new Elysia()インスタンスは1個で済みます。

----

先日、「Expressより18倍速いというElysiaJS、Bun圧倒的?」と「BunでWebSocketのデータ配信サーバーを建ててccchartにリアルタイム表示する」いう記事を書きましたが、Elysiaのベースである Bun は、WebSocket も内部的に uWebSocket を使用しており超速いのです。

そこで今回は、Bun の WebSocket ではなく、Elysia から WebSocket を使って簡単な chat を作ってみようと思います。

(まぁ、Bun も Elysia も勉強中なのでいろいろ駄目なところがあると思いますが、とりまWebSocketチャット実装の基本てことで許してください＜問題あればあとで追記していきます)


https://qiita.com/toshirot/items/579d6f7cf7dfa787f291

https://qiita.com/toshirot/items/3af83303f95ac7baa546

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7103/1a24fc42-020e-d1a2-1746-1292ce632a84.png)


# 今回の成果物 (たぶん期間限定公開)　
先に今回の成果物はこんな感じです。昨日作ったチャットを[ pm2に登録 ](https://qiita.com/toshirot/items/7a0555b46b3a64a4f643)しておいたので置いときます。まぁ、そのうち止めると思いますけど参考までに。ブラウザで複数開くと試せます。
http://74.226.208.203:3000/

# ElysiaとBunのドキュメント


Elysia と Bun の WebSocket ドキュメントは下記にあります。

#### Elysia > WebSocket

https://elysiajs.com/patterns/websocket.html

#### Bun > WebSocket

https://bun.sh/docs/api/websockets

# 今回の環境
クラウド： Azure VM (これは何でも良い)
OS：　Ubuntu 20.04.6 LTS (GNU/Linux 5.15.0-1050-azure x86_64)
Bun：　v1.0.18
Elysia： v0.7.30

# Bunのインストール

まだ Bun が入っていない場合は、以下のコマンドを使用して Bun をインストールします。
```sh: Bun をインストールします
$ curl -fsSL https://bun.sh/install | bash
```

# Elysia プロジェクトを作る

次に、Elysia プロジェクトを作ります。とりあえず、WebSocket に入る前に、Elysia で通常の HTTPサーバーを立ち上げてみましょう。

```sh: Elysia プロジェクトを作ります
$ bun create elysia mychat
```
# 生成されたディレクトリ

この結果、次のようなディレクトリが生成されます。
```sh: 生成されたディレクトリ
mychat/
├─ README.md
├─ bun.lockb
├─ node_modules/
│   ├─ @sinclair
│   ├─ bun-types
│   ├─ cookie
│   ├─ elysia
│   ├─ eventemitter3
│   ├─ fast-decode-uri-component
│   ├─ fast-querystring
│   ├─ memoirist
│   └─ openapi-types
├─ package.json
├─ src/
│   └─ index.ts
└─ tsconfig.json
```
# package.json
package.json　はこうなってます。

```sh: package.json
$ cat mychat/package.json
{
  "name": "mychat",
  "version": "1.0.50",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "bun run --watch src/index.ts"
  },
  "dependencies": {
    "elysia": "latest"
  },
  "devDependencies": {
    "bun-types": "latest"
  },
  "module": "src/index.js"
```
"scripts" の 「"dev": "bun run --watch src/index.ts"」は、--watch パラメータが付いてますので、編集内容が即時反映される機能ホットリロードが機能します。サバ―プロセスを再起動しない --hot でも良いですが、詳しくは下記の記事をご参照ください

https://qiita.com/toshirot/items/4c8e14c9525d9e89b55f

# 開発サーバーを起動します

次の方法で開発サーバーを起動すれば、package.jsonに書いてある通り「src/index.ts」が走ることになります。

```sh: 開発サーバーを起動します
$ cd mychat
$ bun dev
```


# ブラウザで開いてみる

ブラウザで、localhost:3000 などへアクセスするとこう表示されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7103/39afd81d-e968-13c5-c1b4-543d1536824f.png)

# src/index.ts の中身
src/index.ts の中身はこうなっています。

```ts: src/index.ts
import { Elysia } from "elysia";
const app = new Elysia().get("/", () => "Hello Elysia").listen(3000);
console.log(
  `Elysia is running at ${app.server?.hostname}:${app.server?.port}`
);
　
```

port 3000 で "/"へアクセスしたら"Hello Elysia"と返すというだけの簡単なHTTPサーバーです。

#  WebSocket サーバーも立ててみる

では、port 9000 で "/ws"へアクセスし、WebSocket メッセージが届いたら、そのメッセージをオウム返しに返すだけの簡単な WebSocket サーバーも立ててみましょう。

そのまま追記するだけで立てられます。

```ts: WebSocketサーバーを port 9000 に立てる
import { Elysia } from "elysia";

// for HTTP サーバー
const app = new Elysia().get("/", () => "Hello Elysia").listen(3000);

// for WebSocket サーバー
const app_ws = new Elysia()
    .ws('/ws', {
        // メッセージが届いたら返すだけの簡単なエコーサーバー
        message(ws, message) {
            ws.send(message)
        }
    })
    .listen(9000)
```

# クライアント側も作る
サーバー側が完成したので、ブラウザで見る方のクライアント側も作ってみます。

その前に、JSX と HTML用のプラグイン @elysiajs/html を入れておきます。このプラグインは自動的に Content-Type: text/html を追加します。 charset=utf8 ヘッダーや、<!doctype html> も追加してくれます。
```sh: @elysiajs/html をインストールします
$ bun add @elysiajs/html
```
今回は、WebSocket Chat の動作をみるだけなので、ルーティングやスタイル用ファイルなどはガン無視して この src/index.ts ファイル1個へ全部書いてしまおうと思います(^^;　※jsxなどを使った今時の構成は、翌日に書いた [こっちの記事](https://qiita.com/toshirot/items/01c7f779d87985b1f9be) の方が参考になると思います

@elysiajs/html は、.use(html())　の部分で適用しています。

```ts: クライアントのHTMLを"Hello Elysia"の代わりに書く
import { Elysia } from "elysia";
import { html } from '@elysiajs/html'

const app = new Elysia()
    .use(html())
    // HTTPサーバー GET
    .get("/", () => `
        <html lang='ja'>
            <head>
                <title>myChat</title>
            </head>
            <body>
                <h1>myChat</h1>
                名前: <input
                    id="input_name"
                    type="text"
                    placeholder="名前を入れてください"><br>
                メッセージ: <input
                    id="input_msg"
                    type="text"
                    onchange="/*socket.ws.send('Hello, bun')*/"
                    placeholder="メッセージを入れてください"><br>
                <button id="btn_send">送信</button>
                <ul id=msgs></ul>

                <script>
                    const url = 'ws://74.226.208.203:9000/ws'
                    // 接続
                    const socket = new WebSocket(url);
                    // 接続時イベント
                    socket.onopen = function (event) {
                        socket.send('サーバーへ接続しました ' + socket.url)
                    };
　　　　　　　　　　 // 着信時イベント
                    socket.onmessage = function (event) {
                        msgs.innerHTML+='<li>'+event.data+' ('+new Date()+')</li>'
                    };
                    // 送信ボタンクリック時イベント
                    btn_send.addEventListener('click', function () {
                        // 名前とメッセージがあれば送信する
                        if(!!input_name.value && !!input_msg.value)
                            socket.send(
                                '<b>'+ input_name.value +': </b>'+
                                ' '+ input_msg.value
                            );
                    });
                </script>
            </body>
        </html>
    `)

    // WebSocketサーバー 
    .ws('/ws', {
        open(ws){
            // クライアントを配列に保存しておく
            clients.push(ws)
        },
        // メッセージが届いたら返すだけの簡単なエコーサーバー
        message(ws, message) {
            // safeという属性もあるらしい(?)のだけど念のためにscript除去しとく
            message=message
              .replace(/<script\b[^>]*>([\s\S]*?)<\/script>/gi, '');
            // 届いたメッセージをブロードキャストする
            broadCast(ws, message)
            console.log('msg',message)
        }
    })
    .listen(3000);

//===============================================================
// for WebSocket サーバー
//

let clients=[]

// 全クライアントへブロードキャストする
function broadCast(ws, msg){
      clients.forEach(function (socket, i) {
          socket.send( msg );
      })
}

```

# できあがり
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/7103/855d9c72-5bf4-4862-4efd-4556c7834855.png)

DBも無くCSSも当たってないし、何もチューニングもしてないのでざっくりですがまぁ、何とかできました。

ただ、まだ Bun もElysia も勉強始めたばかりなので、もっと良い書き方はあるとは思います。broadcast 機能が見つけられずに配列にクライアントを入れて 回しながらsendするとか、WebSoket　が出始めの頃の古いテクニックまで使ってしまいました(^^;

uWebSocketsにはありそうなのだけどなぁ。。わかったら追記します。

でもまぁ、動けば正義。

# 最近 Qiita に書いた Bun 関連の記事10選

https://qiita.com/advent-calendar/2023/bunjs

https://qiita.com/toshirot/items/94616478d3e73e7cc5b9

https://qiita.com/toshirot/items/f6bd561bc2522f1d26e0

https://qiita.com/toshirot/items/a5dc53ef191d20bbeb42

https://qiita.com/toshirot/items/74476af27e67d8534c5a

https://qiita.com/toshirot/items/94961b6dde72be80ab0c

https://qiita.com/toshirot/items/7a0555b46b3a64a4f643

https://qiita.com/toshirot/items/c4d090bc38f9b85d8cdd

https://qiita.com/toshirot/items/4e8e571dd50a45c651f0

https://qiita.com/toshirot/items/e4eb635596ae4f63bdfd

https://qiita.com/toshirot/items/a81f2073e373b0351493