一个用于解析表单数据(FormData)并上传文件的Node.js模块
亮点
- 更快(~900-2500 mb/sec),流式多部分解析
- 自动上传文件到磁盘(详细的,请看
options.fileWriteStreamHandler
) - 插件API - 可以自定义解析器和插件
- 低内存占用
- 优雅的错误处理
- 极高的测试覆盖率
安装
该 package包是一个双 ESM/commonjs package包。
该项目需要Node.js 版本 >= 10.13,可使用npm或yarn安装。
若你想为这个项目贡献代码,我们强烈建议使用Yarn。
# v2
npm install formidable@v2
# v3
npm install formidable
npm install formidable@v3
例子
更多例子请在项目的 examples/ 目录中查看。
在Node.js http模块中使用
通过Node.js的http模块来解析即将上传的文件。
import http from 'node:http';
import formidable, {errors as formidableErrors} from 'formidable';
/** 译者注:若你的Nodejs版本不支持import导入:
const http = require('http');
const {formidable, errors as formidableErrors} = require('formidable');
*/
const server = http.createServer(async (req, res) => {
if (req.url === '/api/upload' && req.method.toLowerCase() === 'post') {
// 解析上传文件
const form = formidable({});
let fields;
let files;
try {
[fields, files] = await form.parse(req);
} catch (err) {
// 例子:检查特定的错误
if (err.code === formidableErrors.maxFieldsExceeded) {
}
console.error(err);
res.writeHead(err.httpCode || 400, { 'Content-Type': 'text/plain' });
res.end(String(err));
return;
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ fields, files }, null, 2));
return;
}
// 前端显示上传文件表单
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end(`
<h2>With Node.js <code>"http"</code> module</h2>
<form action="/api/upload" enctype="multipart/form-data" method="post">
<div>Text field title: <input type="text" name="title" /></div>
<div>File: <input type="file" name="multipleFiles" multiple="multiple" /></div>
<input type="submit" value="Upload" />
</form>
`);
});
server.listen(8080, () => {
console.log('Server listening on http://localhost:8080/ ...');
});
在Express.js框架中使用
对于解析formData数据并上传文件,许多同类型的中间件都可以做到。但Formidable可以做得更好,因为它只需要Node.js的请求流(Request stream),不依赖任何第三方中间件 ,比如Express.js。
import express from 'express';
import formidable from 'formidable';
/** 译者注:若你的Nodejs版本不支持import导入:
const express = require('http');
const { formidable } = require('formidable');
*/
const app = express();
app.get('/', (req, res) => {
res.send(`
<h2>With <code>"express"</code> npm package</h2>
<form action="/api/upload" enctype="multipart/form-data" method="post">
<div>Text field title: <input type="text" name="title" /></div>
<div>File: <input type="file" name="someExpressFiles" multiple="multiple" /></div>
<input type="submit" value="Upload" />
</form>
`);
});
app.post('/api/upload', (req, res, next) => {
const form = formidable({});
form.parse(req, (err, fields, files) => {
if (err) {
next(err);
return;
}
res.json({ fields, files });
});
});
app.listen(3000, () => {
console.log('Server listening on http://localhost:3000 ...');
});
在Koa框架中使用
无论是Koa v1、v2还是未来的v3版本,使用方式是相同的。您可以像下例所示的那样手动使用formidable,也可以通过koa-better-body包来使用它,该包在底层使用了formidable,并支持更多的特性和不同的请求主体,有关更多信息,请查看其文档。
提示:本例是Koa V2的示例,在v2中你需要通过ctx.req
来获取Node.js的Request对象,而不是通过ctx.request
获取Koa的Request对象
import Koa from 'Koa';
import formidable from 'formidable';
/** 译者注:若你的Nodejs版本不支持import导入:
const Koa = require('http');
const { formidable } = require('formidable');
*/
const app = new Koa();
app.on('error', (err) => {
console.error('server error', err);
});
app.use(async (ctx, next) => {
if (ctx.url === '/api/upload' && ctx.method.toLowerCase() === 'post') {
const form = formidable({});
// not very elegant, but that's for now if you don't want to use `koa-better-body`
// or other middlewares.
await new Promise((resolve, reject) => {
form.parse(ctx.req, (err, fields, files) => {
if (err) {
reject(err);
return;
}
ctx.set('Content-Type', 'application/json');
ctx.status = 200;
ctx.state = { fields, files };
ctx.body = JSON.stringify(ctx.state, null, 2);
resolve();
});
});
await next();
return;
}
// show a file upload form
ctx.set('Content-Type', 'text/html');
ctx.status = 200;
ctx.body = `
<h2>With <code>"koa"</code> npm package</h2>
<form action="/api/upload" enctype="multipart/form-data" method="post">
<div>Text field title: <input type="text" name="title" /></div>
<div>File: <input type="file" name="koaFiles" multiple="multiple" /></div>
<input type="submit" value="Upload" />
</form>
`;
});
app.use((ctx) => {
console.log('The next middleware is called');
console.log('Results:', ctx.state);
});
app.listen(3000, () => {
console.log('Server listening on http://localhost:3000 ...');
});
基准测试程序数值
这个基准相当旧,来自旧的代码库。但也许确实如此。以前的数字是约500mb /秒。目前,随着迁移到新的Node.js流API,它会更快。您可以清楚地看到Node版本之间的差异。
提示:将来可以进行更好的基准测试。
基准测试在8GB内存,Xeon X3440 (2.53 GHz, 4核,8线程)
~/github/node-formidable master
❯ nve --parallel 8 10 12 13 node benchmark/bench-multipart-parser.js
⬢ Node 8
1261.08 mb/sec
⬢ Node 10
1113.04 mb/sec
⬢ Node 12
2107.00 mb/sec
⬢ Node 13
2566.42 mb/sec
API
Formidable / IncomingForm
Formidable
和IncomingForm
(构造函数)是等价的。
请将配置项传递给函数/构造函数,而不是将它们赋值给其实例
import formidable from 'formidable';
const form = formidable(options);
译者注:若你的Nodejs版本不支持import导入,请不要使用export default
的导出,可以使用export
的导出,比如const { IncomingForm } = require('formidable')
。下面是该中间件的导出页面,以供参考
/**源代码src/index.js */
import PersistentFile from './PersistentFile.js';
import VolatileFile from './VolatileFile.js';
import Formidable, { DEFAULT_OPTIONS } from './Formidable.js';
// make it available without requiring the `new` keyword
// if you want it access `const formidable.IncomingForm` as v1
const formidable = (...args) => new Formidable(...args);
const {enabledPlugins} = DEFAULT_OPTIONS;
/** 仅import支持 */
export default formidable;
/** import和require都支持 */
export {
PersistentFile as File,
PersistentFile,
VolatileFile,
Formidable,
// alias
Formidable as IncomingForm,
// as named
formidable,
// misc
DEFAULT_OPTIONS as defaultOptions,
enabledPlugins,
};
export * from './parsers/index.js';
export * from './plugins/index.js';
export * as errors from './FormidableError.js';
配置项
可以从src/Formidable.js中的DEFAULT_OPTIONS
变量查看具体的默认配置项。
options.encoding
{string} - 默认是'utf-8'
; 设置输入的表单字段编码options.uploadDir
{string} - 默认是Node.js的os.tmpdir()
路径;放置文件上传的目录。之后可以使用fs.rename()
来移动它们options.keepExtensions
{boolean} - 默认false
;是否包含原始文件的扩展名options.allowEmptyFiles
{boolean} - 默认false
;是否允许上传空文件options.minFileSize
{number} - 默认1 (1byte);限制每个上传文件的最小字节数options.maxFiles
{number} - 默认Infinity
(无穷); 限制一次性上传文件数量,设置Infinity
表示不限制options.maxFileSize
{number} - 默认200 * 1024 * 1024 (200mb);限制每个上传文件的最大字节数options.maxTotalFileSize
{number} - 默认为options.maxFileSize
的值; 限制一次性上传文件的合计最大字节数options.maxFields
{number} - 默认1000;限制表单字段的数量;设置Infinity
表示不限制options.maxFieldsSize
{number} - 默认20 * 1024 * 1024 (20mb);限制所有表单字段(文件除外)最大字节数options.hashAlgorithm
{string | false} - 默认false
;包含为传入文件计算的校验和,将其设置为某种哈希算法,请参阅crypto. createHash
的可用加密options.fileWriteStreamHandler
{function} - 默认null
, 默认情况下,将解析的每个文件写入当前主机文件系统; 该函数应该返回一个可写流(Writable stream)的实例,该实例将接收上传的文件数据。有了这个选项,您可以自定义任何关于上传的文件数据将被流式传输到哪里的上传行为。如果你想在其他类型的云存储(AWS S3, Azure blob存储,Google云存储)或私有文件存储中写入上传的文件,这就是你要找的选项。当定义此选项时,将丢失在主机文件系统中写入文件的默认行为。options.filename
{function} - 默认undefined
;用它来控制上传文件的名字。必须返回字符串。其将与options.uploadDir
连接options.filter
{function} - 默认该函数总是返回true
。该配置项函数将在文件上传前被调用。该函数须返回Boolean类型,否则将会导致form.parse
错误options.createDirsFromUploads
{boolean} - 默认false
.。如果为true
,则可以直接上传文件夹。使用<input type="file" name="folders" webkitdirectory directory multiple />
来创建一个表单,上传文件夹。必须同options.uploadDir和options.filename一起使用,options.filename
需返回一个带有'/' +【要创建的文件夹名称】
的字符串,其返回值将与options.uploadDir
连接组合成上传文件的路径
options.filename {function} function (name, ext, part, form) -> string
其中part参数可以分解为
const { originalFilename, mimetype} = part;
提:如果表单组合字段或者某个文件的大小超过了设置的阈值,就会触发一个“error”事件。
// The amount of bytes received for this form so far.(到目前为止,此表单收到的字节数。)
form.bytesReceived;
// The expected number of bytes in this form.(表单被期望的字节数)
form.bytesExpected;
options.filter {function} function ({name, originalFilename, mimetype}) -> boolean
类似于Array.filter
:return false
将会忽略正在解析的文件,并且继续处理下一个文件(如果有)
const options = {
filter: function ({name, originalFilename, mimetype}) {
// keep only images
return mimetype && mimetype.includes("image");
}
};
提示:可以在出现第一个错误时,使用外部变量取消所有上传
提示:使用 form.emit('error')
可以触发 form.parse
的错误
let cancelUploads = false;// create variable at the same scope as form
const options = {
filter: function ({name, originalFilename, mimetype}) {
// keep only images
const valid = mimetype && mimetype.includes("image");
if (!valid) {
form.emit('error', new formidableErrors.default('invalid type', 0, 400)); // optional make form.parse error
cancelUploads = true; //variable to make filter return false after the first problem
}
return valid && !cancelUploads;
}
};
.parse(request, ?callback)
解析Node.js Request对象,其中包含表单数据。如果没有提供callback,则返回一个promise。
const form = formidable({ uploadDir: __dirname });
form.parse(req, (err, fields, files) => {
console.log('fields:', fields);
console.log('files:', files);
});
// with Promise
const [fields, files] = await form.parse(req);
如果您对直接访问多部分流(multipart stream)感兴趣,可以重写此方法。但这样做会使'field'
/ 'file'
事件失效,那样的话,您将完全负责处理上传进程。
关于uploadDir
,给出以下目录结构
project-name
├── src
│ └── server.js
│
└── uploads
└── image.jpg
__dirname
表示源文件的目录路径
`${__dirname}/../uploads`
省略__dirname
将使路径相对于当前工作目录(译者注:当前node命令执行的目录)。如果是从 src/ 目录下启动server.js文件而不是 project-name/ 目录启动,则省略__dirname
与否返回的路径是一样的。
如果uploadDir
的值是null
,则将使用默认的Node.js os.tmpdir()
路径。
*提示:如果目录不存在,上传的文件将被丢弃。要确保它存在:。
import {createNecessaryDirectoriesSync} from "filesac";
const uploadPath = `${__dirname}/../uploads`;
createNecessaryDirectoriesSync(`${uploadPath}/x`);
在下面的例子中,我们监听几个事件,并触发data监听器。这样你就可以根据是否在文件发出之前、头值、头名称、字段、文件等等信息做任何事情了。
另一种方法是重写form.onPart
,稍后会展示。
form.once('error', console.error);
form.on('fileBegin', (formname, file) => {
form.emit('data', { name: 'fileBegin', formname, value: file });
});
form.on('file', (formname, file) => {
form.emit('data', { name: 'file', formname, value: file });
});
form.on('field', (fieldName, fieldValue) => {
form.emit('data', { name: 'field', key: fieldName, value: fieldValue });
});
form.once('end', () => {
console.log('Done!');
});
// If you want to customize whatever you want...
form.on('data', ({ name, key, value, buffer, start, end, formname, ...more }) => {
if (name === 'partBegin') {
}
if (name === 'partData') {
}
if (name === 'headerField') {
}
if (name === 'headerValue') {
}
if (name === 'headerEnd') {
}
if (name === 'headersEnd') {
}
if (name === 'field') {
console.log('field name:', key);
console.log('field value:', value);
}
if (name === 'file') {
console.log('file:', formname, value);
}
if (name === 'fileBegin') {
console.log('fileBegin:', formname, value);
}
});
.use(plugin: Plugin)
一个允许您扩展Formidable库的方法。默认情况下,我们包含4个插件,它们本质上是插入不同内置解析器的适配器。
通过这种方法添加的插件总是被启用的。
更多默认插件,详见src/plugins/
参数plugin结构如下:
function(formidable: Formidable, options: Options): void;
plugin是一个函数,该函数传递Formidable实例(README示例中的form
)和配置项作为参数。
提示:plugin函数的this指向Formidable实例。
const form = formidable({ keepExtensions: true });
form.use((self, options) => {
// self === this === form
console.log('woohoo, custom plugin');
// do your stuff; check `src/plugins` for inspiration
});
form.parse(req, (error, fields, files) => {
console.log('done!');
});
值得注意的是,在plugin函数内部使用this. options
, self. options
和options
,他们的值不总是相等的。所以最佳实践是始终使用this
,这样您以后就可以更轻松地独立测试您的插件。
如果你想禁用一些解析功能,你可以禁用对应于解析器插件。例如,如果你想禁用多部分解析(解析器路径src/parsers/Multipart.js,其在 src/plugins/multipart.js中被使用),你可以从options. enabledPlugins
中删除它,像这样:
import formidable, {octetstream, querystring, json} from "formidable";
const form = formidable({
hashAlgorithm: 'sha1',
enabledPlugins: [octetstream, querystring, json],
});
请注意,顺序可能也很重要。名称与src/plugins/ 文件夹中的文件一一对应。
对新的内置插件的Pull Request可能被接受——例如,更高级的querystring解析器。将你的插件作为一个新文件添加到src/plugins/文件夹中(小写),并遵循其他插件的制作方法。
form.onPart
你可以使用Formidable仅为您处理某些部分。或者参考#387获得灵感,例如,您可以验证mime-type。
const form = formidable();
form.onPart = (part) => {
part.on('data', (buffer) => {
// do whatever you want here
});
};
例如,强制将Formidable仅用于非文件的部分(即html字段)。
const form = formidable();
form.onPart = function (part) {
// let formidable handle only non-file parts
if (part.originalFilename === '' || !part.mimetype) {
// used internally, please do not override!
form._handlePart(part);
}
};
File接口
export interface File {
// 上传文件的字节数
// 如果文件仍在上传,该属性则表示已经写入磁盘的字节数 (请看fileBegin事件),
// this property says how many bytes of the file have been written to disk yet.
file.size: number;
// 文件被写入的路径,如果您项改变文件的上传路径,可以在fileBegin事件中修改它
file.filepath: string;
// 客户端上传的文件名称
file.originalFilename: string | null;
// 上传到服务器的文件名称:根据提供的配置项计算
file.newFilename: string | null;
// 客户端上传文件的mime类型
file.mimetype: string | null;
// 一个Date对象(或' null '),其中包含文件做后写入的时间
// 这里主要是为了兼容[W3C文件API草案](http://dev.w3.org/2006/webapi/FileAPI/)。
file.mtime: Date | null;
file.hashAlgorithm: false | |'sha1' | 'md5' | 'sha256'
// 如果配置项hashalgalgorithm的计算已经设置,你可以从这个变量中读取十六进制摘要(最后它将是一个字符串)
file.hash: string | object | null;
}
该方法返回文件的json表示形式,允许使用JSON.stringify()
处理,这对于记录和响应请求很有用。
事件
‘progress’
在每个传入数据块被解析后触发。可以用来处理上传进度(警告:仅用于服务器端进度条)。客户端最好使用XMLHttpRequest
对象的xhr.upload.onprogress =
form.on('progress', (bytesReceived, bytesExpected) => {});
‘field’
每当接收到 字段/值 对时触发。
form.on('field', (name, value) => {});
‘fileBegin’
每当在上传流中检测到新文件时触发。如果您希望将文件流传输到其他地方,同时缓冲文件系统上的上传,则使用此事件。
form.on('fileBegin', (formName, file) => {
// formName -- 表单的name(<input name="thisname" type="file">) 或http传输的文件名
// file.originalFilename -- http传输的文件名或者如果解析错误则是null
// file.newFilename -- 上传到服务器上的文件名(生成的十六进制id或者是options.filename的返回值)
// file.filepath 默认是根据配置项options.uploadDir+options.filename的路径,译者注:可以修改,修改后,上传文件路径也会随之改变
// file.filepath = CUSTOM_PATH // 改变最终上传的路径
});
‘file’
每当接收到字段/文件对时触发。file参数是File
的一个实例。
form.on('file', (formname, file) => {
// 此时修改file.filepath已经太晚了
// 如果配置了options.hash,则file.hash此时可用
//其他的和fileBegin事件一样
});
‘error’
当处理传入的表单数据出错时触发。一次请求发生错误时,将自动停止。如果你想继续触发data事件,必须手动调用request.resume()
。
可能error对象会被附加上error.httpCode
和error.code
form.on('error', (err) => {});
‘aborted’
当用户中止请求时触发。该事件的触发也可能是由于socket上的“timeout”或“close”事件引起的。当触发此事件之后,紧接着就会触发"error"事件。将来会有一个单独的“timeout”事件(需要在node core中进行更改)。
form.on('aborted', () => {});
‘end’
当接收到整个请求,并且所有包含的文件都已完成刷新到磁盘时触发。这是你发送响应的好时机。
form.on('end', () => {});
助手
firstValues
获取表单字段的第一个值,就像之前的3.0.0版本一样,没有多个参数,在仍然需要字符串数组的情况下,传递一个可选异常列表(比如 <select multiple>
)
import { firstValues } from 'formidable/src/helpers/firstValues.js';
// ...
form.parse(request, async (error, fieldsMultiple, files) => {
if (error) {
//...
}
const exceptions = ['thisshouldbeanarray'];
const fieldsSingle = firstValues(form, fieldsMultiple, exceptions);
// ...
}
readBooleans
Html表单input type=“checkbox”,发送已经选中值,将每一个input看成是复选框checkbox,将其转换成Boolean类型。只在firstValues
或类似的调用后使用。
import { firstValues } from 'formidable/src/helpers/firstValues.js';
import { readBooleans } from 'formidable/src/helpers/readBooleans.js';
// ...
form.parse(request, async (error, fieldsMultiple, files) => {
if (error) {
//...
}
const fieldsSingle = firstValues(form, fieldsMultiple);
const expectedBooleans = ['checkbox1', 'wantsNewsLetter', 'hasACar'];
const fieldsWithBooleans = readBooleans(fieldsSingle, expectedBooleans);
// ...
}