文件上传是 Web 应用中的常见需求,例如用户头像、附件、图片上传等。在 Node.js 和 Express 中,最流行的文件上传中间件是 Multer。本章将深入讲解如何使用 Multer 处理单文件和多文件上传,自定义存储、验证文件类型、处理错误,并探讨安全性和性能优化。
HTTP 协议中,文件上传通常使用 multipart/form-data 编码类型。这种格式将表单数据分割成多个部分,每个部分可以有自己的内容类型和文件名。服务器端需要解析这种复杂格式才能提取文件和字段。
Multer 是一个基于 busboy 的 Express 中间件,专门用于处理 multipart/form-data。它会解析请求,并将文件信息挂载到 req.file 或 req.files 上,文本字段则挂载到 req.body。
首先,安装 Multer:
npm install multer
最简单的用法,指定文件上传的目标目录:
const multer = require('multer');
const upload = multer({ dest: 'uploads/' });
app.post('/upload', upload.single('avatar'), (req, res) => {
console.log(req.file); // 文件信息
console.log(req.body); // 其他文本字段
res.send('文件上传成功');
});
这里 upload.single('avatar') 表示处理一个名为 "avatar" 的文件字段。上传的文件会被保存到 uploads/ 目录,文件名是随机生成的,没有扩展名。可以通过 req.file 获取文件详细信息,包括原始文件名、保存路径、大小等。
req.file 包含以下字段:
fieldname - 表单字段名originalname - 用户计算机上的文件名encoding - 文件编码mimetype - 文件的 MIME 类型size - 文件大小(字节)destination - 保存路径的文件夹filename - 保存在 destination 中的文件名path - 已上传文件的完整路径buffer - 整个文件的 Buffer(仅当使用 storage: memoryStorage 时)使用 upload.single(fieldname)。
app.post('/profile', upload.single('avatar'), (req, res) => {
res.json({
message: '头像上传成功',
file: req.file
});
});
使用 upload.array(fieldname[, maxCount]),处理同一字段的多个文件(例如 <input type="file" name="photos" multiple>)。
app.post('/photos', upload.array('photos', 12), (req, res) => {
// req.files 是文件数组
res.json({
message: `上传了 ${req.files.length} 张图片`,
files: req.files
});
});
使用 upload.fields([...]),指定每个字段的名称和最大数量。
app.post('/upload-mixed', upload.fields([
{ name: 'avatar', maxCount: 1 },
{ name: 'gallery', maxCount: 8 }
]), (req, res) => {
// req.files 是一个对象,键为字段名,值为文件数组
console.log(req.files.avatar); // 数组,可能为空
console.log(req.files.gallery);
res.send('上传成功');
});
如果希望上传任意字段的文件(不指定字段名),可以使用 upload.any(),但注意这可能导致安全问题,一般不推荐。
默认的 dest 选项会将文件保存到指定文件夹,但文件名是随机生成的,没有扩展名,也不保留原始文件名。通过自定义存储引擎,你可以完全控制文件保存的细节,例如文件名、路径等。
Multer 提供了 diskStorage 和 memoryStorage 两种存储引擎。
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
// 保留原始扩展名,添加时间戳避免重名
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
const ext = path.extname(file.originalname);
cb(null, file.fieldname + '-' + uniqueSuffix + ext);
}
});
const upload = multer({ storage: storage });
destination 和 filename 函数都接收请求对象 (req)、文件对象 (file) 和回调函数 (cb)。你可以根据需要动态决定保存路径和文件名,甚至可以读取 req.body 中的值来构建路径。
文件数据将作为 Buffer 保存在内存中,不会写入磁盘。适用于需要直接处理文件内容(例如上传到云存储)的场景。
const storage = multer.memoryStorage();
const upload = multer({ storage: storage });
app.post('/profile', upload.single('avatar'), (req, res) => {
// req.file.buffer 包含文件数据
// 可以将 buffer 上传到 S3 或进行其他处理
});
为了安全性和数据完整性,通常需要对上传的文件进行过滤,例如只允许图片、限制大小等。
通过 fileFilter 函数实现。该函数接收 req, file, cb,需要调用 cb(null, true) 接受文件,或 cb(null, false) 拒绝文件(或传递错误)。
const fileFilter = (req, file, cb) => {
// 允许的 MIME 类型
if (file.mimetype === 'image/jpeg' || file.mimetype === 'image/png') {
cb(null, true);
} else {
cb(new Error('仅支持 JPEG 和 PNG 图片'), false);
}
};
const upload = multer({
storage: storage,
fileFilter: fileFilter,
limits: { fileSize: 2 * 1024 * 1024 } // 限制 2MB
});
Multer 的错误(如文件过大、类型不符)会触发 Express 的错误处理中间件。可以定义专门的错误处理中间件来捕获这些错误:
app.use((err, req, res, next) => {
if (err instanceof multer.MulterError) {
// Multer 抛出的错误(如文件过大、太多文件)
return res.status(400).json({ error: err.message });
} else if (err) {
// 自定义错误(如 fileFilter 抛出的 Error)
return res.status(400).json({ error: err.message });
}
next();
});
limits.fileSize 设置最大允许大小,防止 DoS 攻击。file-type)验证。Multer 本身不支持进度事件,但可以结合流式上传或其他中间件(如 busboy 直接使用)实现。或者在前端使用 XMLHttpRequest 的上传进度事件。对于大文件,可以考虑分块上传。
一个简单的进度实现思路:使用 multer 搭配 express 无法直接获取进度,但可以在前端使用 axios 或 fetch 的 onUploadProgress 显示进度条。如果需要服务器端进度,可以考虑使用 busboy 手动处理。
下面是一个综合示例,展示如何处理用户资料更新,包括头像(单文件)和相册图片(多文件)的上传。
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const app = express();
// 确保上传目录存在
const uploadDir = 'uploads/';
const avatarDir = path.join(uploadDir, 'avatars');
const galleryDir = path.join(uploadDir, 'gallery');
[avatarDir, galleryDir].forEach(dir => {
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
});
// 配置存储
const storage = multer.diskStorage({
destination: (req, file, cb) => {
if (file.fieldname === 'avatar') {
cb(null, avatarDir);
} else if (file.fieldname === 'gallery') {
cb(null, galleryDir);
} else {
cb(new Error('未知字段'), null);
}
},
filename: (req, file, cb) => {
const unique = Date.now() + '-' + Math.round(Math.random() * 1E9);
const ext = path.extname(file.originalname);
cb(null, file.fieldname + '-' + unique + ext);
}
});
// 文件过滤
const fileFilter = (req, file, cb) => {
if (file.mimetype.startsWith('image/')) {
cb(null, true);
} else {
cb(new Error('只允许上传图片'), false);
}
};
const upload = multer({
storage: storage,
fileFilter: fileFilter,
limits: {
fileSize: 5 * 1024 * 1024 // 5MB
}
});
// 处理上传
app.post('/profile', upload.fields([
{ name: 'avatar', maxCount: 1 },
{ name: 'gallery', maxCount: 10 }
]), (req, res) => {
const { username, bio } = req.body;
const avatar = req.files.avatar ? req.files.avatar[0] : null;
const gallery = req.files.gallery || [];
// 保存用户信息到数据库(伪代码)
// await User.update({ username, bio, avatar: avatar?.filename, gallery: gallery.map(f => f.filename) });
res.json({
message: '资料更新成功',
user: { username, bio },
files: {
avatar: avatar,
gallery: gallery
}
});
}, (err, req, res, next) => {
// 错误处理
if (err instanceof multer.MulterError) {
return res.status(400).json({ error: err.message });
} else if (err) {
return res.status(400).json({ error: err.message });
}
});
前端可以使用如下表单:
<form action="/profile" method="POST" enctype="multipart/form-data">
<input type="text" name="username" placeholder="用户名">
<textarea name="bio" placeholder="个人简介"></textarea>
<input type="file" name="avatar" accept="image/*">
<input type="file" name="gallery" accept="image/*" multiple>
<button type="submit">提交</button>
</form>
对于生产环境,直接将文件存储在服务器磁盘可能不是最佳选择。可以使用内存存储,然后将文件上传到云存储(如 AWS S3、阿里云 OSS)。这里简要示意使用 multer-s3 中间件。
安装:npm install multer-s3 aws-sdk
const AWS = require('aws-sdk');
const multerS3 = require('multer-s3');
const s3 = new AWS.S3({ /* 配置 */ });
const upload = multer({
storage: multerS3({
s3: s3,
bucket: 'my-bucket',
key: (req, file, cb) => {
cb(null, Date.now().toString() + '-' + file.originalname);
}
})
});
此时 req.file 会包含 S3 相关字段,如 location(文件 URL)。
busboy)避免占用太多内存。