基于node实现文件上传和下载

本文主要讲述基于node如何实现文件上传和下载,分成原生node实现版、中间件实现版。

1、文件上传为什么需要使用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字符的文本,因此提出了一个新的MIME类型multipart/form-data,有效地将与填写的表单相关联的值从客户端发送到服务器。想了解很多MIME类型,点击查看

2、文件对象介绍




  
  
  
  Document
  


  

基于node实现文件上传和下载_第1张图片

文件(File)接口提供有关文件的信息,并允许网页中的 JavaScript 访问其内容。

File 对象是特殊类型的 Blob,且可以用在任意的 Blob 类型的 context 中。比如说, FileReader, URL.createObjectURL(), createImageBitmap(), 及 XMLHttpRequest.send() 都能处理 Blob 和 File。

另外还有一个blob对象,也附上一张chrome浏览器的截图。blob,二进制大文档存储

基于node实现文件上传和下载_第2张图片

两个对象里面都有size和type,按照官方的文档,file集成了blob,而且可以使用slice方法.

slice方法可以在当前的blob数据中,取出一段数据,作为新的blob。常用的就是文件断点上传。

3、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);

输出的结果为:

  

不是说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 进制数字。
总之,这样转化的目的是方便显示和查看。

stream
Stream是一个抽象接口,Node中有很多对象实现了这个接口。例如,对http服务器发起请求的request对象和服务端响应对象response就是Stream,还有stdout(标准输出)。

你可以把流理解成一种传输的能力。通过流,可以以平缓的方式,无副作用的将数据传输到目的地。Stream表示的是一种传输能力,Buffer是传输内容的载体 (可以这样理解,Stream:外卖小哥哥, Buffer:你的外卖)

在node中流无处不在:
基于node实现文件上传和下载_第3张图片

流为什么这么好用还这么重要呢?

现在有个需求,我们要向客户端传输一个大文件。每次接收一个请求,就要把这个大文件读入内存,然后再传输给客户端。

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;
//指定文件路径,将文件转化为可读流

4、原生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(
      '
' + 'First name:
' + '' + '
' + 'Last name:
' + '' + '
' + '' + '
' + '' + '
' ) } 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--

基于node实现文件上传和下载_第4张图片
可以看到客户端传到服务端的数据被编码成二进制,而且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(
      '
' + '' + '
' + '' + '
' ) } 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')

5、使用中间件解析表单二进制数据流实现上传

有没有觉得原生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(
    '
'+ '
'+ '
'+ ''+ '
' ) }) server.listen(8889) console.log('server is listening 8889')

无论是原生node事先文件上传还是使用中间件formidable实现文件上传,使用utf-8编码表单二进制数据时中文文件名变成类似于'身份证反面.jpg',中文被编码成Unicode,这就需要对文件进行重命名。

大家可以参考文章:字符编码-ASCII,Unicode 和 UTF-8

6、大文件上传、多文件上传、断点上传

7、总结

本文主要讲述了文件上传过程中表单编码为什么要采用multipart/form-data?服务端如何处理二进制数据流(buffer、stream)?原生node和中间件如何实现上传?有时间在了解一下大文件如何断点上传还有多文件上传。

参考文献:
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

你可能感兴趣的:(上传文件,node.js)