深入浅出 multipart/form-data

微信扫一扫,分享到朋友圈

深入浅出 multipart/form-data

在前端开发过程中,不可避免地会遇到表单,即 multipart/form-data
,你是否知道:

multipart/form-data
multipart/form-data
multipart/form-data

我们来看一个简单的 form 表单:

<form action="/submit" method="POST" enctype="multipart/form-data">
<input type="text" name="username"><br>
<input type="text" name="password"><br>
<button>提交</button>
</form>
复制代码

当提交的时候,查看浏览器的网络请求:

请求头:

POST /submit HTTP/1.1
Host: localhost:3000
Accept-Encoding: gzip, deflate
Content-Type: multipart/form-data; boundary=---------------------------340073633417401055292887335273
Content-Length: 303
复制代码

请求体:

-----------------------------340073633417401055292887335273
Content-Disposition: form-data; name="username"
张三
-----------------------------340073633417401055292887335273
Content-Disposition: form-data; name="password"
123456
-----------------------------340073633417401055292887335273--
复制代码

这就是 multipart/form-data
的传输过程了,但是这里面有三个大坑:

  • 请求头 Content-Type 里面的 boundary 分隔符比请求体用的分隔符少了两个杠(-)

    从请求头中取分隔符之后,一定要在之前加两个 – 再对请求体进行分割

  • 请求头 Content-Length 的换行用的是 rn
    而不是 n

    请求体的真实面目是下面的字符串:
    “—————————–340073633417401055292887335273rnContent-Disposition: form-data; name=”username”rnrn张三rn—————————–340073633417401055292887335273rnContent-Disposition: form-data; name=”password”rnrn123456rn—————————–340073633417401055292887335273–rn”

  • 请求头 Content-Length 的值表示字节的长度,而不是字符串的长度

    因为字节的长度跟编码无关,而字符串的长度往往跟编码有关,举个例子,在 utf8 编码下:

    console.log('a1'.length) // 2
    console.log(Buffer.from('a1').length) // 2
    console.log('张三'.length) // 2
    console.log(Buffer.from('张三').length) // 6
    复制代码

如果仅仅是基本的字符串类型,完全可以用 www-form-urlencoded
来进行传输, multipart/form-data
强大的地方是其能够传输二进制文件的能力,我们看一下如果包含二进制文件的话应该如何处理。我们增加一个 file 类型的 input,上传一张图片作为头像,发现请求体多出了一部分:

-----------------------------114007818631328932362459060915
Content-Disposition: form-data; name="avatar"; filename="1.jpg"
Content-Type: image/jpeg
xxxxxx文件的二进制数据xxxxx
复制代码

可以发现,文件类型的 part 跟之前字符串的格式有所不同了,head 部分有两个头字段,多出一个 Content-Type 头,而且 Content-Disposition 头多出来 filename 字段,body 部分是文件的二进制数据。

了解这这些规律之后,接下来就可以在服务端对 multipart/form-data
进行解码了:

const http = require('http')
const fs = require('fs')
http
.createServer(function (req, res) {
// 获取 content-type 头,格式为: multipart/form-data; boundary=--------------------------754404743474233185974315
const contentType = req.headers['content-type']
const headBoundary = contentType.slice(contentType.lastIndexOf('=') + 1) // 截取 header 里面的 boundary 部分
const bodyBoundary = '--' + headBoundary // 前面加两个 - 才是 body 里面真实的分隔符
const arr = [], obj = {}
req.on('data', (chunk) => arr.push(chunk))
req.on('end', function () {
const parts = Buffer.concat(arr).split(bodyBoundary).slice(1, -1) // 根据分隔符进行分割
for (let i = 0; i < parts.length; i++) {
const { key, value } = handlePart(parts[i])
obj[key] = value
}
res.end(JSON.stringify(obj))
})
})
.listen(3000)
复制代码

其中关键的就是 handlePart 部分,即对分隔出来的每一部分单独处理,如果是二进制的就保存到文件,是字符串就返回键值对:

function handlePart(part) {
const [head, body] = part.split('rnrn') // buffer 分割
const headStr = head.toString()
const key = headStr.match(/name="(.+?)"/)[1]
const match = headStr.match(/filename="(.+?)"/)
if (!match) {
const value = body.toString().slice(0, -2) // 把末尾的 rn 去掉
return { key, value }
}
const filename = match[1]
const content = part.slice(head.length + 4, -2) // 文件二进制部分是 head + rnrn 再去掉最后的 rn
fs.writeFileSync(filename, content)
return { key, value: filename }
}
复制代码

这里面涉及到 buffer 的分割,nodejs 中并没有提供 split 方法,可根据 slice 方法自己实现:

Buffer.prototype.split = function (sep) {
let sepLength = sep.length, arr = [], offset = 0, currentIndex = 0
while ((currentIndex = this.indexOf(sep, offset)) !== -1) {
arr.push(this.slice(offset, currentIndex))
offset = currentIndex + sepLength
}
arr.push(this.slice(offset))
return arr
}
复制代码

微信扫一扫,分享到朋友圈

深入浅出 multipart/form-data

前端技术框架选型,跨端框架盘点

上一篇

命令行解析工具arg.js源码解读

下一篇

你也可能喜欢

深入浅出 multipart/form-data

长按储存图像,分享给朋友