busboy、express-fileupload、formidable 和 multer 都是用于在 Node.js 应用中解析 multipart/form-data 表单数据(通常用于文件上传)的流行 npm 包。它们都基于底层流式解析器,但抽象层级、与 Express 的集成方式、配置灵活性和使用模式各不相同。busboy 是一个底层、高性能的流式解析器;express-fileupload 和 multer 是专为 Express 设计的中间件;而 formidable 是一个独立的、功能完整的表单解析器,也可用于非 Express 环境。
在构建现代 Web 应用时,处理用户上传的文件是常见需求。Node.js 生态中有多个库用于解析 multipart/form-data 请求,其中 busboy、express-fileupload、formidable 和 multer 最为流行。它们各有侧重 —— 有的追求底层控制,有的专注 Express 集成,有的强调框架无关性。本文将从实际工程角度,对比它们的核心机制、使用模式和适用场景。
busboy 是一个纯 JavaScript 实现的流式解析器,不依赖任何框架。它直接监听 HTTP 请求的 data 事件,通过事件驱动方式暴露 file(文件流)和 field(普通字段)事件。这意味着你需要自己管理流的消费、错误处理和存储逻辑。
// busboy: 原生 HTTP 服务器示例
const http = require('http');
const Busboy = require('busboy');
http.createServer((req, res) => {
if (req.method === 'POST') {
const busboy = new Busboy({ headers: req.headers });
busboy.on('file', (fieldname, file, info) => {
// file 是一个可读流
file.on('data', (data) => {
console.log('File chunk:', data.length);
});
file.on('end', () => {
console.log('File finished');
});
});
busboy.on('finish', () => {
res.writeHead(200, { 'Connection': 'close' });
res.end('Upload complete');
});
req.pipe(busboy);
}
}).listen(8000);
express-fileupload 是专为 Express 设计的中间件。它在内部使用 busboy,但将解析结果直接附加到 req.files(文件)和 req.body(普通字段)上,极大简化了使用流程。
// express-fileupload: Express 中间件
const express = require('express');
const fileUpload = require('express-fileupload');
const app = express();
app.use(fileUpload());
app.post('/upload', (req, res) => {
// 文件自动挂载到 req.files
const uploadedFile = req.files.myFile; // myFile 是表单字段名
uploadedFile.mv('./uploads/' + uploadedFile.name, (err) => {
if (err) return res.status(500).send(err);
res.send('File uploaded!');
});
});
formidable 是一个独立的表单解析器,最初为 Node.js 早期版本设计,现已完全现代化。它既可作为 Express 中间件使用,也支持原生 HTTP 服务器。解析结果可通过回调或 Promise 获取。
// formidable: Express 中间件方式
const express = require('express');
const formidable = require('formidable');
const app = express();
app.post('/upload', (req, res) => {
const form = formidable({ uploadDir: './uploads', keepExtensions: true });
form.parse(req, (err, fields, files) => {
if (err) return res.status(500).send(err);
// files 包含上传的文件信息
res.json({ fields, files });
});
});
multer 是 Express 官方推荐的文件上传中间件(虽非官方维护,但被广泛视为标准)。它通过声明式配置(如 diskStorage、memoryStorage)和中间件链,将文件处理逻辑与路由绑定。
// multer: Express 路由级中间件
const express = require('express');
const multer = require('multer');
const storage = multer.diskStorage({
destination: './uploads',
filename: (req, file, cb) => {
cb(null, file.fieldname + '-' + Date.now());
}
});
const upload = multer({ storage });
const app = express();
// 单文件上传
app.post('/upload', upload.single('myFile'), (req, res) => {
// 文件信息在 req.file
res.json({ file: req.file });
});
// 多文件上传
app.post('/upload-multiple', upload.array('photos', 12), (req, res) => {
// 文件信息在 req.files
res.json({ files: req.files });
});
所有库都支持将文件保存到内存或磁盘,但实现方式不同。
busboy:不提供内置存储。你必须手动将 file 流 pipe 到 fs.WriteStream(磁盘)或收集到 Buffer(内存)。express-fileupload:默认将小文件存入内存(Buffer),大文件写入临时目录。可通过 useTempFiles: true 强制全部使用临时文件,并用 tempFileDir 指定路径。formidable:通过 options 控制存储行为。keepExtensions: true 保留原始扩展名,uploadDir 指定目录。默认写入临时文件,也可通过 options.fileWriteStreamHandler 自定义流处理。multer:通过 storage 引擎明确指定策略。multer.memoryStorage() 存内存,multer.diskStorage() 存磁盘,并允许自定义文件名和路径。// multer 自定义磁盘存储
const storage = multer.diskStorage({
destination: (req, file, cb) => {
// 可根据请求动态决定目录
cb(null, 'uploads/' + req.user.id);
},
filename: (req, file, cb) => {
cb(null, `${Date.now()}-${file.originalname}`);
}
});
在复杂表单中,你可能需要根据字段名、文件类型或大小进行过滤。
busboy:在 file 事件回调中,通过 info.filename、info.mimeType 等参数手动判断,可调用 file.resume() 或 file.destroy() 控制流。express-fileupload:提供全局选项如 limits(限制文件大小)、abortOnLimit,但无法按字段动态过滤。formidable:通过 options.filter 函数实现过滤,可访问字段名、文件信息等。multer:提供 fileFilter 选项,这是一个函数,可在文件写入前决定是否接受。// multer 文件过滤示例
const upload = multer({
fileFilter: (req, file, cb) => {
if (file.mimetype.startsWith('image/')) {
cb(null, true); // 接受
} else {
cb(new Error('Only images allowed!'), false); // 拒绝
}
}
});
busboy:需监听 error 事件处理解析错误,并在流操作中捕获异常。express-fileupload:解析错误会触发 Express 的错误处理中间件(即传递给 next(err))。formidable:在 parse 回调的第一个参数返回错误,或在 Promise 模式下 reject。multer:验证错误(如文件过大、类型不符)会自动传递给 Express 错误处理中间件,也可通过自定义 fileFilter 抛出错误。| 场景 | 推荐库 | 理由 |
|---|---|---|
| Express 应用,需要快速集成 | express-fileupload | 配置极简,开箱即用,适合 MVP 或内部工具 |
| Express 应用,需精细控制存储和验证 | multer | 声明式 API,路由级配置,生产环境首选 |
| 非 Express 环境(如原生 HTTP、Fastify) | busboy 或 formidable | 两者均框架无关,busboy 更轻量,formidable 功能更全 |
| 需要流式处理大文件(如直接上传到 S3) | busboy | 直接暴露文件流,避免中间存储,节省内存 |
| 需要兼容旧版 Node.js 或遗留系统 | formidable | 历史悠久,向后兼容性好 |
busboy)或磁盘存储(multer/formidable),防止内存溢出。file.mimetype)甚至文件头(magic bytes)。express-fileupload 的默认行为),确保在请求结束或出错时清理未使用的临时文件。这四个库覆盖了从底层到高层的文件上传需求:
busboy 是基石,适合构建上层抽象或需要极致控制的场景。express-fileupload 是 Express 的“快捷方式”,牺牲灵活性换取开发速度。formidable 是通用型选手,平衡了功能与独立性。multer 是 Express 生态的“黄金标准”,在灵活性、健壮性和易用性之间取得最佳平衡。选择哪个库,最终取决于你的技术栈(是否用 Express)、项目规模(原型 vs 生产)、以及对控制粒度的需求。对于大多数 Express 应用,multer 通常是最佳起点;若需脱离框架或处理超大文件流,busboy 则不可替代。
选择 busboy 如果你需要对文件解析过程进行最细粒度的控制,或者正在构建一个不依赖 Express 的底层框架或工具。它提供原始事件流(如 file、field),适合需要自定义内存/磁盘存储策略、流式处理大文件或集成到非 Express 服务器(如原生 HTTP 或 Fastify)的场景。但需自行处理中间件逻辑和错误边界。
选择 express-fileupload 如果你使用 Express 并希望快速启用文件上传功能,且偏好将文件直接挂载到 req.files 对象上。它配置简单,支持内存或临时文件存储,适合中小型项目或原型开发。但其 API 较固定,高级定制(如按字段动态决定存储位置)能力有限。
选择 formidable 如果你需要一个独立于框架的、功能全面的表单解析器,或必须兼容非 Express 环境(如原生 Node.js HTTP 服务器)。它支持多种输出格式(文件、内存、流),并提供丰富的配置选项。虽然可与 Express 集成,但需手动编写中间件,不如专用 Express 中间件简洁。
选择 multer 如果你在 Express 应用中需要灵活、声明式的文件处理策略(如按路由指定存储引擎、文件过滤、重命名)。它深度集成 Express,通过 storage 引擎和 fileFilter 提供强大控制力,是 Express 生态中最成熟、广泛采用的文件上传方案,适合大多数生产级 Web 应用。
A node.js module for parsing incoming HTML form data.
Changes (breaking or otherwise) in v1.0.0 can be found here.
npm install busboy
const http = require('http');
const busboy = require('busboy');
http.createServer((req, res) => {
if (req.method === 'POST') {
console.log('POST request');
const bb = busboy({ headers: req.headers });
bb.on('file', (name, file, info) => {
const { filename, encoding, mimeType } = info;
console.log(
`File [${name}]: filename: %j, encoding: %j, mimeType: %j`,
filename,
encoding,
mimeType
);
file.on('data', (data) => {
console.log(`File [${name}] got ${data.length} bytes`);
}).on('close', () => {
console.log(`File [${name}] done`);
});
});
bb.on('field', (name, val, info) => {
console.log(`Field [${name}]: value: %j`, val);
});
bb.on('close', () => {
console.log('Done parsing form!');
res.writeHead(303, { Connection: 'close', Location: '/' });
res.end();
});
req.pipe(bb);
} else if (req.method === 'GET') {
res.writeHead(200, { Connection: 'close' });
res.end(`
<html>
<head></head>
<body>
<form method="POST" enctype="multipart/form-data">
<input type="file" name="filefield"><br />
<input type="text" name="textfield"><br />
<input type="submit">
</form>
</body>
</html>
`);
}
}).listen(8000, () => {
console.log('Listening for requests');
});
// Example output:
//
// Listening for requests
// < ... form submitted ... >
// POST request
// File [filefield]: filename: "logo.jpg", encoding: "binary", mime: "image/jpeg"
// File [filefield] got 11912 bytes
// Field [textfield]: value: "testing! :-)"
// File [filefield] done
// Done parsing form!
const { randomFillSync } = require('crypto');
const fs = require('fs');
const http = require('http');
const os = require('os');
const path = require('path');
const busboy = require('busboy');
const random = (() => {
const buf = Buffer.alloc(16);
return () => randomFillSync(buf).toString('hex');
})();
http.createServer((req, res) => {
if (req.method === 'POST') {
const bb = busboy({ headers: req.headers });
bb.on('file', (name, file, info) => {
const saveTo = path.join(os.tmpdir(), `busboy-upload-${random()}`);
file.pipe(fs.createWriteStream(saveTo));
});
bb.on('close', () => {
res.writeHead(200, { 'Connection': 'close' });
res.end(`That's all folks!`);
});
req.pipe(bb);
return;
}
res.writeHead(404);
res.end();
}).listen(8000, () => {
console.log('Listening for requests');
});
busboy exports a single function:
( function )(< object >config) - Creates and returns a new Writable form parser stream.
Valid config properties:
headers - object - These are the HTTP headers of the incoming request, which are used by individual parsers.
highWaterMark - integer - highWaterMark to use for the parser stream. Default: node's stream.Writable default.
fileHwm - integer - highWaterMark to use for individual file streams. Default: node's stream.Readable default.
defCharset - string - Default character set to use when one isn't defined. Default: 'utf8'.
defParamCharset - string - For multipart forms, the default character set to use for values of part header parameters (e.g. filename) that are not extended parameters (that contain an explicit charset). Default: 'latin1'.
preservePath - boolean - If paths in filenames from file parts in a 'multipart/form-data' request shall be preserved. Default: false.
limits - object - Various limits on incoming data. Valid properties are:
fieldNameSize - integer - Max field name size (in bytes). Default: 100.
fieldSize - integer - Max field value size (in bytes). Default: 1048576 (1MB).
fields - integer - Max number of non-file fields. Default: Infinity.
fileSize - integer - For multipart forms, the max file size (in bytes). Default: Infinity.
files - integer - For multipart forms, the max number of file fields. Default: Infinity.
parts - integer - For multipart forms, the max number of parts (fields + files). Default: Infinity.
headerPairs - integer - For multipart forms, the max number of header key-value pairs to parse. Default: 2000 (same as node's http module).
This function can throw exceptions if there is something wrong with the values in config. For example, if the Content-Type in headers is missing entirely, is not a supported type, or is missing the boundary for 'multipart/form-data' requests.
file(< string >name, < Readable >stream, < object >info) - Emitted for each new file found. name contains the form field name. stream is a Readable stream containing the file's data. No transformations/conversions (e.g. base64 to raw binary) are done on the file's data. info contains the following properties:
filename - string - If supplied, this contains the file's filename. WARNING: You should almost never use this value as-is (especially if you are using preservePath: true in your config) as it could contain malicious input. You are better off generating your own (safe) filenames, or at the very least using a hash of the filename.
encoding - string - The file's 'Content-Transfer-Encoding' value.
mimeType - string - The file's 'Content-Type' value.
Note: If you listen for this event, you should always consume the stream whether you care about its contents or not (you can simply do stream.resume(); if you want to discard/skip the contents), otherwise the 'finish'/'close' event will never fire on the busboy parser stream.
However, if you aren't accepting files, you can either simply not listen for the 'file' event at all or set limits.files to 0, and any/all files will be automatically skipped (these skipped files will still count towards any configured limits.files and limits.parts limits though).
Note: If a configured limits.fileSize limit was reached for a file, stream will both have a boolean property truncated set to true (best checked at the end of the stream) and emit a 'limit' event to notify you when this happens.
field(< string >name, < string >value, < object >info) - Emitted for each new non-file field found. name contains the form field name. value contains the string value of the field. info contains the following properties:
nameTruncated - boolean - Whether name was truncated or not (due to a configured limits.fieldNameSize limit)
valueTruncated - boolean - Whether value was truncated or not (due to a configured limits.fieldSize limit)
encoding - string - The field's 'Content-Transfer-Encoding' value.
mimeType - string - The field's 'Content-Type' value.
partsLimit() - Emitted when the configured limits.parts limit has been reached. No more 'file' or 'field' events will be emitted.
filesLimit() - Emitted when the configured limits.files limit has been reached. No more 'file' events will be emitted.
fieldsLimit() - Emitted when the configured limits.fields limit has been reached. No more 'field' events will be emitted.