Node.js 主線任務二:原生 HTTP 與檔案上傳 (Formidable)
🛑 防暴雷警示:以下筆記包含主線任務的解答與實作細節,強烈建議先親手寫過一次,卡關再來對照參考喔!
本次任務學習了如何使用 Node.js 原生的 node:http 模組建立伺服器,並結合第三方套件 formidable (v3) 來處理前端傳來的 multipart/form-data(檔案上傳)。
為了讓程式碼結構更清晰、好維護,我們將整個流程拆分成多個「單一職責 (Single Responsibility)」的小函式。
1. 取得上傳設定與環境變數 (Config)
利用 process.env 讀取環境變數,並加上 || 給予預設值,避免環境變數未設定時發生錯誤。
關鍵點:
- 使用邏輯或運算子
||設定變數的預設值。 - 環境變數進來時是字串格式,必須使用
Number()轉換型態。 - 需將環境變數設定的 MB 大小,乘以
1024 * 1024換算為系統實際可用的 Bytes 單位。
💻 點擊展開程式碼解答
function getUploadConfig() {
return {
uploadDir: process.env.UPLOAD_DIR || "/tmp",
maxFileSize: (Number(process.env.MAX_FILE_SIZE_MB) || 5) * 1024 * 1024,
gymName: process.env.GYM_NAME || "未命名健身房",
};
}
2. 擷取檔案副檔名
將上傳檔案的完整名稱進行切割,以取得副檔名(例如 .png, .jpg)。
關鍵點:
- 使用
lastIndexOf(".")找出副檔名的起始小數點位置。 - 如果找不到
.(回傳 -1),需提前 return 空字串避免錯誤。 - 使用
slice()切割字串,並強制套用toLowerCase()轉為小寫,確保後續檢查大小寫一致。
💻 點擊展開程式碼解答
function getFileExtension(filename) {
const dotIndex = filename.lastIndexOf(".");
if (dotIndex === -1) {
return "";
}
return filename.slice(dotIndex).toLowerCase();
}
3. 解析與格式化 Metadata
收到上傳的檔案後,將 Formidable 解析出來的 file 物件提取出我們需要的關鍵資訊。
關鍵點:
- 取得的檔案大小 (
file.size) 為 Bytes,除以1024後需使用Math.round()四捨五入為 KB。 - 整合前面寫好的
getFileExtension函式,組裝包含filename,sizeKB,ext的乾淨物件以供記錄或回傳使用。
💻 點擊展開程式碼解答
function parseFileMetadata(file) {
return {
filename: file.originalFilename,
sizeKB: Math.round(Number(file.size) / 1024),
ext: getFileExtension(file.originalFilename),
};
}
function formatUploadLog(meta, config) {
return `[${config.gymName}] Uploaded ${meta.filename} (${meta.sizeKB} KB) → ${config.uploadDir}`;
}
4. 路由分派與檔案上傳邏輯
這裡是伺服器處理的核心。我們將「路由判斷」與「處理上傳」分開,保持 router 函式的乾淨與可讀性。
路由判斷 (Router)
只做一件事:決定誰來處理這個請求。
關鍵點:
- 必須同時比對
req.method === "POST"與req.url === "/coaches/avatar"。 - 不符合此路由的其他請求,應統一導向自訂的處理函式 (如
handleNotFound),回傳 404 狀態碼。
💻 點擊展開程式碼解答
function handleNotFound(req, res) {
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "Not Found" }));
}
function router(req, res, config) {
if (req.method === "POST" && req.url === "/coaches/avatar") {
handleUpload(req, res, config);
} else {
handleNotFound(req, res);
}
}
上傳處理 (handleUpload)
利用 formidable({ ... }) 實體化解析器來接收並儲存檔案。
關鍵點:
- Error 捕捉:使用
form.on("error")額外捕捉 Formidable 底層錯誤並使用console.log記錄。 - 陣列解構:在 Formidable v3 版本中,
files解析出來的欄位是一個「陣列」,必須使用files.file[0]來取得實際的單一檔案物件。 - 防呆檢查:若前端未傳送檔案 (
!file),需以res.writeHead回傳400 Bad Request與對應的 JSON 錯誤訊息。 - 回應成功:處理完畢後,回傳
200 OK,並設定Content-Type: application/json,將整理好的資料字串化回傳前端。
💻 點擊展開程式碼解答
function handleUpload(req, res, config) {
const form = formidable({
uploadDir: config.uploadDir,
maxFileSize: config.maxFileSize,
keepExtensions: true,
});
form.on("error", (err) => {
console.log(err); // 記錄底層 Log
});
form.parse(req, (err, fields, files) => {
if (err) {
res.writeHead(500, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: err.message }));
return;
}
const file = files.file && files.file[0];
if (!file) {
res.writeHead(400, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: "No file uploaded" }));
return;
}
// 解析資料與紀錄 Log
const meta = parseFileMetadata(file);
console.log(formatUploadLog(meta, config));
// 回傳成功資訊
res.writeHead(200, { "Content-Type": "application/json" });
res.end(
JSON.stringify({
filename: meta.filename,
sizeKB: meta.sizeKB,
ext: meta.ext,
savedPath: file.filepath,
})
);
});
}
5. 建立 HTTP 伺服器
最後將以上邏輯封裝進 createUploadServer,並綁定 Router。
關鍵點:
- 在建立伺服器前,務必使用
fs.existsSync檢查環境變數所設定的uploadDir是否存在。 - 若不存在,需使用
fs.mkdirSync建立資料夾,並且必須加上{ recursive: true }參數,以支援多層級目錄的自動建立。 - 透過
http.createServer啟動伺服器並綁定router函式處理所有請求。
💻 點擊展開程式碼解答
function createUploadServer(config) {
if (!fs.existsSync(config.uploadDir)) {
fs.mkdirSync(config.uploadDir, { recursive: true });
}
const server = http.createServer((req, res) => {
router(req, res, config);
});
return server;
}