Express 常用的中间件body-parser
只能解析Content-Type
为
application/json
application/x-www-form-urlencoded
application/octet-stream
text/plain
四种数据,别的类型(比如上传图片用的multipart/form-data
),就无能为力。
针对图片上传,有专门的中间件multer
,用来处理Content-Type
为multipart/form-data
的数据。
multer基本用法
安装
npm install --save multer
Epress
var express = require('express') var multer = require('multer') var upload = multer({ dest: 'uploads/' }) var app = express() // 上传单张图片,并指定上传时input的name为avatar app.post('/profile', upload.single('avatar'), function (req, res, next) { // req.file 对象类型,包含上传文件的基本信息 // req.body 将具有文本域数据,如果存在的话 }) // 上传12张图片,指定上传时input的name为photos app.post('/photos/upload', upload.array('photos', 12), function (req, res, next) { // req.files 数组类型,包含多个file // req.body 将具有文本域数据,如果存在的话 }) var cpUpload = upload.fields([{ name: 'avatar', maxCount: 1 }, { name: 'gallery', maxCount: 8 }]) app.post('/cool-profile', cpUpload, function (req, res, next) { // req.files 是一个对象 (String -> Array) 键是文件名,值是文件数组 // // 例如: // req.files['avatar'][0] -> File // req.files['gallery'] -> Array // // req.body 将具有文本域数据,如果存在的话 })
如果你需要处理一个只有文本域的表单,没有文件提交,你应当使用 .none()
:
var express = require('express') var app = express() var multer = require('multer') var upload = multer() app.post('/profile', upload.none(), function (req, res, next) { // req.body 包含文本域 })
file包含的信息如下
Key | Description | Note |
---|---|---|
fieldname |
Field name 由表单指定 | |
originalname |
用户计算机上的文件的名称 | |
encoding |
文件编码 | |
mimetype |
文件的 MIME 类型 | |
size |
文件大小(字节单位) | |
destination |
保存路径 | DiskStorage |
filename |
保存在 destination 中的文件名 |
DiskStorage |
path |
已上传文件的完整路径 | DiskStorage |
buffer |
一个存放了整个文件的 Buffer |
MemoryStorage |
multer(opts)
Multer 接受一个 options 对象,其中最基本的是 dest
属性,这将告诉 Multer 将上传文件保存在哪。如果你省略 options 对象,这些文件将保存在内存中,永远不会写入磁盘。写入内存容易造成内存溢出,应该避免这种情况,尽量都写入磁盘。
为了避免命名冲突,Multer 会修改上传的文件名。这个重命名功能可以根据您的需要定制。
以下是可以传递给 Multer 的选项。
Key | Description |
---|---|
dest or storage |
在哪里存储文件 |
fileFilter |
文件过滤器,控制哪些文件可以被接受 |
limits |
限制上传的数据 |
preservePath |
保存包含文件名的完整文件路径 |
通常,一般的网页应用,只需要设置 dest
属性,像这样:
var upload = multer({ dest: 'uploads/' })
如果你想在上传时进行更多的控制,你可以使用 storage
选项替代 dest
。Multer 具有 DiskStorage
和 MemoryStorage
两个存储引擎;另外还可以从第三方获得更多可用的。
.single(fieldname)
接受一个以 fieldname
命名的文件。这个文件的信息保存在 req.file
。
.array(fieldname[, maxCount])
接受一个以 fieldname
命名的文件数组。可以配置 maxCount
来限制上传的最大数量。这些文件的信息保存在 req.files
。
.fields(fields)
接受指定 fields
的混合文件。这些文件的信息保存在 req.files
。
fields
应该是一个对象数组,应该具有 name
和可选的 maxCount
属性。
Example:
[ { name: 'avatar', maxCount: 1 }, { name: 'gallery', maxCount: 8 } ]
.none()
只接受文本域。如果任何文件上传到这个模式,将发生 “LIMIT_UNEXPECTED_FILE” 错误。这和 upload.fields([])
的效果一样。
.any()
接受一切上传的文件。文件数组将保存在 req.files
。
警告: 确保你总是处理了用户的文件上传。 永远不要将 multer 作为全局中间件使用,因为恶意用户可以上传文件到一个你没有预料到的路由,应该只在你需要处理上传文件的路由上使用。
storage
磁盘存储引擎 (DiskStorage
)
磁盘存储引擎可以让你控制文件的存储。
var storage = multer.diskStorage({ destination: function (req, file, cb) { cb(null, '/tmp/my-uploads') }, filename: function (req, file, cb) { cb(null, file.fieldname + '-' + Date.now()) } }) var upload = multer({ storage: storage })
有两个选项可用,destination
和 filename
。他们都是用来确定文件存储位置的函数。
destination
是用来确定上传的文件应该存储在哪个文件夹中。也可以提供一个 string
(例如 '/tmp/uploads'
)。如果没有设置 destination
,则使用操作系统默认的临时文件夹。
注意: 如果你提供的 destination
是一个函数,你需要负责创建文件夹。当提供一个字符串,multer 将确保这个文件夹是你创建的。
filename
用于确定文件夹中的文件名的确定。 如果没有设置 filename
,每个文件将设置为一个随机文件名,并且是没有扩展名的。
注意: Multer 不会为你添加任何扩展名,你的程序应该返回一个完整的文件名。
每个函数都传递了请求对象 (req
) 和一些关于这个文件的信息 (file
),有助于你的决定。
注意 req.body
可能还没有完全填充,这取决于向客户端发送字段和文件到服务器的顺序。
内存存储引擎 (MemoryStorage
)
内存存储引擎将文件存储在内存中的 Buffer
对象,它没有任何选项。
var storage = multer.memoryStorage() var upload = multer({ storage: storage })
当使用内存存储引擎,文件信息将包含一个 buffer
字段,里面包含了整个文件数据。
警告: 当你使用内存存储,上传非常大的文件,或者非常多的小文件,会导致你的应用程序内存溢出。
limits
一个对象,指定一些数据大小的限制。Multer 通过这个对象使用 busboy,详细的特性可以在 busboy’s page 找到。
可以使用下面这些:
Key | Description | Default |
---|---|---|
fieldNameSize |
field 名字最大长度 | 100 bytes |
fieldSize |
field 值的最大长度 | 1MB |
fields |
非文件 field 的最大数量 | 无限 |
fileSize |
在 multipart 表单中,文件最大长度 (字节单位) | 无限 |
files |
在 multipart 表单中,文件最大数量 | 无限 |
parts |
在 multipart 表单中,part 传输的最大数量(fields + files) | 无限 |
headerPairs |
在 multipart 表单中,键值对最大组数 | 2000 |
设置 limits 可以帮助保护你的站点抵御拒绝服务 (DoS) 攻击。
fileFilter
设置一个函数来控制什么文件可以上传以及什么文件应该跳过,这个函数应该看起来像这样:
function fileFilter (req, file, cb) { // 这个函数应该调用 `cb` 用boolean值来 // 指示是否应接受该文件 // 拒绝这个文件,使用`false`,像这样: cb(null, false) // 接受这个文件,使用`true`,像这样: cb(null, true) // 如果有问题,你可以总是这样发送一个错误: cb(new Error('I don\'t have a clue!')) }
错误处理
当遇到一个错误,multer 将会把错误发送给 express。你可以使用一个比较好的错误展示页 (express标准方式)。
如果你想捕捉 multer 发出的错误,你可以自己调用中间件程序。如果你想捕捉 Multer 错误,你可以使用 multer
对象下的 MulterError
类 (即 err instanceof multer.MulterError
)。
var multer = require('multer') var upload = multer().single('avatar') app.post('/profile', function (req, res) { upload(req, res, function (err) { if (err instanceof multer.MulterError) { // 发生错误 } else if (err) { // 发生错误 } // 一切都好 }) })
前端代码
直接表单提交
<form action="/profile" method="post" enctype="multipart/form-data"> <h2>单图上传</h2> <input type="file" name="avatar"> <input type="submit" value="提交"> </form>
form 表单提交需要加enctype="multipart/form-data"
,确保Content-Type
是multipart/form-data
JQuery 异步提交
<input type="file" name="avatar" id="fileUploader"> <button onclick="uploadFile()">上传</button> <script src="https://www.geekschool.org/wp-content/uploads/2021/01/1610132616.9276361.jpg"></script> <script> function uploadFile () { var formData = new FormData(); var file = document.getElementById('fileUploader').files[0]; // 这里指定name值,input指定的name不起作用 formData.append('avatar', file); $.ajax({ url : '/profile', type : 'post', data : formData, // processData 默认true,会将data转化为string传输,这里必须设置为false processData: false, // contentType 默认'application/x-www-form-urlencoded; charset=UTF-8' // 这里不要设置为multipart/form-data,会丢失部分信息,设置为false,会自动识别contentType contentType: false, success : function (res) { console.log(res); }, }) } </script>
multipart/form-data
一个常见的数据提交的方式。我们使用表单上传文件时,必须让
表单的 enctype 等于 multipart/form-data。看下刚才上传图片的请求
Request Headers POST /profile HTTP/1.1 Host: localhost:3000 Connection: keep-alive Content-Length: 20502 Pragma: no-cache Cache-Control: no-cache Accept: application/json, text/javascript, */*; q=0.01 Origin: http://localhost:3000 X-Requested-With: XMLHttpRequest Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryIRoGm8wWREx5foPw Form Data ------WebKitFormBoundaryIRoGm8wWREx5foPw Content-Disposition: form-data; name="avatar"; filename="2646377831-58e393a0c7822_articlex.png" Content-Type: image/png ------WebKitFormBoundaryIRoGm8wWREx5foPw--
首先生成了一个 boundary 用于分割不同的字段,为了避免与正文内容重复,boundary 很长很复杂。
然后 Content-Type 里指明了数据是以 multipart/form-data 来编码,本次请求的 boundary 是什么内容。
消息主体里按照字段个数又分为多个结构类似的部分,每部分都是以 –boundary 开始,紧接着是内容描述信息,然后是回车,最后是字段具体内容(文本或二进制)。如果传输的是文件,还要包含文件名和文件类型信息。
消息主体最后以 –boundary– 标示结束。
举个例子:
<form action="/profile" method="post" enctype="multipart/form-data"> <input type="file" name="avatar" > <input type="text" name="myTextField"> <input type="checkbox" name="myCheckBox">Check</input> <button type="submit">提交</button> </form>
点击提交,提交的内容如下
POST /profile HTTP/1.1 Host: localhost:3000 Connection: keep-alive Content-Length: 1814270 Pragma: no-cache Cache-Control: no-cache Origin: http://localhost:3000 Upgrade-Insecure-Requests: 1 Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryJuLO36ZzIGEt5gGs ------WebKitFormBoundaryJuLO36ZzIGEt5gGs Content-Disposition: form-data; name="avatar"; filename="red-btn.png" Content-Type: image/png (imagedata) ------WebKitFormBoundaryJuLO36ZzIGEt5gGs Content-Disposition: form-data; name="myTextField" 2 ------WebKitFormBoundaryJuLO36ZzIGEt5gGs Content-Disposition: form-data; name="myCheckBox" on ------WebKitFormBoundaryJuLO36ZzIGEt5gGs--
参考文档:
jQuery ajax 文档: api.jquery.com/jquery.ajax…
multer 文档: github.com/expressjs/m…