12k
All articles

Multer NPM:Node.js中的文件上传

使用 Multer 中间件配合 Express 处理 Node.js 中的文件上传,涵盖磁盘存储、内存存储、文件过滤及 AWS S3 集成的安全配置。

OpenReplay Team
OpenReplay Team
Multer NPM:Node.js中的文件上传

文件上传是Web应用程序中的常见需求,但在Node.js中正确处理它们可能具有挑战性。Multer是Express.js的中间件,使文件上传变得简单高效。本文探讨如何使用Multer在Node.js应用程序中处理文件上传。

关键要点

  • Multer通过处理multipart/form-data简化了Node.js中的文件上传
  • 根据需要配置存储选项(磁盘、内存或云存储)
  • 实施适当的验证和过滤以确保安全性
  • 设置适当的限制以防止滥用
  • 优雅地处理错误以提供良好的用户体验
  • 考虑为生产应用程序使用云存储

什么是Multer?

Multer是专门为处理multipart/form-data设计的Node.js中间件,这是通过HTML表单上传文件时使用的格式。基于busboy构建,Multer高效处理文件上传并使它们在Express路由中可访问。

Multer的主要特性包括:

  • 处理单个和多个文件上传
  • 可配置的存储选项
  • 文件过滤功能
  • 大小限制和验证
  • 与Express.js无缝集成

理解Multipart表单数据

在深入了解Multer之前,了解为什么常规表单处理不适用于文件上传很重要。

当表单包含文件时,浏览器将其编码为multipart/form-data,而不是默认的application/x-www-form-urlencoded。这种编码允许二进制数据(文件)与文本字段一起传输。

Express的内置中间件(express.json()express.urlencoded())无法处理multipart数据,这就是为什么我们需要Multer。

<!-- This form will send multipart/form-data -->
<form action=""/upload"" method=""POST"" enctype=""multipart/form-data"">
  <input type=""file"" name=""document"">
  <button type=""submit"">Upload</button>
</form>

Multer入门

安装

首先,在你的Node.js项目中安装Multer:

npm install multer

基本设置

以下是在Express应用程序中设置Multer的简单示例:

const express = require('express');
const multer = require('multer');

const app = express();
const upload = multer({ dest: 'uploads/' });

