seansie's blog

深入解析 Express 中介軟體 Multer:架構原理、實戰範例與生產環境最佳實踐

在現代 Web 開發中,處理客戶端傳遞的檔案上傳(如使用者大頭貼、PDF 報表、CSV 批次匯入等)是後端不可或缺的核心功能。Express 本身內建的 express.json()express.urlencoded() 僅能解析標準 JSON 與 URL 編碼字串,無法處理包含二進位檔案資料的 multipart/form-data 格式。

Multer 是一套專為 Node.js 與 Express 生態系設計的輕量級中介軟體(Middleware),它基於高效能的串流解析庫 busboy 構建,能無縫攔截並解析傳入的多部分表單資料,將文字欄位掛載至 req.body,將檔案資訊掛載至 req.filereq.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 負擔。