こんにちは。
大学生が使えるGemini Pro 15ヶ月無料を使用しているおかげで、現在の私はプロンプトエンジニアリングやりたい放題な訳です。
これを活用しない手はない!と思いまして、何かこのブログにも実装してみたいと思いました。
まあすでにこのブログはGeminiの息がかかった部分が多々あるわけですが。
今回は、かねてより実装したかったBBS(掲示板)を追加してみます。
完成したものはこちら
この虹色、ダサくて気に入っています。
↑こんな感じになりました
なんかSQLインジェクションをしようとしている輩がいますが見なかったことにします。
仕組みとコードについてザックリ解説していきます。
まず、BBSの投稿データを保存・取得するための型定義をしておきます。
interface BbsPost {
name: string;
message: string;
createdAt: number;
}
最初に、BBSのページにジャンプしたときに呼び出される関数です。
async function getBbsPage(): Promise<Response> {
const postsResponse = await getBbsPostsHtml();
const postsHtml = await postsResponse.text();
const bbsHtml = await loadTemplate('templates/bbs.html', {
posts: postsHtml,
});
const fullHtml = await wrapLayout(bbsHtml, 'BBS');
return new Response(fullHtml, { headers: { 'Content-Type': 'text/html; charset=utf-8' } });
}
HTMLを読み込んだりしていますね。
ここでgetBbsPostsHtml()
を呼んでいます。これは、投稿一覧のHTMLだけを生成して返す関数です。この関数は以下のコードです。
async function getBbsPostsHtml(): Promise<Response> {
const kv = await Deno.openKv();
const entries = kv.list<BbsPost>({ prefix: ["bbs_posts"] });
const posts: BbsPost[] = [];
for await (const entry of entries) {
posts.push(entry.value);
}
kv.close();
posts.sort((a, b) => b.createdAt - a.createdAt);
const postsHtml = posts.map(p => {
const jstDate = new Date(p.createdAt).toLocaleString('ja-JP', { timeZone: 'Asia/Tokyo' });
return `
<div class="bbs-post">
<div class="bbs-post-meta">
<span class="bbs-post-name">${escapeHtml(p.name)}</span>
<span class="bbs-post-date">${jstDate}</span>
</div>
<p class="bbs-post-message">${escapeHtml(p.message).replace(/\n/g, '<br>')}</p>
</div>
`;
}).join('');
return new Response(postsHtml, { headers: { 'Content-Type': 'text/html; charset=utf-8' } });
}
がちゃがちゃと書いていますが、半分くらいHTMLです。落ち着いてください。
このブログは、Deno Deployによってつくられています。データベースもそれに合わせてDeno KVを使用しました。(来訪者カウンタにも使っている)
for周辺の記述ですべてのメッセージを取得して、枠組みを作って繋げてHTMLを返して…………という具合です。
HTML内の${escapeHtml(p.name)}
という部分は、入力された文字列を安全な形式に変換(エスケープ)しています。これをしないと、悪意のあるHTMLタグを埋め込まれてめちゃくちゃにされてしまいます。(XSS脆弱性)
ここまでが、メッセージの表示に関わる処理ですね。
次に、メッセージが投稿されたときの処理です。
async function handleBbsPost(req: Request): Promise<Response> {
const form = await req.formData();
const name = form.get("name")?.toString() || "名無しさん";
const message = form.get("message")?.toString();
if (!message) {
return new Response("メッセージが入力されていません。", { status: 400 });
}
const post: BbsPost = {
name,
message,
createdAt: Date.now(),
};
const kv = await Deno.openKv();
await kv.set(["bbs_posts", Date.now()], post);
kv.close();
return new Response(null, {
status: 303,
headers: { 'Location': '/bbs' },
});
}
BBSのHTMLにはformタグがあって、そのPost時にこの関数が呼び出されます。
フォームから送信された名前とメッセージを取得し、投稿データを作成します。名前が空欄の場合は「名無しさん」になります。Deno.Kv
に["bbs_posts", Date.now()]
というキーでデータを保存し、BBSのページ(同じページ)にリダイレクトさせています。
なので、勘が鋭い人は気づいたかもしれませんが、全く同じ時間に投稿するとバグります(たぶんどっちかのメッセージが消える)
UUIDとかで管理するのも考えましたが、まあ面倒なのでこっちでいいです。そんなこと滅多におきないでしょう
そんなことよりも重大な問題がありますよね?
このままだと、誰かが投稿してもリロードしないと新しい投稿が見れません。
かといって、WebSocketはなんとなくハードルが高そう。というかめんどくさい
つーことで、ブラウザ側から定期的にサーバーへ「新しい投稿ある?」と問い合わせて、新しい投稿があった場合、投稿一覧を更新することにします。(ポーリングといいます。WebSocketの登場前のちょっと古い手法)
まあそこまで強いリアルタイム性を求めているわけではないので、これで十分でしょう。ね? ね?
BBSのJavaScriptに次の処理を追加しました。
document.addEventListener('DOMContentLoaded', () => {
// ... (省略) ...
const postsContainer = document.querySelector('.bbs-posts-container .posts-list');
const fetchPosts = async () => {
try {
const response = await fetch('/bbs/posts');
if (!response.ok) {
throw new Error('投稿の取得に失敗しました。');
}
const newPostsHtml = await response.text();
// 取得したHTMLで投稿一覧をぜーんぶ置き換えちゃう
postsContainer.innerHTML = newPostsHtml;
} catch (error) {
console.error(error);
}
};
setInterval(fetchPosts, 5000);
});
fetchPosts
で5秒に一回投稿を取得しています(/bbs/postsにアクセスすると投稿の一覧のみが返ってくる。)これで、複数人が書き込んでもある程度のリアルタイム性を確保できました。
…………え? 無駄な通信がおおい??
…………………………………………
これで、複数人が書き込んでもある程度のリアルタイム性を確保できました!
いかがでしたか?今回は真面目な解説になってしまいましたね(というかこんなブログのどこに需要があるんだよ)
今回はBBSを実装してみました。DenoとDeno KVを使うことで、比較的少ないコード量でデータベース機能を持つWebアプリケーションが作れることがわかりました。 皆さんもぜひ、このBBSで遊んでみてくださいね。
…え? この記事のどこにGeminiを使ったのかって??
それはね……
Gemini は不正確な情報を表示することがあるため、生成された回答を再確認するようにしてください。