app.post('/upload', upload.single('document'), (req, res) => {
  // req.file包含有关上传文件的信息
  console.log(req.file);
  
  // req.body包含任何文本字段
  console.log(req.body);
  
  res.send('File uploaded successfully');
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

在此示例中:

  1. 我们创建了一个Multer实例,并指定上传文件的目标文件夹
  2. 我们使用upload.single('document')中间件处理单个文件上传
  3. 上传的文件信息在req.file中可用

处理不同的上传场景

Multer提供了几种方法来处理不同的文件上传场景:

单文件上传

// 处理字段名为'profile'的单个文件
app.post('/profile', upload.single('profile'), (req, res) => {
  // req.file包含上传的文件
  res.json({ 
    message: 'File uploaded successfully',
    file: req.file 
  });
});

多文件上传(相同字段)

// 处理字段名为'photos'的多个文件(最多5个)
app.post('/photos', upload.array('photos', 5), (req, res) => {
  // req.files包含文件数组
  res.json({ 
    message: `${req.files.length} files uploaded successfully`,
    files: req.files 
  });
});

多文件上传(不同字段)

const uploadFields = upload.fields([
  { name: 'avatar', maxCount: 1 },
  { name: 'gallery', maxCount: 3 }
]);

app.post('/profile', uploadFields, (req, res) => {
  // req.files是一个对象,字段名作为键
  // req.files.avatar包含头像文件
  // req.files.gallery包含图库文件
  res.json({ 
    message: 'Files uploaded successfully',
    avatar: req.files.avatar,
    gallery: req.files.gallery
  });
});

仅文本的Multipart表单

app.post('/form-data', upload.none(), (req, res) => {
  // 只处理文本字段,拒绝任何文件
  res.json(req.body);
});

存储选项

Multer提供不同的存储引擎来控制文件存储的位置和方式。

磁盘存储

要更好地控制文件存储,使用diskStorage

const storage = multer.diskStorage({
  destination: function(req, file, cb) {
    cb(null, 'uploads/');
  },
  filename: function(req, file, cb) {
    // 创建唯一文件名
    const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
    cb(null, file.fieldname + '-' + uniqueSuffix + getExtension(file.originalname));
  }
});

function getExtension(filename) {
  return filename.substring(filename.lastIndexOf('.'));
}

const upload = multer({ storage: storage });

此配置让你可以控制:

  • 文件存储位置(destination)
  • 文件命名方式(filename)

内存存储

如果你需要在内存中处理文件而不保存到磁盘:

const storage = multer.memoryStorage();
const upload = multer({ storage: storage });

app.post('/upload', upload.single('file'), (req, res) => {
  // req.file.buffer包含文件数据
  // 在内存中处理文件(例如,上传到云存储)
  
  // 示例:获取文件缓冲区和类型
  const fileBuffer = req.file.buffer;
  const fileType = req.file.mimetype;
  
  res.send('File processed');
});

内存存储在以下情况下很有用:

  • 你将文件传递给另一个服务(如S3或Cloudinary)
  • 你需要在保存前处理文件
  • 你在无服务器环境中工作

文件过滤

你可以使用fileFilter选项控制接受哪些文件:

const fileFilter = (req, file, cb) => {
  // 只接受图像文件
  if (file.mimetype.startsWith('image/')) {
    cb(null, true);
  } else {
    cb(new Error('Only image files are allowed!'), false);
  }
};

const upload = multer({ 
  storage: multer.diskStorage({...}),
  fileFilter: fileFilter
});

大小限制和安全性

设置限制对安全性和性能至关重要:

const upload = multer({
  storage: multer.diskStorage({...}),
  limits: {
    fileSize: 5 * 1024 * 1024, // 5MB(字节)
    files: 5 // 每个请求最多5个文件
  }
});

其他限制选项包括:

  • fieldNameSize:最大字段名大小(默认100字节)
  • fieldSize:最大字段值大小(默认1MB)
  • fields:最大非文件字段数(默认Infinity)
  • parts:最大部分数(字段+文件)(默认Infinity)

错误处理

应适当处理Multer错误以提供有意义的反馈:

app.post('/upload', (req, res) => {
  upload.single('file')(req, res, (err) => {
    if (err instanceof multer.MulterError) {
      // 发生Multer错误
      if (err.code === 'LIMIT_FILE_SIZE') {
        return res.status(400).json({ error: 'File too large' });
      }
      return res.status(400).json({ error: err.message });
    } else if (err) {
      // 发生未知错误
      return res.status(500).json({ error: 'Server error' });
    }
    
    // 一切正常
    res.json({ message: 'File uploaded successfully', file: req.file });
  });
});

完整示例:构建文件上传系统

让我们构建一个包含前端和后端的完整文件上传系统:

后端(server.js)

const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');

const app = express();
const port = 3000;

// 提供静态文件
app.use(express.static('public'));

// 配置存储
const storage = multer.diskStorage({
  destination: (req, file, cb) => {
    const uploadDir = 'uploads';
    // 如果目录不存在则创建
    if (!fs.existsSync(uploadDir)) {
      fs.mkdirSync(uploadDir);
    }
    cb(null, uploadDir);
  },
  filename: (req, file, cb) => {
    const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1E9)}${path.extname(file.originalname)}`;
    cb(null, uniqueName);
  }
});

// 配置文件过滤器
const fileFilter = (req, file, cb) => {
  // 接受图像和PDF
  const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'application/pdf'];
  
  if (allowedTypes.includes(file.mimetype)) {
    cb(null, true);
  } else {
    cb(new Error('Invalid file type. Only JPEG, PNG, GIF, and PDF are allowed.'), false);
  }
};

// 配置multer
const upload = multer({
  storage: storage,
  fileFilter: fileFilter,
  limits: {
    fileSize: 10 * 1024 * 1024, // 10MB
    files: 5
  }
});

// 单文件上传路由
app.post('/upload/single', (req, res) => {
  upload.single('file')(req, res, (err) => {
    if (err instanceof multer.MulterError) {
      return res.status(400).json({ success: false, message: err.message });
    } else if (err) {
      return res.status(400).json({ success: false, message: err.message });
    }
    
    if (!req.file) {
      return res.status(400).json({ success: false, message: 'No file provided' });
    }
    
    res.json({
      success: true,
      message: 'File uploaded successfully',
      file: {
        filename: req.file.filename,
        originalname: req.file.originalname,
        mimetype: req.file.mimetype,
        size: req.file.size,
        path: req.file.path
      }
    });
  });
});

// 多文件上传路由
app.post('/upload/multiple', (req, res) => {
  upload.array('files', 5)(req, res, (err) => {
    if (err instanceof multer.MulterError) {
      return res.status(400).json({ success: false, message: err.message });
    } else if (err) {
      return res.status(400).json({ success: false, message: err.message });
    }
    
    if (!req.files || req.files.length === 0) {
      return res.status(400).json({ success: false, message: 'No files provided' });
    }
    
    const fileDetails = req.files.map(file => ({
      filename: file.filename,
      originalname: file.originalname,
      mimetype: file.mimetype,
      size: file.size,
      path: file.path
    }));
    
    res.json({
      success: true,
      message: `${req.files.length} files uploaded successfully`,
      files: fileDetails
    });
  });
});

// 获取上传文件列表
app.get('/files', (req, res) => {
  const uploadDir = 'uploads';
  
  fs.readdir(uploadDir, (err, files) => {
    if (err) {
      return res.status(500).json({ success: false, message: 'Error reading files directory' });
    }
    
    res.json({
      success: true,
      files: files
    });
  });
});

app.listen(port, () => {
  console.log(`Server running at http://localhost:${port}`);
});

前端(public/index.html)

<!DOCTYPE html>
<html lang=""en"">
<head>
  <meta charset=""UTF-8"">
  <meta name=""viewport"" content=""width=device-width, initial-scale=1.0"">
  <title>File Upload with Multer</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      max-width: 800px;
      margin: 0 auto;
      padding: 20px;
    }
    .upload-section {
      margin-bottom: 30px;
      padding: 20px;
      border: 1px solid #ddd;
      border-radius: 5px;
    }
    .file-list {
      margin-top: 20px;
    }
    .file-item {
      padding: 10px;
      margin: 5px 0;
      background-color: #f5f5f5;
      border-radius: 3px;
    }
    .error {
      color: red;
      margin-top: 10px;
    }
    .success {
      color: green;
      margin-top: 10px;
    }
    progress {
      width: 100%;
      margin-top: 10px;
    }
  </style>
