2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

BunAdvent Calendar 2023

Day 24

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

Last updated at Posted at 2023-12-16

※修正:2023/12/20 元記事では、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チャット実装の基本てことで許してください<問題あればあとで追記していきます)

image.png

今回の成果物 (たぶん期間限定公開) 

先に今回の成果物はこんな感じです。昨日作ったチャットを pm2に登録 しておいたので置いときます。まぁ、そのうち止めると思いますけど参考までに。ブラウザで複数開くと試せます。
http://74.226.208.203:3000/

ElysiaとBunのドキュメント

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

Elysia > WebSocket

Bun > WebSocket

今回の環境

クラウド: 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 をインストールします。

Bun をインストールします
$ curl -fsSL https://bun.sh/install | bash

Elysia プロジェクトを作る

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

Elysia プロジェクトを作ります
$ bun create elysia mychat

生成されたディレクトリ

この結果、次のようなディレクトリが生成されます。

生成されたディレクトリ
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 はこうなってます。

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 でも良いですが、詳しくは下記の記事をご参照ください

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

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

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

ブラウザで開いてみる

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

image.png

src/index.ts の中身

src/index.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 サーバーも立ててみましょう。

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

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 ヘッダーや、 も追加してくれます。

@elysiajs/html をインストールします
$ bun add @elysiajs/html

今回は、WebSocket Chat の動作をみるだけなので、ルーティングやスタイル用ファイルなどはガン無視して この src/index.ts ファイル1個へ全部書いてしまおうと思います(^^; ※jsxなどを使った今時の構成は、翌日に書いた こっちの記事 の方が参考になると思います

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

クライアントの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

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

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

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

でもまぁ、動けば正義。

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

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?