文件上传 #
一、文件上传概述 #
1.1 为什么需要文件上传? #
- 用户头像上传
- 图片/视频分享
- 文档管理
- 数据导入
1.2 常用中间件 #
| 中间件 | 说明 |
|---|---|
| multer | 最流行的文件上传中间件 |
| formidable | 功能强大的表单处理库 |
| busboy | 流式解析器 |
二、multer基础 #
2.1 安装 #
bash
npm install multer
2.2 基本用法 #
javascript
const express = require('express');
const multer = require('multer');
const app = express();
const upload = multer({ dest: 'uploads/' });
app.post('/upload', upload.single('file'), (req, res) => {
res.json({
file: req.file,
body: req.body
});
});
app.listen(3000);
2.3 req.file属性 #
javascript
{
fieldname: 'file',
originalname: 'document.pdf',
encoding: '7bit',
mimetype: 'application/pdf',
destination: 'uploads/',
filename: 'abc123',
path: 'uploads/abc123',
size: 1024
}
三、上传模式 #
3.1 单文件上传 #
javascript
app.post('/avatar', upload.single('avatar'), (req, res) => {
if (!req.file) {
return res.status(400).json({ error: '请选择文件' });
}
res.json({
message: '上传成功',
file: {
originalname: req.file.originalname,
size: req.file.size,
mimetype: req.file.mimetype
}
});
});
3.2 多文件上传(同名字段) #
javascript
app.post('/photos', upload.array('photos', 5), (req, res) => {
res.json({
message: '上传成功',
count: req.files.length,
files: req.files.map(f => ({
originalname: f.originalname,
size: f.size
}))
});
});
3.3 多文件上传(不同字段) #
javascript
app.post('/profile', upload.fields([
{ name: 'avatar', maxCount: 1 },
{ name: 'gallery', maxCount: 8 }
]), (req, res) => {
res.json({
avatar: req.files['avatar']?.[0],
gallery: req.files['gallery']
});
});
3.4 无文件上传 #
javascript
app.post('/data', upload.none(), (req, res) => {
res.json(req.body);
});
3.5 任意字段 #
javascript
app.post('/any', upload.any(), (req, res) => {
res.json({
files: req.files,
body: req.body
});
});
四、存储配置 #
4.1 磁盘存储 #
javascript
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 });
4.2 动态目录 #
javascript
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const userId = req.user.id;
const dir = `uploads/${userId}/`;
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
cb(null, dir);
},
filename: (req, file, cb) => {
cb(null, Date.now() + path.extname(file.originalname));
}
});
4.3 内存存储 #
javascript
const storage = multer.memoryStorage();
const upload = multer({ storage: storage });
app.post('/upload', upload.single('file'), (req, res) => {
const buffer = req.file.buffer;
res.json({
size: buffer.length,
mimetype: req.file.mimetype
});
});
五、文件过滤 #
5.1 按类型过滤 #
javascript
const fileFilter = (req, file, cb) => {
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('不支持的文件类型'), false);
}
};
const upload = multer({
storage: storage,
fileFilter: fileFilter
});
5.2 按扩展名过滤 #
javascript
const fileFilter = (req, file, cb) => {
const allowedExts = ['.jpg', '.jpeg', '.png', '.gif'];
const ext = path.extname(file.originalname).toLowerCase();
if (allowedExts.includes(ext)) {
cb(null, true);
} else {
cb(new Error('不支持的文件扩展名'), false);
}
};
5.3 动态过滤 #
javascript
const createFileFilter = (allowedTypes) => {
return (req, file, cb) => {
if (allowedTypes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error(`只支持 ${allowedTypes.join(', ')} 格式`), false);
}
};
};
const imageUpload = multer({
fileFilter: createFileFilter(['image/jpeg', 'image/png'])
});
const documentUpload = multer({
fileFilter: createFileFilter(['application/pdf', 'application/msword'])
});
六、文件大小限制 #
6.1 全局限制 #
javascript
const upload = multer({
limits: {
fileSize: 5 * 1024 * 1024
}
});
6.2 完整限制配置 #
javascript
const upload = multer({
limits: {
fileSize: 5 * 1024 * 1024,
files: 10,
fields: 20,
fieldSize: 1024 * 1024,
parts: 100
}
});
| 限制 | 说明 |
|---|---|
| fileSize | 单文件最大大小 |
| files | 最大文件数量 |
| fields | 最大字段数量 |
| fieldSize | 字段最大大小 |
| parts | 最大part数量 |
七、错误处理 #
7.1 基本错误处理 #
javascript
app.post('/upload', (req, res) => {
upload.single('file')(req, res, (err) => {
if (err instanceof multer.MulterError) {
if (err.code === 'LIMIT_FILE_SIZE') {
return res.status(400).json({ error: '文件太大' });
}
if (err.code === 'LIMIT_FILE_COUNT') {
return res.status(400).json({ error: '文件数量超限' });
}
return res.status(400).json({ error: err.message });
} else if (err) {
return res.status(400).json({ error: err.message });
}
res.json({ file: req.file });
});
});
7.2 错误类型 #
javascript
const multerErrors = {
LIMIT_PART_COUNT: 'part数量超限',
LIMIT_FILE_SIZE: '文件大小超限',
LIMIT_FILE_COUNT: '文件数量超限',
LIMIT_FIELD_KEY: '字段名太长',
LIMIT_FIELD_VALUE: '字段值太长',
LIMIT_FIELD_COUNT: '字段数量超限',
LIMIT_UNEXPECTED_FILE: '意外的文件字段'
};
7.3 完整错误处理 #
javascript
const handleUpload = (upload) => {
return (req, res, next) => {
upload(req, res, (err) => {
if (err) {
if (err instanceof multer.MulterError) {
return res.status(400).json({
error: multerErrors[err.code] || err.message
});
}
return res.status(400).json({ error: err.message });
}
next();
});
};
};
app.post('/upload',
handleUpload(upload.single('file')),
(req, res) => {
res.json({ file: req.file });
}
);
八、图片处理 #
8.1 安装sharp #
bash
npm install sharp
8.2 图片压缩 #
javascript
const sharp = require('sharp');
app.post('/avatar', upload.single('avatar'), async (req, res) => {
try {
const { buffer, originalname } = req.file;
const filename = Date.now() + path.extname(originalname);
const outputPath = `uploads/${filename}`;
await sharp(buffer)
.resize(300, 300, { fit: 'cover' })
.jpeg({ quality: 80 })
.toFile(outputPath);
res.json({
message: '上传成功',
path: outputPath
});
} catch (error) {
res.status(500).json({ error: '处理失败' });
}
});
8.3 生成缩略图 #
javascript
app.post('/photo', upload.single('photo'), async (req, res) => {
try {
const { buffer, originalname } = req.file;
const ext = path.extname(originalname);
const baseName = path.basename(originalname, ext);
const originalPath = `uploads/${Date.now()}_${originalname}`;
const thumbnailPath = `uploads/thumbnails/${Date.now()}_${baseName}_thumb${ext}`;
await sharp(buffer).toFile(originalPath);
await sharp(buffer)
.resize(200, 200, { fit: 'cover' })
.toFile(thumbnailPath);
res.json({
original: originalPath,
thumbnail: thumbnailPath
});
} catch (error) {
res.status(500).json({ error: '处理失败' });
}
});
九、完整示例 #
9.1 文件上传服务 #
javascript
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const sharp = require('sharp');
const app = express();
const storage = multer.diskStorage({
destination: (req, file, cb) => {
const uploadDir = 'uploads/';
if (!fs.existsSync(uploadDir)) {
fs.mkdirSync(uploadDir, { recursive: true });
}
cb(null, uploadDir);
},
filename: (req, file, cb) => {
const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1E9);
const ext = path.extname(file.originalname);
cb(null, uniqueSuffix + ext);
}
});
const fileFilter = (req, file, cb) => {
const allowedTypes = {
image: ['image/jpeg', 'image/png', 'image/gif', 'image/webp'],
document: ['application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
video: ['video/mp4', 'video/mpeg', 'video/quicktime']
};
const allAllowed = [...allowedTypes.image, ...allowedTypes.document, ...allowedTypes.video];
if (allAllowed.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('不支持的文件类型'), false);
}
};
const upload = multer({
storage: storage,
fileFilter: fileFilter,
limits: {
fileSize: 10 * 1024 * 1024
}
});
const handleUploadError = (err, req, res, next) => {
if (err instanceof multer.MulterError) {
const messages = {
LIMIT_FILE_SIZE: '文件大小不能超过10MB',
LIMIT_FILE_COUNT: '文件数量超限',
LIMIT_UNEXPECTED_FILE: '意外的文件字段'
};
return res.status(400).json({ error: messages[err.code] || err.message });
}
if (err) {
return res.status(400).json({ error: err.message });
}
next();
};
app.post('/upload/single', upload.single('file'), handleUploadError, (req, res) => {
res.json({
success: true,
file: {
originalname: req.file.originalname,
filename: req.file.filename,
size: req.file.size,
mimetype: req.file.mimetype,
path: req.file.path
}
});
});
app.post('/upload/multiple', upload.array('files', 5), handleUploadError, (req, res) => {
res.json({
success: true,
count: req.files.length,
files: req.files.map(f => ({
originalname: f.originalname,
filename: f.filename,
size: f.size
}))
});
});
app.post('/upload/avatar', upload.single('avatar'), handleUploadError, async (req, res) => {
try {
const { path: tempPath, filename } = req.file;
const avatarPath = `uploads/avatars/${filename}`;
if (!fs.existsSync('uploads/avatars/')) {
fs.mkdirSync('uploads/avatars/', { recursive: true });
}
await sharp(tempPath)
.resize(200, 200, { fit: 'cover' })
.jpeg({ quality: 80 })
.toFile(avatarPath);
fs.unlinkSync(tempPath);
res.json({
success: true,
avatar: avatarPath
});
} catch (error) {
res.status(500).json({ error: '处理失败' });
}
});
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: '服务器错误' });
});
app.listen(3000);
十、安全考虑 #
10.1 文件类型验证 #
javascript
const fileType = require('file-type');
app.post('/upload', upload.single('file'), async (req, res) => {
const buffer = fs.readFileSync(req.file.path);
const type = await fileType.fromBuffer(buffer);
if (!type || !['image/jpeg', 'image/png'].includes(type.mime)) {
fs.unlinkSync(req.file.path);
return res.status(400).json({ error: '无效的文件类型' });
}
res.json({ file: req.file });
});
10.2 文件名清理 #
javascript
const sanitizeFilename = (filename) => {
return filename
.replace(/[^a-zA-Z0-9.-]/g, '_')
.toLowerCase();
};
10.3 病毒扫描 #
javascript
const clamav = require('clamav.js');
app.post('/upload', upload.single('file'), (req, res) => {
clamav.scan(req.file.path, (err, isInfected) => {
if (isInfected) {
fs.unlinkSync(req.file.path);
return res.status(400).json({ error: '文件包含病毒' });
}
res.json({ file: req.file });
});
});
十一、总结 #
文件上传要点:
| 概念 | 说明 |
|---|---|
| multer | 文件上传中间件 |
| storage | 存储配置 |
| fileFilter | 文件过滤 |
| limits | 大小限制 |
| 错误处理 | MulterError处理 |
| 图片处理 | sharp库 |
下一步,让我们学习模板引擎!
最后更新:2026-03-28