</head>
<body>
  <h1>File Upload with Multer</h1>
  
  <div class=""upload-section"">
    <h2>Single File Upload</h2>
    <form id=""singleUploadForm"">
      <input type=""file"" id=""singleFile"" name=""file"" required>
      <button type=""submit"">Upload File</button>
    </form>
    <div id=""singleUploadResult""></div>
    <progress id=""singleUploadProgress"" value=""0"" max=""100"" style=""display:none;""></progress>
  </div>
  
  <div class=""upload-section"">
    <h2>Multiple File Upload</h2>
    <form id=""multipleUploadForm"">
      <input type=""file"" id=""multipleFiles"" name=""files"" multiple required>
      <button type=""submit"">Upload Files</button>
    </form>
    <div id=""multipleUploadResult""></div>
    <progress id=""multipleUploadProgress"" value=""0"" max=""100"" style=""display:none;""></progress>
  </div>
  
  <div class=""upload-section"">
    <h2>Uploaded Files</h2>
    <button id=""refreshFiles"">Refresh List</button>
    <div id=""fileList"" class=""file-list""></div>
  </div>

  <script>
    document.addEventListener('DOMContentLoaded', function() {
      // Single file upload
      const singleUploadForm = document.getElementById('singleUploadForm');
      const singleUploadResult = document.getElementById('singleUploadResult');
      const singleUploadProgress = document.getElementById('singleUploadProgress');
      
      singleUploadForm.addEventListener('submit', function(e) {
        e.preventDefault();
        const fileInput = document.getElementById('singleFile');
        
        if (!fileInput.files.length) {
          singleUploadResult.innerHTML = '<div class=""error"">Please select a file</div>';
          return;
        }
        
        const formData = new FormData();
        formData.append('file', fileInput.files[0]);
        
        singleUploadProgress.style.display = 'block';
        singleUploadProgress.value = 0;
        singleUploadResult.innerHTML = '';
        
        const xhr = new XMLHttpRequest();
        
        xhr.upload.addEventListener('progress', function(e) {
          if (e.lengthComputable) {
            const percentComplete = (e.loaded / e.total) * 100;
            singleUploadProgress.value = percentComplete;
          }
        });
        
        xhr.onload = function() {
          if (xhr.status === 200) {
            const response = JSON.parse(xhr.responseText);
            singleUploadResult.innerHTML = `<div class=""success"">${response.message}</div>`;
            singleUploadForm.reset();
            loadFiles();
          } else {
            let errorMessage = 'Upload failed';
            try {
              const response = JSON.parse(xhr.responseText);
              errorMessage = response.message || errorMessage;
            } catch (e) {}
            singleUploadResult.innerHTML = `<div class=""error"">${errorMessage}</div>`;
          }
          singleUploadProgress.style.display = 'none';
        };
        
        xhr.onerror = function() {
          singleUploadResult.innerHTML = '<div class=""error"">Network error occurred</div>';
          singleUploadProgress.style.display = 'none';
        };
        
        xhr.open('POST', '/upload/single', true);
        xhr.send(formData);
      });
      
      // Multiple file upload
      const multipleUploadForm = document.getElementById('multipleUploadForm');
      const multipleUploadResult = document.getElementById('multipleUploadResult');
      const multipleUploadProgress = document.getElementById('multipleUploadProgress');
      
      multipleUploadForm.addEventListener('submit', function(e) {
        e.preventDefault();
        const fileInput = document.getElementById('multipleFiles');
        
        if (!fileInput.files.length) {
          multipleUploadResult.innerHTML = '<div class=""error"">Please select at least one file</div>';
          return;
        }
        
        const formData = new FormData();
        for (let i = 0; i < fileInput.files.length; i++) {
          formData.append('files', fileInput.files[i]);
        }
        
        multipleUploadProgress.style.display = 'block';
        multipleUploadProgress.value = 0;
        multipleUploadResult.innerHTML = '';
        
        const xhr = new XMLHttpRequest();
        
        xhr.upload.addEventListener('progress', function(e) {
          if (e.lengthComputable) {
            const percentComplete = (e.loaded / e.total) * 100;
            multipleUploadProgress.value = percentComplete;
          }
        });
        
        xhr.onload = function() {
          if (xhr.status === 200) {
            const response = JSON.parse(xhr.responseText);
            multipleUploadResult.innerHTML = `<div class=""success"">${response.message}</div>`;
            multipleUploadForm.reset();
            loadFiles();
          } else {
            let errorMessage = 'Upload failed';
            try {
              const response = JSON.parse(xhr.responseText);
              errorMessage = response.message || errorMessage;
            } catch (e) {}
            multipleUploadResult.innerHTML = `<div class=""error"">${errorMessage}</div>`;
          }
          multipleUploadProgress.style.display = 'none';
        };
        
        xhr.onerror = function() {
          multipleUploadResult.innerHTML = '<div class=""error"">Network error occurred</div>';
          multipleUploadProgress.style.display = 'none';
        };
        
        xhr.open('POST', '/upload/multiple', true);
        xhr.send(formData);
      });
      
      // Load files
      const fileList = document.getElementById('fileList');
      const refreshFilesButton = document.getElementById('refreshFiles');
      
      function loadFiles() {
        fetch('/files')
          .then(response => response.json())
          .then(data => {
            if (data.success) {
              if (data.files.length === 0) {
                fileList.innerHTML = '<p>No files uploaded yet</p>';
              } else {
                let html = '';
                data.files.forEach(file => {
                  html += `<div class=""file-item"">${file}</div>`;
                });
                fileList.innerHTML = html;
              }
            } else {
              fileList.innerHTML = `<div class=""error"">${data.message}</div>`;
            }
          })
          .catch(error => {
            fileList.innerHTML = '<div class=""error"">Error loading files</div>';
          });
      }
      
      refreshFilesButton.addEventListener('click', loadFiles);
      
      // Initial load
      loadFiles();
    });
  </script>
