Back

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

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配合良好。使用以下命令安装类型定义: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