今回やること
今回は、Honoというフレームワークを使って、前回作ったTODOアプリを簡単に書き直してみましょう。
Honoを使ってみる
まずは、前回の作業ディレクトリsimple-todo-app
に移動します。
Honoを使うためにpackage.json
を編集します。
{
"name": "simple-todo-app",
"type": "module",
"dependencies": {
"@hono/node-server": "^1.11.1",
"hono": "^4.3.6"
},
"scripts": {
"start": "node server.js"
}
}
次に、npmでpackage.json
を読み込み、必要なパッケージをインストールします。
npm install
package-lock.json
とnode_modules
ディレクトリが生成されたら、server.js
を以下のように編集します。
import { serve } from "@hono/node-server";
import { Hono } from "hono";
const app = new Hono();
const todoList = [
{ title: "JavaScriptを勉強する", completed: false },
{ title: "TODOアプリを自作する", completed: false },
{ title: "漫画を読み切る", completed: true },
{ title: "ゲームをクリアする", completed: false },
];
app.get("/", (c) => c.json(todoList, 200));
app.post("/", async (c) => {
const param = await c.req.json();
if (!param.title) {
throw new HttpException(400, { message: "Title must be provided" });
}
const newTodo = {
completed: !!param.completed,
title: param.title,
};
todoList.push(newTodo);
return c.json({ message: "Successfully created" }, 200);
});
app.onError((err, c) => {
if (err instanceof HTTPException) {
return err.getResponse()
}
});
serve({
fetch: app.fetch,
port: 8000,
});
サーバーを起動してみよう
以下のコマンドでサーバーを起動します。
npm run start &
以下のコマンドを実行後にブラウザをリロードすると、TODOが追加されていることが確認できます。
curl -X POST -H "Content-Type: application/json" -d '{"title": "次回のWeb研に出席する"}' http://localhost:8000
実際に動作することが確認出来たら、サーバーを切断してコミットしましょう。
git add .
git commit -m "Honoに移行する"
TODOの編集・削除機能を追加しよう
server.js
を以下のように編集して、TODOの編集・削除機能を追加します。
import { serve } from "@hono/node-server";
import { Hono } from "hono";
const app = new Hono();
const todoList = [
{ id: "1", title: "JavaScriptを勉強する", completed: false },
{ id: "2", title: "TODOアプリを自作する", completed: false },
{ id: "3", title: "漫画を読み切る", completed: true },
{ id: "4", title: "ゲームをクリアする", completed: false },
];
let currentId = 1;
app.get("/", (c) => c.json(todoList, 200));
app.post("/", async (c) => {
const param = await c.req.json();
if (!param.title) {
throw new Error("Title must be provided");
}
const newTodo = {
id: String(currentId++),
completed: !!param.completed,
title: param.title,
};
todoList.push(newTodo);
return c.json({ message: "Successfully created" }, 200);
});
app.put("/:id", async (c) => {
const param = await c.req.json();
const id = c.req.param("id");
if (!param.title && param.completed === undefined) {
throw new HTTPException(400, { message: "Either title or completed must be provided" });
}
const todo = todoList.find((todo) => todo.id === id);
if (!todo) {
throw new HTTPException(400, { message: "Failed to update task title" });
}
if (param.title) {
todo.title = param.title;
}
if (param.completed !== undefined) {
todo.completed = param.completed;
}
return c.json({ message: "Task updated" }, 200);
});
app.delete("/:id", async (c) => {
const id = c.req.param("id");
const todoIndex = todoList.findIndex((todo) => todo.id === id);
if (todoIndex === -1) {
throw new HttpException(400, { message: "Failed to delete task" });
}
todoList.splice(todoIndex, 1);
return c.json({ message: "Task deleted" }, 200);
});
app.onError((err, c) => {
if (err instanceof HTTPException) {
return err.getResponse()
}
});
serve({
fetch: app.fetch,
port: 8000,
});
コマンドでTODO項目を操作してみよう
まず、サーバーを起動します。
npm run start &
以下のコマンドを実行してみましょう。
- TODOの追加コマンド
以下のコマンドで、新しいTODOを追加します。
curl -X POST -H "Content-Type: application/json" -d '{"title": "次回のWeb研に出席する"}' http://localhost:8000
- TODOの編集コマンド
以下のコマンドで、「TODOアプリを自作する」のタスクが完了済みになります。
curl -X PUT -H "Content-Type: application/json" -d '{"completed": true}' http://localhost:8000/2
以下のコマンドで、「JavaScriptを勉強する」のタイトルが「Node.jsを勉強する」に変更されます。
curl -X PUT -H "Content-Type: application/json" -d '{"title": "Node.jsを勉強する"}' http://localhost:8000/1
- TODOの削除コマンド
以下のコマンドで、「漫画を読み切る」のタスクが削除されます。
curl -X DELETE http://localhost:8000/3
実際に動作することが確認出来たら、サーバーを切断してコミットしましょう。
git add .
git commit -m "TODOの編集・削除機能を追加"
ちょっとした解説
server.js
の中で、app.post
、app.put
、app.delete
を使って、それぞれPOST、PUT、DELETEリクエストを処理しています。
app.post
の部分のみ解説します。
app.post
は、新しいTODOアイテムを追加するためのリクエストを処理します。
param
には、リクエストのボディをJSON形式で受け取ります。
const param = await c.req.json();
具体的には、こんな感じです。
{
"title": "次回のWeb研に出席する"
}
新しいTODOアイテムのid
、completed
、title
を受け取り、newTodo
として定義します。
const newTodo = {
id: String(currentId++),
completed: !!param.completed,
title: param.title,
};
todoList
にnewTodo
を追加します。
todoList.push(newTodo);
フロントとサーバーを連携してみよう
新しい作業ディレクトリhono-todo-app
を作成し、その中にserver
ディレクトリとclient
ディレクトリを作成します。
下のようにディレクトリ構造を作成します。
hono-todo-app
├── client
└── server
Gitリポジトリの初期化を行います。
git init
フロント側の実装
client
ディレクトリに移動し、index.html
とscript.js
を作成します。
hono-todo-app
├── client
│ ├── index.html
│ └── script.js
└── server
index.html
とscript.js
を以下のように編集します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0"
/>
<title>TODOアプリ</title>
</head>
<body>
<h1>連携テスト</h1>
<button id="fetch">データ取得</button>
<div id="output"></div>
<script src="./script.js"></script>
</body>
</html>
const fetchButton = document.getElementById("fetch");
const output = document.getElementById("output");
// ボタンがクリックされたときの処理
fetchButton.addEventListener("click", async () => {
// サーバーにリクエストを送信する
const response = await fetch("http://localhost:8000");
// レスポンスの内容を取得する
console.log(response);
// レスポンスの内容をテキストとして取得する
const data = await response.text();
// レスポンスの内容を出力する
output.innerHTML = data;
});
サーバー側の実装
server
ディレクトリに移動し、package.json
とserver.js
を作成します。
hono-todo-app
├── client
│ ├── index.html
│ └── script.js
└── server
├── package.json
└── server.js
package.json
とserver.js
を以下のように編集します。
{
"name": "hono-todo-app",
"type": "module",
"dependencies": {
"@hono/node-server": "^1.11.1",
"hono": "^4.3.6"
},
"scripts": {
"start": "node server.js"
}
}
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { cors } from "hono/cors";
const app = new Hono();
// CORSの設定
app.use(cors({ origin: "*" }));
// ルートパスにアクセスしたときの処理
app.get("/", (c) => {
return c.text("Hello from Node.js server!");
});
serve({
fetch: app.fetch,
port: 8000,
});
サーバーを起動してみよう
まず、必要なパッケージをインストールします。
npm install
次に、サーバーを起動します。
npm run start
index.html
をブラウザで開いて、ボタンをクリックしてみましょう。
「Hello from Node.js server!」と表示されました。
実際に動作することが確認出来たら、サーバーを切断してhono-todo-app
ディレクトリでコミットしましょう。
git add .
git commit -m "フロントとサーバーを連携"
ちょっとした解説
index.html
を開いてボタンをクリックすると、テキストが表示されますがこれはどこから来たのでしょうか?
script.js
の中で、fetch
関数を使ってサーバーhttp://localhost:8000
にリクエストを送信しています。
const response = await fetch("http://localhost:8000");
fetch
関数は、指定したURLにリクエストを送信し、レスポンスを返します。
response
には、レスポンスの内容が入っています。
const data = await response.text();
response.text()
は、サーバーにアクセスしたときに返ってきたデータをテキストとして取得します。
つまり、クライアント(ブラウザ)がサーバーにリクエストを送信し、それに対するレスポンスを受け取ってブラウザに表示しているということです。
TODOを表示してみよう
次にTODOを一覧で表示し、各TODOにチェックボックスと削除ボタンを追加してみましょう。
index.html
とscript.js
を以下のように編集します。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>TODOアプリ</title>
</head>
<body>
<h1>TODOアプリ</h1>
<div id="todo-list"></div>
<input type="text" id="todo-title" placeholder="タイトル" required />
<button type="submit" id="add-button">追加</button>
<script src="./script.js"></script>
</body>
</html>
// サーバーからTODOリストを取得して表示する
const fetchAndDisplayTodoList = async () => {
// サーバーからTODOリストを取得
const response = await fetch("http://localhost:8000/api/todo");
// レスポンスの内容をJSON形式で取得
const todoList = await response.json();
const todoListElement = document.getElementById("todo-list");
todoListElement.innerHTML = "";
// todoList配列の各要素に対して処理を行う
todoList.forEach((todo) => {
// チェックボックスを生成
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = todo.completed;
// チェックボックスの状態が変更されたときに、updateTodoStatus関数を呼び出す
checkbox.addEventListener("change", function () {
updateTodoStatus(todo.id, this.checked);
});
// テキストボックスを生成
// <input type="text" value="..." />
const inputElement = document.createElement("input");
inputElement.type = "text";
inputElement.value = todo.title;
// テキストボックスの値が変更されたときに、updateTodoTitle関数を呼び出す
inputElement.addEventListener("change", function () {
updateTodoTitle(todo.id, this.value);
});
// 削除ボタンを生成
// <button>削除</button>
const deleteButtonElement = document.createElement("button");
deleteButtonElement.textContent = "削除";
// 削除ボタンがクリックされたときに、deleteTodo関数を呼び出す
deleteButtonElement.addEventListener("click", function () {
deleteTodo(todo.id);
});
// <div>
// <input type="checkbox" />
// <input type="text" value="..." />
// <button>削除</button>
// </div>
// を生成
const todoElement = document.createElement("div");
todoElement.appendChild(checkbox);
todoElement.appendChild(inputElement);
todoElement.appendChild(deleteButtonElement);
// <div>をtodo-listに追加
todoListElement.appendChild(todoElement);
});
};
// ボタンが押されたときにaddTodo関数を呼び出す
const addButton = document.getElementById("add-button");
document.addEventListener("DOMContentLoaded", fetchAndDisplayTodoList);
server.js
を以下のように編集します。
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { cors } from "hono/cors";
const app = new Hono();
app.use(cors({ origin: "*" }));
const todoList = [
{ id: "1", title: "JavaScriptを勉強する", completed: false },
{ id: "2", title: "TODOアプリを自作する", completed: false },
{ id: "3", title: "漫画を読み切る", completed: true },
{ id: "4", title: "ゲームをクリアする", completed: false },
];
// TODOリストの取得
app.get("/api/todo", (c) => c.json(todoList, 200));
serve({
fetch: app.fetch,
port: 8000,
});
サーバーを起動してみよう
以下のコマンドでサーバーを起動します。
npm run start
index.html
をブラウザで開いて、TODOリストにチェックボックスと削除ボタンが表示されていることを確認しましょう。
ただし、チェックボックスや削除ボタンをクリックしても、まだ何も起こりません。
実際に動作することが確認出来たら、サーバーを切断してhono-todo-app
ディレクトリでコミットしましょう。
git add .
git commit -m "TODOを表示する"
TODOの状態を更新してみよう(実践)
先ほど作成したチェックボックスや削除ボタン、テキストボックスにイベントを追加して、TODOの状態を更新できるようにしてみましょう。
まずは、script.js
に以下のコードを貼り付け、抜けているコードを埋めてみましょう。
// サーバーからTODOリストを取得して表示する
const fetchAndDisplayTodoList = async () => {
const response = await fetch("http://localhost:8000/api/todo");
const todoList = await response.json();
const todoListElement = document.getElementById("todo-list");
todoListElement.innerHTML = "";
todoList.forEach((todo) => {
// チェックボックスを生成
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = todo.completed;
// チェックボックスの状態が変更されたときに、updateTodoStatus関数を呼び出す
checkbox.addEventListener("change", function () {
updateTodoStatus(todo.id, this.checked);
});
// テキストボックスを生成
// <input type="text" value="..." />
const inputElement = document.createElement("input");
inputElement.type = "text";
inputElement.value = todo.title;
// テキストボックスの値が変更されたときに、updateTodoTitle関数を呼び出す
inputElement.addEventListener("change", function () {
updateTodoTitle(todo.id, this.value);
});
// 削除ボタンを生成
// <button>削除</button>
const deleteButtonElement = document.createElement("button");
deleteButtonElement.textContent = "削除";
// 削除ボタンがクリックされたときに、deleteTodo関数を呼び出す
deleteButtonElement.addEventListener("click", function () {
deleteTodo(todo.id);
});
// <div>
// <input type="checkbox" />
// <input type="text" value="..." />
// <button>削除</button>
// </div>
const todoElement = document.createElement("div");
todoElement.appendChild(checkbox);
todoElement.appendChild(inputElement);
todoElement.appendChild(deleteButtonElement);
todoListElement.appendChild(todoElement);
});
};
// サーバーに新しいTODOアイテムを追加する
const addTodo = async () => {
// フォームからTODOのタイトルを取得
const todoTitleInput = document.getElementById("todo-title");
const todoTitle = todoTitleInput.value;
// タイトルが空でない場合にサーバーにTODOアイテムを追加
if (todoTitle) {
const response = await fetch("http://localhost:8000/api/todo", {
// POSTリクエストを送信
method: "POST",
headers: {
"Content-Type": "application/json",
},
// TODOのタイトルをサーバーに送信
body: JSON.stringify({ title: todoTitle }),
});
// サーバーからのレスポンスが成功の場合はTODOリストを再取得して表示
if (response.status === 200) {
// フォームをクリア
todoTitleInput.value = "";
fetchAndDisplayTodoList();
}
}
};
// サーバーからTODOアイテムを削除する
const deleteTodo = async (id) => {
/* 書いてみよう */
};
// サーバー上のTODOアイテムの completed を更新する
const updateTodoStatus = async (id, completed) => {
/* 書いてみよう */
};
// サーバー上のTODOアイテムの title を更新する
const updateTodoTitle = async (id, title) => {
/* 書いてみよう */
};
// ボタンが押されたときにaddTodo関数を呼び出す
const addButton = document.getElementById("add-button");
addButton.addEventListener("click", addTodo);
document.addEventListener("DOMContentLoaded", fetchAndDisplayTodoList);
次に、server.js
に以下のコードを貼り付け、抜けているコードを埋めてみましょう。
TODOの編集・削除機能を追加しようのコードを参考にして書いてみましょう。
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { cors } from "hono/cors";
const app = new Hono();
app.use(cors({ origin: "*" }));
const todoList = [
{ id: "1", title: "JavaScriptを勉強する", completed: false },
{ id: "2", title: "TODOアプリを自作する", completed: false },
{ id: "3", title: "漫画を読み切る", completed: true },
{ id: "4", title: "ゲームをクリアする", completed: false },
];
// IDの初期値
let currentId = 1;
// TODOリストを取得
app.get("/api/todo", (c) => c.json(todoList, 200));
// 新しいTODOアイテムを作成
app.post("/api/todo", async (c) => {
});
// TODOの状態の更新
app.put("/api/todo/:id", async (c) => {
});
// TODOの削除
app.delete("/api/todo/:id", async (c) => {
});
// エラーハンドリング
app.onError((err, c) => {
return c.json({ message: err.message }, 400);
});
// サーバーを起動する
serve({
fetch: app.fetch,
port: 8000,
});
サンプルコード
// サーバーからTODOリストを取得して表示する
const fetchAndDisplayTodoList = async () => {
const response = await fetch("http://localhost:8000/api/todo");
const todoList = await response.json();
const todoListElement = document.getElementById("todo-list");
todoListElement.innerHTML = "";
todoList.forEach((todo) => {
// チェックボックスを生成
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.checked = todo.completed;
// チェックボックスの状態が変更されたときに、updateTodoStatus関数を呼び出す
checkbox.addEventListener("change", function () {
updateTodoStatus(todo.id, this.checked);
});
// テキストボックスを生成
// <input type="text" value="..." />
const inputElement = document.createElement("input");
inputElement.type = "text";
inputElement.value = todo.title;
// テキストボックスの値が変更されたときに、updateTodoTitle関数を呼び出す
inputElement.addEventListener("change", function () {
updateTodoTitle(todo.id, this.value);
});
// 削除ボタンを生成
// <button>削除</button>
const deleteButtonElement = document.createElement("button");
deleteButtonElement.textContent = "削除";
// 削除ボタンがクリックされたときに、deleteTodo関数を呼び出す
deleteButtonElement.addEventListener("click", function () {
deleteTodo(todo.id);
});
// <div>
// <input type="checkbox" />
// <input type="text" value="..." />
// <button>削除</button>
// </div>
const todoElement = document.createElement("div");
todoElement.appendChild(checkbox);
todoElement.appendChild(inputElement);
todoElement.appendChild(deleteButtonElement);
todoListElement.appendChild(todoElement);
});
};
// サーバー上のTODOアイテムの completed を更新する
const updateTodoStatus = async (id, completed) => {
const response = await fetch(`http://localhost:8000/api/todo/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ completed }),
});
if (response.status === 200) {
fetchAndDisplayTodoList();
}
};
// サーバー上のTODOアイテムの title を更新する
const updateTodoTitle = async (id, title) => {
const response = await fetch(`http://localhost:8000/api/todo/${id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ title }),
});
if (response.status === 200) {
fetchAndDisplayTodoList();
}
};
// サーバーに新しいTODOアイテムを追加する
const addTodo = async () => {
const todoTitleInput = document.getElementById("todo-title");
const todoTitle = todoTitleInput.value;
if (todoTitle) {
const response = await fetch("http://localhost:8000/api/todo", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ title: todoTitle }),
});
if (response.status === 200) {
todoTitleInput.value = "";
fetchAndDisplayTodoList();
}
}
};
// サーバーからTODOアイテムを削除する
const deleteTodo = async (id) => {
const response = await fetch(`http://localhost:8000/api/todo/${id}`, {
method: "DELETE",
});
if (response.status === 200) {
fetchAndDisplayTodoList();
}
};
// ボタンが押されたときにaddTodo関数を呼び出す
const addButton = document.getElementById("add-button");
addButton.addEventListener("click", addTodo);
document.addEventListener("DOMContentLoaded", fetchAndDisplayTodoList);
import { serve } from "@hono/node-server";
import { Hono } from "hono";
import { cors } from "hono/cors";
const app = new Hono();
app.use(cors({ origin: "*" }));
const todoList = [
{ id: "1", title: "JavaScriptを勉強する", completed: false },
{ id: "2", title: "TODOアプリを自作する", completed: false },
{ id: "3", title: "漫画を読み切る", completed: true },
{ id: "4", title: "ゲームをクリアする", completed: false },
];
let currentId = 1;
app.get("/api/todo", (c) => c.json(todoList, 200));
app.post("/api/todo", async (c) => {
const param = await c.req.json();
if (!param.title) {
throw new Error("Title must be provided");
}
const newTodo = {
id: String(currentId++),
completed: !!param.completed,
title: param.title,
};
todoList.push(newTodo);
return c.json({ message: "Successfully created" }, 200);
});
app.put("/api/todo/:id", async (c) => {
const param = await c.req.json();
const id = c.req.param("id");
if (!param.title && param.completed === undefined) {
throw new Error("Either title or completed must be provided");
}
const todo = todoList.find((todo) => todo.id === id);
if (!todo) {
throw new Error("Failed to update task title");
}
if (param.title) {
todo.title = param.title;
}
if (param.completed !== undefined) {
todo.completed = param.completed;
}
return c.json({ message: "Task updated" }, 200);
});
app.delete("/api/todo/:id", async (c) => {
const id = c.req.param("id");
const todoIndex = todoList.findIndex((todo) => todo.id === id);
if (todoIndex === -1) {
throw new Error("Failed to delete task");
}
todoList.splice(todoIndex, 1);
return c.json({ message: "Task deleted" }, 200);
});
app.onError((err, c) => {
return c.json({ message: err.message }, 400);
});
serve({
fetch: app.fetch,
port: 8000,
});
サーバーを起動してみよう
もし、途中でテストをしたり動作を確認したい場合は、サーバーを起動してみましょう。
npm run start
ブラウザでindex.html
を開いて、新しいTODOを追加、完了済みチェック、削除ボタンなどの機能が正常に動作することを確認しましょう。
実際に動作することが確認出来たら、サーバーを切断してhono-todo-app
ディレクトリでコミットしましょう。
git add .
git commit -m "TODOアプリの完成"
まとめ
今回のTODOアプリの解説・サンプルコードの解説は次回の講習会の最初に行います。
次回の講習会では、データベースについて学んでいきます。