</body>
</html>

与云存储集成

对于生产应用程序,你通常希望将文件存储在云存储而不是服务器上。以下是如何将Multer与AWS S3集成:

const AWS = require('aws-sdk');
const multer = require('multer');
const multerS3 = require('multer-s3');

// 配置AWS
AWS.config.update({
  accessKeyId: process.env.AWS_ACCESS_KEY_ID,
  secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
  region: process.env.AWS_REGION
});

const s3 = new AWS.S3();

// 配置multer使用S3
const upload = multer({
  storage: multerS3({
    s3: s3,
    bucket: process.env.S3_BUCKET_NAME,
    metadata: function (req, file, cb) {
      cb(null, { fieldName: file.fieldname });
    },
    key: function (req, file, cb) {
      const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1E9)}`;
      cb(null, uniqueName);
    }
  }),
  limits: {
    fileSize: 5 * 1024 * 1024 // 5MB
  }
});

app.post('/upload', upload.single('file'), (req, res) => {
  res.json({
    success: true,
    message: 'File uploaded to S3 successfully',
    fileLocation: req.file.location // S3 URL
  });
});

注意:你需要安装multer-s3包:npm install multer-s3 aws-sdk

安全最佳实践

实施文件上传时,请遵循以下安全最佳实践:

  1. 验证文件类型:始终检查MIME类型和扩展名
  2. 限制文件大小:防止大文件上传导致的DoS攻击
  3. 扫描恶意软件:考虑为上传的文件集成病毒扫描
  4. 使用安全存储:不要将文件存储在公共目录中
  5. 生成随机文件名:不要直接使用用户提供的文件名
  6. 实施身份验证:确保只有授权用户可以上传文件
  7. 设置上传限制:限制每个请求和每个用户的文件数量

结论

Multer为Node.js应用程序中的文件上传处理提供了强大的解决方案。通过遵循本文概述的模式和实践,你可以实现安全、高效且用户友好的文件上传功能。无论你是构建简单的图像上传器还是复杂的文档管理系统,Multer灵活的API和集成能力使其成为在Express应用程序中处理multipart/form-data的绝佳选择。

常见问题

如何使用Multer处理大文件上传?

对于大文件,请考虑:在Multer配置中增加大小限制,为非常大的文件实施分块上传,使用流式处理文件而不将它们完全加载到内存中,以及在前端实施进度跟踪。

我可以在TypeScript中使用Multer吗?

是的,Multer与TypeScript配合良好。使用以下命令安装类型定义:npm install @types/multer

Listen to your bugs 🧘, with OpenReplay

See how users use your app and resolve issues fast.
Loved by thousands of developers

We use cookies to improve your experience. By using our site, you accept cookies.