深入解析 Express 中介軟體 Multer:架構原理、實戰範例與生產環境最佳實踐
在現代 Web 開發中,處理客戶端傳遞的檔案上傳(如使用者大頭貼、PDF 報表、CSV 批次匯入等)是後端不可或缺的核心功能。Express 本身內建的 express.json() 與 express.urlencoded() 僅能解析標準 JSON 與 URL 編碼字串,無法處理包含二進位檔案資料的 multipart/form-data 格式。
Multer 是一套專為 Node.js 與 Express 生態系設計的輕量級中介軟體(Middleware),它基於高效能的串流解析庫 busboy 構建,能無縫攔截並解析傳入的多部分表單資料,將文字欄位掛載至 req.body,將檔案資訊掛載至 req.file 或 req.files。
一、 Multer 的核心工作機制與儲存引擎
Multer 在生命週期中會先檢驗請求標頭的 Content-Type 是否為 multipart/form-data。一旦匹配,它便會以資料串流(Streams)方式解析資料區塊,避免一次性將過大檔案載入伺服器記憶體造成崩潰。
Multer 提供了兩種核心儲存引擎(Storage Engines):
-
MemoryStorage (
multer.memoryStorage()): 將檔案暫存在系統記憶體中,封裝為Buffer物件。 -
適用場景:檔案需要先經過記憶體處理(如即時圖片裁切、壓縮、加密)或立即透過 SDK 轉傳至第三方物件儲存服務(如 AWS S3、Google Cloud Storage)。
-
DiskStorage (
multer.diskStorage()): 直接將檔案寫入本機檔案系統的指定目錄。 -
適用場景:單機部署、需要持久化本機暫存,或檔案體積較大不宜長駐記憶體的情境。
二、 實戰配置:基礎與多情境範例
1. 磁碟儲存與命名正規化
使用 diskStorage 時,建議自訂檔案名稱以避免檔名衝突或路徑遍歷(Path Traversal)漏洞。
const express = require('express');
const multer = require('multer');
const path = require('path');
const crypto = require('crypto');
const app = express();
// 設定磁碟儲存引擎
const diskStorage = multer.diskStorage({
destination: (req, file, cb) => {
// 確保該目錄已存在
cb(null, path.join(__dirname, 'uploads/'));
},
filename: (req, file, cb) => {
// 產生安全且唯一的隨機檔名,保留原始副檔名
const uniqueSuffix = crypto.randomBytes(16).toString('hex');
const ext = path.extname(file.originalname).toLowerCase();
cb(null, `${Date.now()}-${uniqueSuffix}${ext}`);
}
});
const upload = multer({ storage: diskStorage });
2. 常見應用場景路由配置
- 單一檔案上傳(例如:頭像上傳)
使用
upload.single(fieldName),成功後檔案物件存放於req.file。
app.post('/api/users/avatar', upload.single('avatar'), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: '請選擇要上傳的檔案' });
}
res.status(200).json({
message: '頭像上傳成功',
fileInfo: {
filename: req.file.filename,
size: req.file.size,
mimetype: req.file.mimetype
}
});
});
- 同欄位多檔案上傳(例如:相簿批次發布)
使用
upload.array(fieldName, maxCount),成功後檔案陣列存放於req.files。
app.post('/api/photos', upload.array('photos', 5), (req, res) => {
res.status(200).json({
message: `成功上傳 ${req.files.length} 張相片`,
files: req.files.map(f => f.filename)
});
});
- 多欄位混合上傳(例如:履歷投遞,含頭像與 PDF)
使用
upload.fields([{ name, maxCount }])。
const applicationUpload = upload.fields([
{ name: 'resume', maxCount: 1 },
{ name: 'coverLetter', maxCount: 1 }
]);
app.post('/api/applications', applicationUpload, (req, res) => {
const resumeFile = req.files['resume'] ? req.files['resume'][0] : null;
const letterFile = req.files['coverLetter'] ? req.files['coverLetter'][0] : null;
res.status(200).json({
applicant: req.body.applicantName, // 同步取得文字欄位
resume: resumeFile ? resumeFile.filename : null,
coverLetter: letterFile ? letterFile.filename : null
});
});
三、 生產級安全防護與檔案過濾
在生產環境中直接開放檔案上傳具有高度安全風險,必須嚴格配置過濾規則與錯誤處理。
// 檔案過濾器:僅允許 JPEG 與 PNG
const fileFilter = (req, file, cb) => {
const allowedMimeTypes = ['image/jpeg', 'image/png', 'image/webp'];
if (allowedMimeTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('不支援的檔案格式,僅接受 JPG/PNG/WEBP'), false);
}
};
const secureUpload = multer({
storage: diskStorage,
limits: {
fileSize: 2 * 1024 * 1024, // 限制最大 2MB
files: 1 // 限制單次請求最多檔案數
},
fileFilter: fileFilter
});
集中式錯誤處理(Error Handling)
Multer 拋出的例外需妥善捕獲,區分業務驗證錯誤與 Multer 本身限制錯誤:
app.post('/api/upload', (req, res, next) => {
secureUpload.single('doc')(req, res, (err) => {
if (err instanceof multer.MulterError) {
// 處理 Multer 內建錯誤(如超大檔案)
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(413).json({ error: '檔案大小超過 2MB 限制' });
}
return res.status(400).json({ error: `Multer 錯誤: ${err.message}` });
} else if (err) {
// 處理自訂 filter 拋出的錯誤
return res.status(400).json({ error: err.message });
}
// 正常處理後續邏輯
res.status(200).json({ success: true });
});
});
四、 高級整合架構:雲端儲存直接上傳(以 S3 為例)
在微服務或分散式無狀態(Stateless)架構中,不建議將檔案持久化於應用伺服器本機。標準做法是使用 multer-s3 套件或透過 memoryStorage 搭配 AWS SDK 直傳雲端:
const { S3Client } = require('@aws-sdk/client-s3');
const multerS3 = require('multer-s3');
const s3 = new S3Client({ region: 'ap-northeast-1' });
const s3Upload = multer({
storage: multerS3({
s3: s3,
bucket: 'your-production-bucket-name',
contentType: multerS3.AUTO_CONTENT_TYPE,
metadata: (req, file, cb) => {
cb(null, { fieldName: file.fieldname });
},
key: (req, file, cb) => {
const ext = path.extname(file.originalname);
cb(null, `uploads/${Date.now()}-${crypto.randomUUID()}${ext}`);
}
})
});
這種做法能確保伺服器水平擴展(Scale-out)時,所有實例都能一致存取檔案資源,同時卸載伺服器儲存容量與 I/O 負擔。