フロントエンド開発において、フォーム、つまりmultipart/form-dataに遭遇することは避けられません:
- HTTP multipart/form-dataを転送する方法
- サーバ側でmultipart/form-dataを解析する方法。
- ブラウザがどのようにmultipart/form-dataをアセンブルするか
簡単なフォームを見てみましょう:
<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の転送プロセスですが、これには3つの大きな穴があります:
リクエストヘッダContent-Typeの境界セパレータは、リクエストボディで使われるセパレータより2本小さい。
リクエストヘッダからセパレータを取った後、リクエストボディを分割するために必ず2つの - を追加してください。
リクエストヘッダContent-Lengthの改行が "Ъ "ではなく "Ъ "になっています。
リクエスト・ボディの本当の顔は以下の文字列です: "-----------------------------340073633417401055292887335273rnContent-Disposition: form-data; name="ユーザ名"୧⃛(๑⃙⃘⁼̴̀꒳⁼̴́๑⃙⃘)r Zhang San -----------------------------340073633417401055292887335273 Content-Disposition: form-data; name="password" \ n123456 -----------------------------340073633417401055292887335273-- "
リクエストヘッダのContent-Lengthの値は、文字列ではなくバイト長を表します。
console.log('a1'.length) // 2 console.log(Buffer.from('a1').length) // 2 console.log(' .length) // 2 console.log(Buffer.from('張さん').length) // 6-----------------------------114007818631328932362459060915 Content-Disposition: form-data; name="avatar"; filename="1.jpg" Content-Type: image/jpeg xxxxxxファイルxxxxxxのバイナリデータ
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) // ヘッダーの境界部分をインターセプトする
const bodyBoundary = '--' + headBoundary // ボディ内の本当のセパレータは、その前にある2つの-である。
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)
ファイルタイプ部分は文字列形式とは異なり、ヘッド部分には2つのヘッダーフィールドがあり、Content-Typeヘッダーが1つ多く、Content-Dispositionヘッダーにはファイル名フィールドが1つ多く、ボディ部分はファイルのバイナリデータであることがわかります。
これらのルールがわかれば、サーバー側でmultipart/form-dataをデコードすることができます:
function handlePart(part) {
const [head, body] = part.split('
') // 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) // の最後でバッファを分割する。
return { key, value }
}
const filename = match[1]
const content = part.slice(head.length + 4, -2) // ファイルのバイナリ部分は、head +
最後の
fs.writeFileSync(filename, content)
return { key, value: filename }
}
キーの1つは、handlePartの部分で、つまり、個々の処理の各部分を分離するために、ファイルに保存するバイナリであれば、キーと値のペアを返す文字列です:
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
}
これはバッファの分割を伴いますが、nodejsは分割メソッドを提供していません:





