12

前端上传方式

  • 使用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>

image.png

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

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

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

两个对象里面都有sizetype,按照官方的文档,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 堆内存之外的一块原始内存(bufferC++ 层面分配的,所得内存不在 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 进制数字。
总之,这样转化的目的是方便显示和查看。

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

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

在node中流无处不在:
image

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

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

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--

微信截图_20191012142415.png
可以看到客户端传到服务端的数据被编码成二进制,在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编码表单二进制数据时中文文件名变成类似于'&#36523;&#20221;&#35777;&#21453;&#38754;.jpg',中文被编码成Unicode,这就需要对文件进行重命名。

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

Java后端接收文件对象方式

当前端以multipart/form-data方式上传二进制文件时,tomcat会通过request.getInputStream().read()读取请求数据流,然后放入到paramHashValues当中。后端可以通过((MultipartHttpServletRequest) request).getFiles("file")来获取上传文件对象,至于其他的表单参数,通过((MultipartHttpServletRequest) request).getParameter(name)来获取;或者使用springMVC中注解@RequestParamMultipartFile来接收
image.png

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


记得要微笑
1.9k 声望4.5k 粉丝

知不足而奋进,望远山而前行,卯足劲,不减热爱。


« 上一篇
MIME类型介绍
下一篇 »
URI编码