※修正: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チャット実装の基本てことで許してください<問題あればあとで追記していきます)
今回の成果物 (たぶん期間限定公開)
先に今回の成果物はこんな感じです。昨日作ったチャットを 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 をインストールします。
$ curl -fsSL https://bun.sh/install | bash
Elysia プロジェクトを作る
次に、Elysia プロジェクトを作ります。とりあえず、WebSocket に入る前に、Elysia で通常の HTTPサーバーを立ち上げてみましょう。
$ 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 はこうなってます。
$ 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 などへアクセスするとこう表示されます。
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 サーバーも立ててみましょう。
そのまま追記するだけで立てられます。
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 ヘッダーや、 も追加してくれます。
$ bun add @elysiajs/html
今回は、WebSocket Chat の動作をみるだけなので、ルーティングやスタイル用ファイルなどはガン無視して この src/index.ts ファイル1個へ全部書いてしまおうと思います(^^; ※jsxなどを使った今時の構成は、翌日に書いた こっちの記事 の方が参考になると思います
@elysiajs/html は、.use(html()) の部分で適用しています。
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 );
})
}
できあがり
DBも無くCSSも当たってないし、何もチューニングもしてないのでざっくりですがまぁ、何とかできました。
ただ、まだ Bun もElysia も勉強始めたばかりなので、もっと良い書き方はあるとは思います。broadcast 機能が見つけられずに配列にクライアントを入れて 回しながらsendするとか、WebSoket が出始めの頃の古いテクニックまで使ってしまいました(^^;
uWebSocketsにはありそうなのだけどなぁ。。わかったら追記します。
でもまぁ、動けば正義。
最近 Qiita に書いた Bun 関連の記事10選