前端上传方式
- 使用
form
表单,enctype
设置为multipart/form-data
- 使用
ajax、fetch
或者axios
发送post
请求,请求参数使用FormData
进行封装,FormData
封装后请求参数默认为multipart/form-data
编码
文件上传为什么使用multipart/form-data
编码方式?
The encoding type application/x-www-form-urlencoded is inefficient for sending large quantities of binary data or text containing non-ASCII characters. Thus, a new media type,multipart/form-data, is proposed as a way of efficiently sending the values associated with a filled-out form from client to server.
编码类型application / x-www-form-urlencoded
对于发送大量二进制数据或包含非ASCII字符的文本效率不高。因此,提出了一种新的媒体类型,multipart / form-data
,作为从客户端到服务器有效发送与填写表格相关联的值的一种方式。
详情可以参考这篇文章chrome-dev-tools中Form Data与Request Payload有什么区别?
文件对象介绍
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
</head>
<body>
<form method="POST" id="uploadForm" enctype="multipart/form-data">
<input type="file" id="file" name="file" />
</form>
<button id="submit">submit</button>
</body>
</html>
<script>
$("#submit").click(function() {
console.log($("#file")[0].files[0])
});
</script>
文件(File
)接口提供有关文件的信息,并允许网页中的 JavaScript
访问其内容。
File
对象是特殊类型的 Blob
,且可以用在任意的 Blob
类型的 context
中。比如说, FileReader
, URL.createObjectURL()
, createImageBitmap()
, 及 XMLHttpRequest.send()
都能处理 Blob
和 File
。
另外还有一个blob
对象,也附上一张chrome
浏览器的截图。blob
,二进制大文档存储
两个对象里面都有size
和type
,按照官方的文档,file
集成了blob
,而且可以使用slice
方法.
slice
方法可以在当前的blob
数据中,取出一段数据,作为新的blob
。常用的就是文件断点上传。
node中的buffer、Stream、fs模块介绍
buffer
JavaScript
语言没有用于读取或处理二进制数据流的机制。
但在处理像TCP
流或文件流时,必须使用到二进制数据。因此在 Node.js
中,定义了一个 Buffer
类,该类用来创建一个专门存放二进制数据的缓存区。
在 Node.js
中,Buffer
类是随 Node
内核一起发布的核心库。Buffer
库为 Node.js 带来了一种存储原始数据的方法,可以让 Node.js
处理二进制数据,每当需要在 Node.js
中处理I/O
操作中移动的数据时,就有可能使用 Buffer
库。原始数据存储在 Buffer
类的实例中。一个 Buffer
类似于一个整数数组,但它对应于 V8
堆内存之外的一块原始内存(buffer
是 C++
层面分配的,所得内存不在 V8
内)。
const Buffer = require('buffer').Buffer
var buf1 = Buffer.from('tést');
var buf2 = Buffer.from('tést', 'latin1');
var buf3 = Buffer.from([1, 2, 3]);
console.log(buf1, buf2, buf3);
输出的结果为:
<Buffer 74 c3 a9 73 74> <Buffer 74 e9 73 74> <Buffer 01 02 03>
不是说node
引入buffer
是来处理二进制数据流吗?怎么转换成buffer
对象打印出来却不是二进制,而是十六进制呢?
在计算机内使用二进制表示数据,一个存储空间叫做一个 bit ,只能存储 0 或是 1。 通常,计算机把 8 个bit作为一个存储的单位,称为一个 Byte。
于是一个 Byte 可以出现 256 种不同的情况。一个 Buffer 是一段内存,比如大小为 2(Byte)的buffer,一共有 16 bit ,比如是
00000001 00100011
,可是这样显示太不方便。所以显示这段内存的数据的时候,用其对应的 16 进制就比较方便了,是01 23
,之所以用 16 进制是因为转换比较方便。内存仅仅存储的是二进制的数据,但是如何解释就是我们人类自己的事情了。。。。比如
A
在 内存中占用两个Byte,对应的内存状态是0000000 01000001
,而uint16
(JS不存在这个类型) 类型的65
对应的存储内存的状态也是这个。如果输出 Buffer 那么nodejs 输出的是内存实际存储的值(因为你没有给出如何解释这段内存中的数据),可是二进制显示起来不方便看,所以转换为 16 进制方便人类阅读。
如果转换为数组,那么意思就是把这个 buffer 的每一个字节解释为一个数字(其实是10进制数字,这是人类最方便的),所以是 0~255 的 10 进制数字。
总之,这样转化的目的是方便显示和查看。
streamStream
是一个抽象接口,Node
中有很多对象实现了这个接口。例如,对http
服务器发起请求的request
对象和服务端响应对象response
就是Stream
,还有stdout
(标准输出)。
你可以把流理解成一种传输的能力。通过流,可以以平缓的方式,无副作用的将数据传输到目的地。Stream
表示的是一种传输能力,Buffer
是传输内容的载体 (可以这样理解,Stream
:外卖小哥哥, Buffer
:你的外卖)。
在node中流无处不在:
流为什么这么好用还这么重要呢?
现在有个需求,我们要向客户端传输一个大文件。每次接收一个请求,就要把这个大文件读入内存,然后再传输给客户端。
const fs = require('fs');
const server = require('http').createServer();
server.on('request', (req, res) => {
fs.readFile('./big.file', (err, data) => {
if (err) throw err;
res.end(data);
});
});
server.listen(8000);
通过这种方式可能会产生以下三种后果:
- 内存耗尽
- 拖慢其他进程
- 增加垃圾回收器的负载
所以这种方式在传输大文件的情况下,不是一个好的方案。并发量一大,几百个请求过来很容易就将内存耗尽。
如果采用流呢?
const fs = require('fs');
const server = require('http').createServer();
server.on('request', (req, res) => {
const src = fs.createReadStream('./big.file');
src.pipe(res);
});
server.listen(8000);
采用这种方式,不会占用太多内存,可以将文件一点一点读到内存中,再一点一点返回给用户,读一部分,写一部分。(利用了 HTTP
协议的 Transfer-Encoding: chunked
分段传输特性),用户体验得到优化,同时对内存的开销明显下降。如果想在传输的过程中,想对文件进行处理,比如压缩、加密等等,也很好扩展。
fs文件模块
function readFile(path: PathLike | number, options: { encoding?: null; flag?: string; } | undefined | null, callback: (err: NodeJS.ErrnoException | null, data: Buffer) => void): void;
//当不指定encoding方式,默认返回buffer
function readFile(path: PathLike | number, options: { encoding: string; flag?: string; } | string, callback: (err: NodeJS.ErrnoException | null, data: string) => void): void;
//当指定encoding方式为string时,返回string数据
function createReadStream(path: PathLike, options?: string | {
flags?: string;
encoding?: string;
fd?: number;
mode?: number;
autoClose?: boolean;
start?: number;
end?: number;
highWaterMark?: number;
}): ReadStream;
//指定文件路径,将文件转化为可读流
原生node实现文件上传
运行以下程序看看二进制流数据传到服务端打印出来是怎么样子的
const fs = require('fs')
const http = require('http')
const server = http.createServer((req, res) => {
res.writeHead(200, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE'
})
if (req.url === "/upload" && req.method.toLowerCase() === "get") {
res.writeHead(200, {
"content-type": "text/html"
})
res.end(
'<form method="POST" id="uploadForm" action="http://127.0.0.1:8888/upload" enctype="multipart/form-data">' +
'First name:<br>' +
'<input type="text" name="firstname" value="Mickey">' +
'<br>' +
'Last name:<br>' +
'<input type="text" name="lastname" value="Mouse">' +
'<br>' +
'<input type="file" id="file" name="file" />' +
'<br>' +
'<input type="submit" value="Submit">' +
'</form>'
)
}
if (req.url === "/upload" && req.method.toLowerCase() === "post") {
// 上传接口
parseFile(req, res)
}
})
function parseFile(req, res) {
req.setEncoding("binary"); // 二进制编码
let body = ""; // 文件数据
req.on("data", function(chunk) {
body += chunk;
});
req.on("end", function() {
console.log(body);
})
}
server.listen(8888)
console.log('server is listening on 8888')
打印结果:
------WebKitFormBoundaryXTV5zMRqZzTOZtKa
Content-Disposition: form-data; name="firstname"
Mickey
------WebKitFormBoundaryXTV5zMRqZzTOZtKa
Content-Disposition: form-data; name="lastname"
Mouse
------WebKitFormBoundaryXTV5zMRqZzTOZtKa
Content-Disposition: form-data; name="file"; filename="bug.txt"
Content-Type: text/plain
1ã个人ä¸å¿æ°æ®æ¾ç¤ºnull 解å³
2ã设置é»è®¤å°åé®é¢ 解å³
3ãé¦é¡µååå¸å±é®é¢ï¼ä»·æ ¼æ¾ç¤ºé®é¢ 解å³
4ãæç订åä»·æ ¼è®¡ç®é®é¢ 解å³
5ãloginç»å
¥äºï¼ä½æ¯ä¸ªäººä¸å¿é¡µåè´ç©è½¦é¡µåºç°ç»å
¥æ ¡éªé®é¢ 解å³
6ã头åæ¾ç¤ºé®é¢
globalDataçææ¶é´æ®µ
1ãå¤ä¹ èè¯ppt éæ¥ è§£å³
2ãå¦æåå°æå¡å¯å¨ï¼ä¿®æ¹å¥½è®¢åé¨åææçbug é
3ãè¿ä¸é项ç®æµç¨ï¼ççè¿æåªäºç¹éè¦æ¹å è½»
4ãæ交订å页å°åéæ©è·³è½¬é®é¢ 解å³
5ãè´ç©è½¦ååæ°éè¾å
¥æ¡æ ¡éª 解å³
6ãè´ç©è½¦ååå¢å æ ¡éªé®é¢ 解å³
7ãè´ç©è½¦å订åä»·æ ¼é®é¢ 解å³
8ãæç订åä½é¢æ¾ç¤ºé®é¢ 解å³
9ãä¸åæ¶åçä½é¢å个人ä¸å¿çä½é¢ä¸ä¸è´
10ãåºä»éé¢ãææ£éé¢éä¹±discounté®é¢
------WebKitFormBoundaryXTV5zMRqZzTOZtKa--
可以看到客户端传到服务端的数据被编码成二进制,在request
流中。
使用stream
流模块中data、end
事件来完成文件读写:
const fs = require('fs')
const http = require('http')
const querystring = require('querystring')
const server = http.createServer((req, res) => {
res.writeHead(200, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'Content-Type',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE'
})
if (req.url === "/upload" && req.method.toLowerCase() === "get") {
res.writeHead(200, {
"content-type": "text/html"
})
res.end(
'<form method="POST" id="uploadForm" action="http://127.0.0.1:8888/upload" enctype="multipart/form-data">' +
'<input type="file" id="file" name="file" />' +
'<br>' +
'<input type="submit" value="Submit">' +
'</form>'
)
}
if (req.url === "/upload" && req.method.toLowerCase() === "post") {
// 上传接口
parseFile(req, res)
}
})
function parseFile(req, res) {
req.setEncoding("binary");
let body = ""; // 文件数据
let fileName = ""; // 文件名
// 边界字符串
let boundary = req.headers['content-type']
.split('; ')[1]
.replace("boundary=", "")
req.on("data", function(chunk) {
body += chunk;
});
req.on("end", function() {
// 字符串转化为对象
const file = querystring.parse(body, "\r\n", ":");
// 只处理图片文件;
if (file["Content-Type"].indexOf("image") !== -1) {
//获取文件名
var fileInfo = file["Content-Disposition"].split("; ");
for (value in fileInfo) {
if (fileInfo[value].indexOf("filename=") != -1) {
fileName = fileInfo[value].substring(10, fileInfo[value].length - 1);
if (fileName.indexOf("\\") != -1) {
fileName = fileName.substring(fileName.lastIndexOf("\\") + 1);
}
fileName = reconvert(fileName); // unicode转中文
console.log("文件名: " + fileName);
}
}
// 获取图片类型(如:image/gif 或 image/png))
const entireData = body.toString();
const contentTypeRegex = /Content-Type: image\/.*/;
contentType = file["Content-Type"].substring(1);
//获取文件二进制数据开始位置,即contentType的结尾
const upperBoundary = entireData.indexOf(contentType) + contentType.length;
const shorterData = entireData.substring(upperBoundary);
// 替换开始位置的空格
const binaryDataAlmost = shorterData
.replace(/^\s\s*/, "")
.replace(/\s\s*$/, "");
// 去除数据末尾的额外数据,即: "--"+ boundary + "--"
const binaryData = binaryDataAlmost.substring(
0,
binaryDataAlmost.indexOf("--" + boundary + "--")
);
// console.log("binaryData", binaryData);
const bufferData = new Buffer.from(binaryData, "binary");
console.log("bufferData", bufferData);
// fs.writeFile(fileName, binaryData, "binary", function(err) {
// res.end("sucess");
// });
fs.writeFile(fileName, bufferData, function(err) {
res.end("sucess");
});
} else {
res.end("reupload");
}
})
}
/**
* @description unicode转中文
* @param {String} str
*/
function reconvert(str){
str = str.replace(/(\\u)(\w{1,4})/gi,function($0){
return (String.fromCharCode(parseInt((escape($0).replace(/(%5Cu)(\w{1,4})/g,"$2")),16)));
});
str = str.replace(/(&#x)(\w{1,4});/gi,function($0){
return String.fromCharCode(parseInt(escape($0).replace(/(%26%23x)(\w{1,4})(%3B)/g,"$2"),16));
});
str = str.replace(/(&#)(\d{1,6});/gi,function($0){
return String.fromCharCode(parseInt(escape($0).replace(/(%26%23)(\d{1,6})(%3B)/g,"$2")));
});
return str;
}
server.listen(8888)
console.log('server is listening on 8888')
使用中间件解析表单二进制数据流实现上传
有没有觉得原生node
实现文件上传很费劲,需要自己去解析表单二进制数据流,有没有已经封装好的解析表单数据的库呢?答案肯定是有的,常见的解析表单数据的库有formidable、multer
。以formidable
为例来试试看:
npm
上对formidable
的描述如下:
A node.js module for parsing form data, especially file uploads.
const fs = require('fs')
const http = require('http')
const formidable = require('formidable')
const server = http.createServer((req, res) => {
/**
* @description unicode转中文
* @param {String} str
*/
function reconvert(str){
str = str.replace(/(\\u)(\w{1,4})/gi,function($0){
return (String.fromCharCode(parseInt((escape($0).replace(/(%5Cu)(\w{1,4})/g,"$2")),16)));
});
str = str.replace(/(&#x)(\w{1,4});/gi,function($0){
return String.fromCharCode(parseInt(escape($0).replace(/(%26%23x)(\w{1,4})(%3B)/g,"$2"),16));
});
str = str.replace(/(&#)(\d{1,6});/gi,function($0){
return String.fromCharCode(parseInt(escape($0).replace(/(%26%23)(\d{1,6})(%3B)/g,"$2")));
});
return str;
}
if (req.url === "/upload" && req.method.toLowerCase() === "post") {
var form = new formidable.IncomingForm();
// 指定解析规则
form.encoding = 'utf-8'; // 设置编码
form.uploadDir = 'public/upload'; // 指定上传目录
form.keepExtensions = true; // 保留文件后缀
form.maxFieldsSize = 2 * 1024 * 1024; // 指定上传文件大小
form.parse(req, (err, fields, files) => {
// fields表单字段对象,files文件对象
if(err) throw err;
// 重命名文件,将unicode编码转化为中文
var oldPath = files.upload.path;
var newPath = oldPath.substring(0, oldPath.lastIndexOf('\\')) + '\\' + reconvert(files.upload.name);
fs.rename(oldPath, newPath, err => {
if(err) throw err;
res.writeHead(200, {"Content-Type": "text/html;charset=UTF8"});
res.end('上传成功!');
})
})
return;
}
res.writeHead(200, {
'content-type': 'text/html'
})
res.end(
'<form action="/upload" enctype="multipart/form-data" method="post">'+
'<input type="text" name="title"><br>'+
'<input type="file" name="upload"><br>'+
'<input type="submit" value="Upload">'+
'</form>'
)
})
server.listen(8889)
console.log('server is listening 8889')
无论是原生node
事先文件上传还是使用中间件formidable
实现文件上传,使用utf-8
编码表单二进制数据时中文文件名变成类似于'身份证反面.jpg'
,中文被编码成Unicode
,这就需要对文件进行重命名。
大家可以参考文章:字符编码-ASCII,Unicode 和 UTF-8
Java后端接收文件对象方式
当前端以multipart/form-data
方式上传二进制文件时,tomcat
会通过request.getInputStream().read()
读取请求数据流,然后放入到paramHashValues
当中。后端可以通过((MultipartHttpServletRequest) request).getFiles("file")
来获取上传文件对象,至于其他的表单参数,通过((MultipartHttpServletRequest) request).getParameter(name)
来获取;或者使用springMVC
中注解@RequestParam
和MultipartFile
来接收
参考文献:
buffer概述:
http://www.runoob.com/nodejs/nodejs-buffer.html
https://nodejs.org/api/buffer.html#buffer\_buffer
buffer数据显示:
https://segmentfault.com/q/1010000009002065
stream运行机制:
https://www.php.cn/js-tutorial-412138.html
stream的理解:
https://www.jianshu.com/p/4eb9077a8956
原生node实现文件上传:
https://juejin.im/post/5d84ab33e51d4561b072ddd0#heading-4
formidable使用:
https://segmentfault.com/a/1190000004057022
https://github.com/node-formidable/node-formidable
https://segmentfault.com/a/1190000011424511
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。