这篇文章给学校组织上课的一部分,同时也是为了帮我补充以前没注意的问题。
网上有许多解析 From 表单的教程,但是他们要么太复杂,要么没法跟着完成,所以这里就从零开始解析一下 From 表单,当然实际使用还是用 mscdex/busboy 之类成熟的库。
🕵️♂️试一下
我们先来测试一下 form 表单上传服务器端会拿到什么东西。
const http = require('http');
const fs = require('fs');
let server = http.createServer((req,res) => {
let str = '';
req.on('data',(data) => {
str+=data;
});
req.on('end',() => {
console.log('|start|')
console.log(str);
console.log('|end|')
console.log(`content-type:`, req.headers['content-type'])
res.end();
})
})
server.listen(8001, () => {
console.log('Runing at: http://localhost:8001');
});
输出的结果:(是一个 1.txt 文件,里面的内容是 “redrock 测试”)
Runing at: http://localhost:8001
|start|
----------------------------083754191852957706053326
Content-Disposition: form-data; name="file"; filename="1.txt"
Content-Type: text/plain
redrock 测试
----------------------------083754191852957706053326
Content-Disposition: form-data; name="test"
testValue
----------------------------083754191852957706053326--
|end|
content-type: multipart/form-data; boundary=--------------------------083754191852957706053326
🔎 分析 FORM 表单内容
这里我们有两个问题,form 表单可以有多个属性,比如你可以上传头像,同时上传你的个人信息。所以怎么样将多个属性分出来,然后对是否是文件进行判断。
- 多个属性分出来:在这里我们可以看到是比较有规律的,我们可以通过请求头里面的 content-type 字段中的 boundary 来对进行分割
-
区分文件:
Content-Disposition 是否有两行来进行判别。
- 文件
Content-Disposition: form-data; name="file"; filename="1.txt" Content-Type: text/plain
- 普通
Content-Disposition: form-data; name="test"
上面的 from 表单部分我们就先转换成
--<分隔符>
Content-Disposition: form-data; name="file"; filename="1.txt"
Content-Type: text/plain
redrock 测试
--<分隔符>
Content-Disposition: form-data; name="test"
testValue
--<分隔符>--
把换行转换一下就成了这样
--<分隔符>\r\nContent-Disposition: form-data; name="file"; filename="1.txt"\r\nContent-Type: text/plain\r\n\r\nredrock 测试\r\n--<分隔符>\r\nContent-Disposition: form-data; name="test"\r\n\r\ntestValue\r\n--<分隔符>--\r\n
简化一下就是
--<分隔符>\r\n数据描述1\r\n数据描述2\r\n\r\n<文件内容>\r\n
--<分隔符>\r\n数据描述\r\n\r\n数据值\r\n
--<分隔符>--
我们学过 js 中有些内建类型中有 split方法能分割
[
空,
\r\n数据描述1\r\n数据描述2\r\n\r\n<文件内容>\r\n,
\r\n数据描述\r\n\r\n数据值\r\n,
--\r\n
]
将数组两头的东西拿掉
[
\r\n数据描述1\r\n数据描述2\r\n\r\n<文件内容>\r\n,
\r\n数据描述\r\n\r\n数据值\r\n
]
观察一下数组中每个两头都有 \r\n
[
数据描述1\r\n数据描述2\r\n\r\n<文件内容>,
数据描述\r\n\r\n数据值
]
再在 \r\n\r\n 分割一下
文件数据:[数据描述1\r\n数据描述2, <文件内容>]
或
普通数据:[数据描述, 数据值]
📊 完整代码
const http = require('http'),
URL = require('url'),
fs = require('fs');
Buffer.prototype.split = Buffer.prototype.split || function (b) {
let arr = [];
let cur = 0;
let n = 0;
while ((n = this.indexOf(b, cur)) != -1) {
arr.push(this.slice(cur, n));
cur = n + b.length;
}
arr.push(this.slice(cur));
return arr;
};
/**
* 显示文件
* @param {String} pathname
* @return {Promise<fs.ReadStream>}
*/
const fileShow = pathname =>
new Promise((reslove, reject) => {
fs.stat(pathname, (err, status) => {
if (err) reject(err);
if (!status.isFile()) reslove(false);
else reslove(fs.createReadStream(pathname, { encoding: 'utf-8', flag: 'r' }));
});
});
/**
* 上传文件
* @param {http.IncomingMessage} req
* @return {Promise}
*/
const uploadFile = req => new Promise((reslove, reject) => {
let arr = []; // 文件数据
req.on("data", function (chunk) {
// Buffer
arr.push(chunk)
});
req.on("end", function () {
try {
let data = Buffer.concat(arr);
if (!req.headers['content-type']) reject()
// req.headers['content-type'] ----> multipart/form-data; boundary=----WebKitFormBoundarys4qd7J4TILGjv4KE
let str = req.headers['content-type'].split('; ')[1];
// str ----> boundary=----WebKitFormBoundarys4qd7J4TILGjv4KE
if (str) reject();
// content-type 和 form-data 有两个 -- 的差距, 抹平差异
let boundary = '--' + str.split('=')[1];
//1.用"分隔符切分整个数据"
let arr = data.split(boundary);
//2.丢弃头尾两个数据
arr.shift();
arr.pop();
//3.丢弃掉每个数据头尾的"\r\n"
arr = arr.map(buffer => buffer.slice(2, buffer.length - 2));
//4.每个数据在第一个"\r\n\r\n"处切成两半
arr.forEach(buffer => {
let n = buffer.split('\r\n\r\n');
let disposition = n[0];
let content = n[1];
disposition = disposition.toString();
if (disposition.indexOf('\r\n') === -1) {
//普通数据
// Content-Disposition: form-data; name="user"
} else {
//文件数据
/*Content-Disposition: form-data; name="file"; filename="发现_1.png"
Content-Type: image/png*/
let [line1, line2] = disposition.split('\r\n');
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];
filename = filename.substring(1, filename.length - 1);
let path = `upload/${filename}`;
fs.writeFile(path, content, err => {
if (err) {
reject(err)
} else {
reslove()
}
});
}
});
} catch (error) {
reject(error)
}
})
});
const server = http.createServer(async (req, res) => {
const { url, method } = req;
const { pathname } = URL.parse(url);
if (method.toUpperCase() === 'GET') {
try {
const data = await fileShow('.' + pathname);
if (!data) {
res.writeHead(404);
res.end('404 not found');
} else {
res.writeHead(200);
data.pipe(res);
}
} catch (error) {
console.log(error);
res.writeHead(500);
res.end('500 Internal Server Error');
}
} else if (method.toUpperCase() === 'POST') {
try {
await uploadFile(req)
res.end();
} catch (error) {
console.log(error);
res.writeHead(500);
res.end('500 Internal Server Error');
}
} else {
// https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status/405
res.writeHead(405);
res.end('405 Method Not Allowed');
}
});
server.listen(8000);
console.log('Runing at: http://localhost:8000')