已經來到第十天了,我們已經學習不少Node.js的相關知識與概念,
今天要整合以下幾個模組:
把這些模組串起來,做出一個「簡易的網站伺服器」,有以下功能:
透過這樣的練習就能明白,Node.js是如何扮演「網站伺服器 + API 提供者」的角色,以及過程是如何運作的。
project/
├─ public/
│ ├─ index.html
│ ├─ style.css
│ └─ logo.jpg
└─ server.js
public/
→ 放所有靜態檔案server.js
→ Node.js 伺服器程式import http from "node:http";
import { promises as fsp } from "node:fs";
import { fileURLToPath } from "node:url";
import { dirname, join, extname, resolve } from "node:path";
// ESM 環境模擬 __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const PUBLIC_DIR = join(__dirname, "public");
// 假資料庫
let notes = [
{ id: 1, title: "第一則筆記" },
{ id: 2, title: "第二則筆記" }
];
// 常見 MIME 類型
const MIME = {
".html": "text/html; charset=utf-8",
".css": "text/css; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".json": "application/json; charset=utf-8",
".png": "image/png",
".jpg": "image/jpeg",
".jpeg": "image/jpeg",
".svg": "image/svg+xml",
};
// 讀取並回應檔案
async function serveStatic(pathname, res) {
const target = pathname === "/" ? "/index.html" : decodeURIComponent(pathname);
const filePath = resolve(join(PUBLIC_DIR, "." + target));
// 防止目錄穿越攻擊
if (!filePath.startsWith(PUBLIC_DIR)) {
res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
return res.end("Bad Request");
}
try {
const data = await fsp.readFile(filePath);
const ext = extname(filePath);
res.writeHead(200, { "Content-Type": MIME[ext] || "application/octet-stream" });
res.end(data);
} catch {
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
res.end("❌ Not Found");
}
}
// 建立伺服器
const server = http.createServer(async (req, res) => {
if (req.url.startsWith("/api/notes")) {
// API:取得所有筆記
if (req.method === "GET") {
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
res.end(JSON.stringify(notes));
}
// API:新增筆記
else if (req.method === "POST") {
let body = "";
req.on("data", chunk => (body += chunk));
req.on("end", () => {
const newNote = { id: Date.now(), title: JSON.parse(body).title };
notes.push(newNote);
res.writeHead(201, { "Content-Type": "application/json; charset=utf-8" });
res.end(JSON.stringify(newNote));
});
} else {
res.writeHead(405, { "Content-Type": "text/plain; charset=utf-8" });
res.end("Method Not Allowed");
}
} else {
// 靜態檔案處理
serveStatic(req.url, res);
}
});
server.listen(3000, () => {
console.log("🚀 靜態伺服器運行中:http://localhost:3000");
});
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>Node.js 靜態伺服器 + API</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<h1>Hello Node.js 🚀</h1>
<p>這是一個結合靜態檔案 & API 的伺服器。</p>
<h2>📒 筆記列表</h2>
<ul id="notes"></ul>
<h2>✍️ 新增筆記</h2>
<input type="text" id="noteInput" placeholder="輸入筆記內容">
<button id="addBtn">新增</button>
<script>
const notesList = document.getElementById("notes");
const noteInput = document.getElementById("noteInput");
const addBtn = document.getElementById("addBtn");
// 取得所有筆記 (GET)
async function fetchNotes() {
const res = await fetch("/api/notes");
const data = await res.json();
notesList.innerHTML = "";
data.forEach(note => {
const li = document.createElement("li");
li.textContent = note.title;
notesList.appendChild(li);
});
}
// 新增筆記 (POST)
async function addNote() {
const title = noteInput.value.trim();
if (!title) return alert("請輸入內容");
await fetch("/api/notes", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title })
});
noteInput.value = "";
fetchNotes(); // 新增後重新載入
}
addBtn.addEventListener("click", addNote);
// 頁面載入時執行
fetchNotes();
</script>
</body>
</html>
body {
font-family: sans-serif;
background: #eef7ff;
text-align: center;
padding: 50px;
}
h1 {
color: #3c873a;
}
li {
margin: 5px 0;
font-size: 16px;
}
path.resolve + startsWith
?假設有人輸入:
http://部屬網址/../../etc/passwd
伺服器可能會「跑出去」讀取敏感檔案。
這是 目錄穿越攻擊 (Directory Traversal)。
因此要用 resolve()
與 startsWith()
,確保只能存取 public/
目錄內的檔案,避免資安漏洞。
把檔案都設定好後,只要在 終端機上 輸入 node server.js
後,就會啟動伺服器了
會看到有一行 靜態伺服器運行中:http://localhost:3000 ,表示有成功運行!
打開後會看到這樣的畫面
成功順利的把靜態資源都放到網頁上了!
同時,也順利的成功用 GET
拿到 notes
的資料,並渲染到頁面上。
上面的範例圖,用開發者工具看會發現到有四個請求資源,
我們來看它請求資源的過程
[Client]
│
├── GET /index.html → 靜態檔案
├── GET /style.css → 靜態檔案
├── GET /node.svg → 靜態檔案
├── GET /api/notes → API (回傳 JSON)
└── 尚未發送 POST /api/notes → API (新增筆記)
│
▼
[Node.js Server]
│ http + fs + path
│
▼
[Response]
│
│ HTML / CSS / JSON / 圖片
接下來,就換你動手做看看囉~
今天我們完成了一個:
/api/notes
) → 讀取 & 新增筆記