はじめに
この記事はサーバーにログが残らない匿名形式のチャットアプリ作成するための記事です。
匿名と言っても、WebSocketを
この記事はWebSocketの使い方を学ぶためのものであり、フレームワーク等の詳しい使い方には触れません。
使う技術・フレームワーク等
名前 | 目的 |
---|---|
Deno | バックエンド実装 |
Deno Deploy | Denoをデプロイする |
NextJS | サイトで使うフレームワーク |
TailwindCSS | フロントエンドのCSS |
Vercel | サイトのデプロイ |
この記事で書くこと
- DenoでWebSocketを使ったチャットアプリのバックエンドを作る方法
- バックエンドのコードの解説
- WebSocketの簡単な説明
- NextJSで作るシンプルなフロントエンドの書き方
この記事で書かないこと
- 書くこと以外のほぼ全て
バックエンドの作成
バックエンドを先に作ったほうが後々楽なので、先にバックエンドを作ります。
バックエンド用のフォルダを作って直下にmain.tsファイルを作成して以下のようにする。
import { serve } from "https://deno.land/std@0.191.0/http/server.ts"; const useLog = false; const clients = new Map<string, WebSocket>(); //接続してる全員にメッセージを送る const sendMessageToAll = (message: string) => { clients.forEach((client) => { if (client.readyState === client.OPEN) { client.send(message); } }); log(JSON.parse(message)); }; function wsHandler(ws: WebSocket, url: URL) { //ユーザーにUUIDを振る const id = crypto.randomUUID(); clients.set(id, ws); ws.onopen = () => { log("connected"); }; ws.onmessage = (e) => { const payload = JSON.parse(e.data); if (payload.type === "message") { sendMessageToAll(e.data); } }; ws.onclose = () => { clients.delete(id); log("disconnected"); }; } //HttpをWebsocketにUpgradeして //レスポンスを返す関数 function handler(req: Request) { if (req.method != "GET") { const err = new Response("Method Not Allowed", { status: 405, headers: { "Content-Type": "text/plain" }, }); return err; } const url = new URL(req.url); //Upgrade const { response, socket } = Deno.upgradeWebSocket(req); //Websocketの処理 wsHandler(socket, url); return response; } //Denoのエントリーポイント serve(handler, { port: parseInt(Deno.env.get("PORT") ?? "") || 443 }); //開発時はログを表示しないようにするための //ログのラップ関数 function log(content: string) { if (useLog == false) return; console.log(content); }
serve関数がDenoのエントリーポイントで、ここを起点にいろいろな処理をします。
第一引数でhandler関数を呼び出してますが後ほど。
ポート番号は環境変数があればそいつを、なければ443を使うようにしてます。ポート番号443はwssプロトコルでのデフォルトらしいです。参考
handler関数は主にリクエストを捌く部分です。
まず、GET以外のメソッドは弾いて405を返すようにしてます。
そしてプロトコルがwsかwssじゃなければ400を返します。
GETメソッドなら、HTTP通信をWebSocketにUpgradeします。
Upgradeとは、HTTP通信のリクエストとレスポンスが1対1となっている状態から、WebSocket通信という双方向にデータを送受信できる状態にすることです。
UpgradeしたらwsHandlerを呼びます。
wsHandlerはWebSocketクライアントの追加と、各イベントに対応する処理を書きます。 最初にUUIDを生成してユーザーに割り振って、クライアントのリストに追加します。 IDをクライアントのリストに追加するのは、一気にデータを送信するときに使うためです。 また、UUIDは2128通りあるので多分重複しないです。この確率のお陰でチェックする手間が省けます。気になる人は重複がないかチェックする処理書いてください。
ws.onopenはクライアントがWebSocketを開始したときに呼ばれます。今回はログを出すだけで特に処理はしてないです。
ws.onmessageは接続されてるクライアントからデータを受信したときに呼ばれます。今回は受信したデータのtypeがmessageのときにsendMessageToAllを読んで、接続されてるクライアント全員にメッセージを送っています。送ったメッセージをログとして出力してます。ちなみに、接続されてるクライアントには自分も含まれています。
ws.oncloseはクライアントの接続が切れたときに呼ばれます。今回はクライアントのリストから接続されたクライアントのIDを削除して切断された旨のログを出してます。
ここまでできたらOK。
Deno Deployにデプロイします。
やり方は調べてください。先人方のわかりやすい記事がたくさんあります。
デプロイするとき、エントリーポイントをmain.tsにしてください。
フロントエンドの作成
プロジェクトを作成
npx create-next-app chat-app --use-npm --typescript
プロジェクト作成のオプションは以下のとおりです。お好みの場合、()内はおすすめを書いてます。
- ESLint → お好み(TypeScriptのフォーマットをやってくれるので基本はYesがおすすめです)
- Tailwind CSS → Yes
- src directory → お好み(ディレクトリを読み替えるのがめんどくさい場合はNoがおすすめです)
- App Router → お好み(ディレクトリを読み替えるのがめんどくさい場合はNoがおすすめです)
- import alias → No
プロジェクトフォルダに移動
cd chat-app
プロジェクトを立ち上げる。 デフォルトだとローカルホストのポートは3000、http://localhost:3000で開くことができる。
npm run dev
以下の画面が出たら成功です。
ここまでできたら爆速でフロントエンドを組み立てて行きましょう。
import { useEffect, useState } from "react"; let messagesArr: any[] = []; const author = randomString(); //ランダムなユーザーIDを生成するための関数 function randomString(): String { let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; let name = ""; for (var i = 0; i < 8; i++) { name += chars.charAt(Math.floor(Math.random() * chars.length)); } return name; } export default function Home() { const [socket, setSocket] = useState<WebSocket | null>(null); const [messages, setMessages] = useState<any[]>([]); const url = process.env.NEXT_PUBLIC_API_SERVER as string; useEffect(() => { setSocket((socket: WebSocket | null) => { // 既に入ってたら新しくsocketを作成しない // これをしないと2回以上接続するという変なことになってしまう if (socket) return socket; return new WebSocket(url); }); }, [setSocket]); useEffect(() => { if (socket) { // WebSocketからメッセージを受け取った時の処理 socket.onmessage = (res: MessageEvent) => { const payload = JSON.parse(res.data); messagesArr = [...messagesArr, payload]; setMessages(messagesArr); }; } }, [socket]); // メッセージを送信する処理 const sendMessage = (content: string) => { const packet = { type: "message", data: { author: author, text: content, ts: Date.now(), }, }; socket?.send(JSON.stringify(packet)); }; function sendMessageWrapper() { const field = document.querySelector("#message") as HTMLInputElement; if (field == undefined) return; const message = field?.value as string; if (message == "") return; sendMessage(message); field.value = ""; } return ( <main className="flex min-h-screen flex-col items-center justify-between bg-gray-900"> <div className="overflow-y-scroll overscroll-contain"> {messages && messages .filter((x) => x.type == "message") .map((x) => { return ( <div className="m-2 h-full w-96 bg-white px-4 py-2" key={x.data.ts} > <div> <p className="text-md justify-items-start text-black"> ID : {x.data.author} </p> <p className="text-2xl text-black">{x.data.text}</p> </div> </div> ); })} </div> <div className="sticky bottom-0 flex justify-between"> <input type="text" id="message" className="w-80 mx-6 text-black md:w-96" placeholder="Enter your message" ></input> <button className="w-16 p-2 bg-white text-black" onClick={sendMessageWrapper} > Send </button> </div> </main> ); }
Reactにまつわるオマジナイは調べてください。useEffectとか。先人方の(以下略)。
クライアント側では、WebSocketをURL Sendボタンを押したら、sendMessageWrapperがsendMessageを呼んで、その中でWebSocketで送るオブジェクトをJSONにして送信します。あとはバックエンドがよしなに通信してクライアントにメッセージが送られます。
ここまでできたらOK。
Vercelにデプロイします。
Vercelの環境変数、NEXT_PUBLIC_API_SERVERにDeno DeployにデプロイしたURLを貼り付けるのを忘れないでください。 URLの初めにws://かwss://にしないとプロトコルが不正で通信できないし、WebSocketのインスタンス生成するところで死にます。 例 : wss://sample.deno.dev
以上で完成です。お疲れ様でした!
なにか質問・ツッコミがあればコチラまでどうぞ。DMでもリプライでも分かるところまで返します。
参考
【受験生が作る】リアルタイム性を重視した新感覚チャットアプリ「のーろぐちゃっと」 | Next.js + WebSocket