让你明明白白学知识,有代码,有讲解,抄的走,学的会!
我们知道,在node中,如果你想要获取到http请求中的响应体,必须监听 data和 end事件, 具体示例如下:
router.post('/api/t', (req, res) {
let data = ""
req.on('data', (chunk)= {
// 将请求体片段,都存储起来
data += chunk
})
req.on('end', ()=> {
// 在这里,你已经获取到所有的请求体数据
console.log(data)
// 异步 将数据形成实体文件
fs.writeFile('./1.txt', data, (err)=> {
if(err) throw err
console.log('文件写入成功')
})
})
})
思考几个问题
- 没有文件上传的情况下,响应体是什么样子?
- 有文件上传的情况下,响应体是什么样子
- 普通数据+文件上传时,响应体是什么样子?
- 如何处理二进制形式的响应体?
以下图片,都是将请求体生成实体的txt文件
前面3个问题已经回答完毕,我们从图片中看到,其实响应体的内容是存在规律的,所以,我们来具体分析第4个问题
响应体规律
- 每个字段都是以 ------WebKitFormBoundaryl8dFcQjQhK8gHvKY 开头
- 内容体和前面的内容,以空行分割
- 内容体、Content-Disposition: form-data; name="name" 都存在换行
- 如果请求体中包含【文件】,则描述行,多一行描述,对文件类型的描述 Content-Type: image/png
------WebKitFormBoundaryl8dFcQjQhK8gHvKY【换行符】
Content-Disposition: form-data; name="img"; filename="error-stack.png"【换行符】
Content-Type: image/png 【换行符】
空行【换行符】
具体的图片二进制数据【换行符】
------WebKitFormBoundaryl8dFcQjQhK8gHvKY--【换行符】
空行
前端代码
前端就没什么处理,直接一个表单即可, 记得指定input 的name, 后台是通过name去取值的, 这里使用了bootstrap美化一下表单,其他没有任何过多的代码
<form enctype="multipart/form-data" id='multy-form' method="post" action='/cors/multy-upload'>
<div class="form-group">
<label for="">姓名:</label><input class="form-control" type="text" name='name' id='name'>
</div>
<div class="form-group">
<label for="">年龄:</label><input class="form-control" type="text" name='age' id='age'>
</div>
<div class="form-group">
<label for="">介绍:</label><input type="text" class="form-control" name='desc' id='desc'>
</div>
<div class="form-group">
<label for="">上传照片</label>
<div>
<input type="file" name='img' id='file-upload' multiple>
</div>
</div>
<div>
<button type="submit" class="btn btn-primary">提交表单</button>
</div>
</form>
后台处理请求
这里使用 express起一个简单的服务,我们可以很轻松处理静态文件的问题,纯原生 node需要自己处理文件读写并返回,比较繁琐,我们的重点在 文件上传
express 服务
const express = require('express')
const path = require('path')
const logger = require('morgan')
const bodyParser = require('body-parser')
const app = express()
const router = express.Router()
// 记录请求日志的 日志中间件
app.use(logger('dev'))
app.use(express.static(path.join(__dirname, 'public')))
// 就写一个接口服务
router.post('/cors/multy-upload', (req, res) => {
resoveFile(req, res)
})
app.use(router)
app.listen(3000, ()=> {
console.log('http://localhost:3000')
})
// 解析请求体
function resoveFile(req, res) {
// .... 看下面的分析过程
}
解析请求体
现在,我们重点在 resoveFile 这个函数上
我们从请求头中会得到如下信息:
接受到前端发送的表单请求,HTTP中的请求体 长下面这样
------WebKitFormBoundarygD6nnN7FoXJAn7oh
Content-Disposition: form-data; name="age"
12
------WebKitFormBoundarygD6nnN7FoXJAn7oh
Content-Disposition: form-data; name="img"; filename="error-stack.png"
Content-Type: image/png
�PNG
这些都是文件类型,我删除了,观察更直观&
------WebKitFormBoundarygD6nnN7FoXJAn7oh--
我们发现,表单中的请求头中的 Conent-Type中,有一个boundary,这里的内容,不正是我们的【二进制数据转换成字符串】文件的分隔符吗,只是分割符号比我们多了2个 --
下面这个图中的红色的都是 \r\n 换行符
分隔符切割请求体
使用 ------WebKitFormBoundaryIvpVAvwvkcjmd139 作为分隔符号,切割buffer
分隔符示例:
let a = "-1-2-3-4-"
console.log( a.split('-') ) // 返回数组 [ '', '1', '2', '3', '4', '' ]
我们使用分割符号,切割以后 就是一个数组
在HTTP请求体中,一行完整的数据,是以\r\n作为结束符,去掉
// 3、丢弃掉每个buffer片段前后的\r\n
arr = arr.map(buffer => buffer.slice(2, buffer.length - 2))
这里,一定要明白,我们现在的数据是一个数组,下面的图清晰的标出了哪些数据,是一个整体
处理属性与属性值
我们从上图看出,前端传过来的name和值,在数组中,是一起的,他们的分隔符 \r\n\r\n
- 循环遍历
- 在遍历的项中找到 \r\n\r\n 的下标索引位置n
- 分割\r\n\r\n前面的内容,就可以获取到属性名 name
- 对于普通数据,对于内容属性的描述,就一个name,但是对于file类型的数据,有2行
// 普通数据
disposition Content-Disposition: form-data; name="name"
// 文件上传的数据
Content-Disposition: form-data; name="img"; filename="error-stack.png"
Content-Type: image/png
所以,我们的代码中需要有所区分
if (disposition.indexOf('\r\n') == -1) {
// 没找到,说明只有一行,那就是普通数据
} else {
// 文件上传的
}
- 分离内容
// 实体内容与 表单的key 之间存在一行空行\r\n
// 表单的属性name结束行会有\r\n, 所以n+4就可以截取到内容
let content = bufferItem.slice(n + 4)
n是前面的 \r\n\r\n 的索引位置
- 从字符串中分离普通数据的属性名 Content-Disposition: form-data; name="age"
... 前面省略的代码
if (disposition.indexOf('\r\n') == -1) {
// Content-Disposition: form-data; name="age"
let name = disposition.split("; ")[1].split("=")[1]
// 去掉key的前后引号
name = name.substring(1, name.length - 1);
// content怎么获取,上面5有介绍
post[name] = content
}
- 针对存在文件上传的数据,处理属性,分离文件类型
// 文件类型的数据
/*
Content-Disposition: form-data; name="img"; filename="error-stack.png" \r\n
Content-Type: image/png
*/
// 文件类型,会有2行的描述, 第一行有文件名filename,前端传过来的name
// 第二行是文件类型的描述
let [line1, line2] = disposition.split('\r\n')
// 抽出name,filename
let [, name, filename] = line1.split('; ')
let type = line2.split(': ')[1]
name = name.split('=')[1]
// 去掉引号
name = name.substring(1, name.length - 1)
filename = filename.split('=')[1]
// 去掉引号-- error-stack.png
filename = filename.substring(1, filename.length - 1)
至此, 我们已经完整的剥离了文件上传中,请求体的内容,这只是分享文件上传的过程,但是作为实际项目开发,这部分的逻辑实在是繁杂而臃肿,还是需要使用 multer 这种第三方的包去实现快速开发
完整的 resoveFile 代码
function resoveFile (req, res) {
let chunks = []
let num = 0;
req.on('data', (chunk) => {
chunks.push(chunk)
num += chunk.length
})
req.on('end', (err) => {
console.log('内容体长度--》', num)
// 最终流的内容体
let buffer = Buffer.concat(chunks)
console.log(buffer)
// 第一个阶段的buffer--log
writeTempBufferData(buffer, 1, '原始请求体')
// 解析字符串数据
let post = {}
let files = {}
if (req.headers['content-type']) {
// 'content-type': 'multipart/form-data; boundary=----WebKitFormBoundaryNDLBdEgBhssBJUQd',
// 获取到后面需要用到的解析二进制数据的分隔符
let str = req.headers['content-type'].split('; ')[1]
// ------WebKitFormBoundaryIvpVAvwvkcjmd139
if (str) {
let boundary = '--' + str.split('=')[1]
// 1、使用分隔符去切割整个请求体数据
let arr = buffer.split(boundary)
// 第2个阶段的buffer--log
writeTempBufferData(arr, 2)
// 2、丢掉头部和尾部, 因为切割后的数组,头部和剩下的就是一个 \r\n 没内容的,直接删除
arr.shift()
arr.pop()
writeTempBufferData(arr, 3)
// 3、丢弃掉每个buffer片段前后的\r\n
arr = arr.map(buffer => buffer.slice(2, buffer.length - 2))
// 第2个阶段的buffer--log
writeTempBufferData(arr, 4)
console.log("arr-->",arr.length)
// 4、每个数据在第一个 \r\n\r\n 处切割
arr.forEach(bufferItem => {
let n = bufferItem.indexOf('\r\n\r\n')
// 拿到有属性的数据-- Content-Disposition: form-data; name="age"
let disposition = bufferItem.slice(0, n)
// 实体内容与 表单的key 之间存在一行空行\r\n,表单的属性name结束行会有\r\n, 所以n+4就可以截取到内容
let content = bufferItem.slice(n + 4)
writeTempBufferData(content, 5, '内容体')
console.log('disposition',disposition, disposition.indexOf('\r\n'))
writeTempBufferData(disposition, 7, 'disposition')
if (disposition.indexOf('\r\n') == -1) {
// 转化成普通数据 Content-Disposition: form-data; name="age"
content = content.toString()
let name = disposition.split("; ")[1].split("=")[1]
// 去掉key的前后引号
name = name.substring(1, name.length - 1);
post[name] = content
} else {
// 文件类型的数据
/*
Content-Disposition: form-data; name="img"; filename="error-stack.png" \r\n
Content-Type: image/png
*/
// 文件类型,会有2行的描述, 第一行有文件名filename,前端传过来的name
// 第二行是文件类型的描述
let [line1, line2] = disposition.split('\r\n')
// 抽出name,filename
let [, name, filename] = line1.split('; ')
let type = line2.split(': ')[1]
name = name.split('=')[1]
// 去掉引号
name = name.substring(1, name.length - 1)
filename = filename.split('=')[1]
// 去掉引号-- error-stack.png
filename = filename.substring(1, filename.length - 1)
// 文件内容的实体,我们在上面已经切出啦了 content
fs.writeFile('./public/temp/' + filename, content, err => {
if (err) throw err
console.log(`写入的文件的数据`)
console.log('type-->', type)
console.log('filename-->', filename)
files[name] = {
filename,
type,
path: './public/temp/' + filename
}
})
}
})
// 上面读写文件 都是异步的,下面直接响应前端,断开HTTP连接
res.json({
msg: 'ok'
})
}
}
})
}
// 写入每个阶段,截取出来的Buffer数据,并查看是什么样子的
function writeTempBufferData (data, type, desc = '') {
let basePath = './public/temp/' + type + '--' + desc + '.txt'
fs.writeFile(basePath, data, (err) => {
console.log('写入成功')
})
}
注意事项:
let buffer = Buffer.concat(chunks)
上面代码中 这个buffer是一个Buffer类型,然后下面使用了Buffer中没有提供的split方法,我代码里面跟下面的链接的split是一模一样的
Node中 Buffer 利用 slice + indexOf 生成 split 方法