🔥
HonoのWebsocketを使用して簡易チャット機能を作ってみた
はじめに
お正月休みで時間があるのでHonoの学習がてらWebsocketを使って簡易なチャットを作って遊んでみました。(とても簡易な実装です)
準備するもの
- bun
- nodejs
基本的にクライアント側はbun create vite client --template react-ts
で作成し、
server側はbun create hono@latest server
で作成しています(Typescriptなどの各種設定は各自で〜)
ディレクトリ構成
client
./src
├── App.css
├── App.tsx
├── assets
│ └── react.svg
├── index.css
├── main.tsx
└── vite-env.d.ts
server
./src
└── index.ts
実装まわり
Client
// App.tsx
import { useEffect, useRef, useState } from "react";
import "./App.css";
interface Message {
id: string;
text: string;
}
function App() {
const [messages, setMessages] = useState<Message[]>([]);
const [inputText, changeInputText] = useState("");
const [isEditMode, changeEditMode] = useState(false);
const edittingMessageId = useRef<string>();
useEffect(() => {
const websocket = new WebSocket("ws://localhost:3000/ws");
const getMessages = async () => {
const res = await fetch("http://localhost:3000/messages");
const jsonData = await res.json();
setMessages(jsonData.messages);
};
getMessages();
websocket.onmessage = (event) => {
const parsedData = JSON.parse(event.data);
if (parsedData.eventType === "CREATE") {
setMessages((prev) => [parsedData.message, ...prev]);
return;
}
if (parsedData.eventType === "UPDATE") {
setMessages((prev) =>
prev.map((msg) => {
if (parsedData.message.id === msg.id) {
return parsedData.message;
}
return msg;
})
);
return;
}
if (parsedData.eventType === "DELETE") {
setMessages((prev) =>
prev.filter((msg) => parsedData.messageId !== msg.id)
);
return;
}
};
websocket.onclose = (event) => {
console.log("WebSocket client closed", event);
};
return () => {
websocket.close();
};
}, []);
const submit = async () => {
if (inputText !== "") {
await fetch("http://localhost:3000/messages", {
method: "POST",
body: JSON.stringify({
message: inputText,
}),
});
}
changeInputText("");
};
const updateMessage = async () => {
if (inputText !== "" && edittingMessageId) {
await fetch(
`http://localhost:3000/messages/${edittingMessageId.current}`,
{
method: "PATCH",
body: JSON.stringify({
message: inputText,
}),
}
);
}
changeEditMode(false);
changeInputText("");
};
const deleteMessage = async (id: string) => {
await fetch(`http://localhost:3000/messages/${id}`, {
method: "DELETE",
});
};
return (
<div className="App">
<div>
<h1>Messages</h1>
<ul>
{messages.length === 0 ? (
<div>No messages</div>
) : (
messages.map((message) => (
<li key={message.id}>
{message.text}
<button
onClick={(e) => {
e.preventDefault();
changeEditMode(true);
edittingMessageId.current = message.id;
changeInputText(message.text);
}}
>
edit
</button>
<button
onClick={(e) => {
e.preventDefault();
deleteMessage(message.id);
}}
>
delete
</button>
</li>
))
)}
</ul>
<div>
<input
value={inputText}
onChange={(e) => changeInputText(e.target.value)}
/>
<button onClick={isEditMode ? updateMessage : submit}>
{isEditMode ? "update" : "send"}
</button>
</div>
</div>
</div>
);
}
export default App;
Server
// index.ts
import { Hono } from "hono";
import { createBunWebSocket } from "hono/bun";
import type { ServerWebSocket } from "bun";
import { cors } from "hono/cors";
const { upgradeWebSocket, websocket } = createBunWebSocket<ServerWebSocket>();
const chatRoom = "chat-room";
interface Message {
id: string;
text: string;
}
type EventType = "CREATE" | "UPDATE" | "DELETE";
interface ResponseData {
eventType: EventType;
message?: Message;
messageId?: string;
}
let messages: Message[] = [];
const app = new Hono();
app.use(
"*",
cors({
origin: ["http://localhost:5173"],
})
);
app.get("/", (c) => {
return c.text("Hello!");
});
app.get("/messages", (c) => {
return c.json({ messages });
});
app.post("/messages", async (c) => {
const uuid = crypto.randomUUID();
const { message } = await c.req.json();
const newMessage = {
id: uuid,
text: message,
};
messages.unshift(newMessage);
const data: ResponseData = {
eventType: "CREATE",
message: newMessage,
};
server.publish(chatRoom, JSON.stringify({ ...data }));
return c.text("OK");
});
app.patch("/messages/:id", async (c) => {
const id = c.req.param("id");
const { message } = await c.req.json();
const updateMessage = {
id,
text: message,
};
messages = messages.map((msg) => {
if (msg.id === id) {
return updateMessage;
}
return msg;
});
const data: ResponseData = {
eventType: "UPDATE",
message: updateMessage,
};
server.publish(chatRoom, JSON.stringify({ ...data }));
return c.text("OK");
});
app.delete("/messages/:id", async (c) => {
const id = c.req.param("id");
messages = messages.filter((msg) => msg.id !== id);
const data: ResponseData = {
eventType: "DELETE",
messageId: id,
};
server.publish(chatRoom, JSON.stringify({ ...data }));
return c.text("OK");
});
app.get(
"/ws",
upgradeWebSocket((c) => {
return {
onOpen: (_event, ws) => {
ws.raw?.subscribe(chatRoom);
},
onClose: (_event, ws) => {
ws.raw?.unsubscribe(chatRoom);
},
};
})
);
export const server = Bun.serve({
fetch: app.fetch,
websocket,
});
export default app;
解説
Pub/Sub機能を使って同一のチャットルームにいるユーザにチャットが届くようにしています。
Websocketでコネクションを張った(onOpen)"chat-room"にSubscribeし、POST,UPDATE
,DELETEリクエストが送信された際にmessages
変数を操作しclientにpublishしています。
Discussion