揭秘前端文件上传原理(二)

上一篇文章讲到了以Form表单,将文件数据编码为特定的类型,来作为前端文件上传的载体,这一篇再来看看,如果不使用Form表单,不以FormData去提交数据,我们又将如何上传文件到云端呢?

Form表单的意义

首先来想一想,Form表单对文件上传的文件内容做了什么,它格式化了文件内容,在请求时以特定的格式发送了数据至服务器,像下面的格式这样。

------WebKitFormBoundary7YGEQ1Wf4VuKd0cE
Content-Disposition: form-data; name="file"; filename="index.html"
Content-Type: text/html
...
文件内容
...
------WebKitFormBoundary7YGEQ1Wf4VuKd0cE--

再思考一下,这样的格式化的目的又是什么?先看看格式化后的内容,它包含了一个文件的全部信息,如格式,文件名,文件内容均已特定的字段或者位置出现,所以格式化的目的就是在制定一种规范,一种约定俗成的规范,无论哪一个项目或是那一个网站它的文件上传如果选择Form表单编码类型,它均是一种输出。

再想想为什么要制定这种规范呢?它带来了什么?举个生活中的例子,就比如苹果的充电头和安卓的充电头,而且安卓的充电头还分了Type-C和普通的,这些繁杂不一的充电头带给了我们什么呢?其实是不便,有时候就是想充个电,可是身边的同学没有一个和我一样的充电头。

再来看文件传输的规范,如果我们面对的后端是世界上最好的语言PHP提供的,他需要一直别人不能企及的方式处理上传文件,而后来世界上无所不能的JavaScript服务端Nodejs出现后,它需要与PHP不一致的方式处理上传文件,这时候你还要寻求另一种规范来解决这个问题,当新的规范解决了这个问题,你还有最“短小精悍”的Python,“太子爷”Go语言等等一系列,如果每人一种规范,其实带来的还是不便,所以规范代表即是一种约束,还是一种最佳实践,大家都趟过的坑,你再去趟坑肯定少一些。

上面解释了规范的必要性,其实也说明了另一个方面,From表单是一种规范,我就不遵守规范可以吗?当然可以,不遵守规范即代表你用了新的规范,或者说不叫规范,而是一种前后端都认可的方式,只要你的后端支持就好。


无Form表单的文件上传

接下来看看没有Form这种规范,又该如何上传文件。前面已经说清楚了,文件上传的实质是上传文件的内容以及文件的格式,当我们使用HTML提供的Input上传文件的时候,它将文件的内容读进内存里,那我们直接将内存里的数据当成普通的数据提交到服务端可以么?看下面的例子。

<!-- 前端代码:-->
<div>
  <input id="file" type="file" />
  <input type="button" value="文件上传" onclick="uploadFile()" />
</div>
<script>
  function uploadFile() {
    const file = document.getElementById('file').files[0];
    const xhr = new XMLHttpRequest();
    xhr.open('POST', `http://127.0.0.1:8000/upload?name=${file.name}`, true);
    xhr.onreadystatechange = function() {
      if (xhr.readyState == 4 && xhr.status == 200) {
        alert(xhr.responseText);
      }
      }
    xhr.send(file);
  }
</script>

先分析一下代码,先使用Input的type为file类型建立一块文件上传区域,页面上绑定一个uploadFile执行的click事件,uploadFile里先获取了上传区域的文件内容,然后构建Ajax直接提交数据,很简单,文件就被上传到服务器上了,当然前提是 http://127.0.0.1:8000/upload 这个API知道你传的是什么?然后再去解析存储。

相信上面这种方式,很多人对这个file变量到底是什么还是比较含糊的,接下里看看这个file是个啥。

这里我先没上传任何内容打印了一下file变量,是undefined,然后我上传了一张图片,再次打印后file变量是一个File函数构造出的对象了,它里面有文件的一些简略信息,如大小size,文件名name以及文件格式type等,而且文件内容也在这个对象里,只不过以ArrayBuffer的方式在文件的原型链上体现,看看下面对于File对象的操作。

上面这些数字其实就是文件的内容,大家都知道数据是0,1组成的世界,而ArrayBuffer则是更多的数字来体现的数据世界,它和二进制的目的是一样的,它被用来表示通用的、固定长度的原始二进制数据缓冲区。说到这里则必须要提起一个新的概念,浏览器的提供的Blob接口。

Blob对象

Blob 对象表示一个不可变、原始数据的类文件对象。上面的file变量的构造函数File就是继承与基于Blob,继承了 blob 的功能并将其扩展使其支持用户系统上的文件。看看下面的Blob与File的示例。

上面我先打印了一下file与浏览器提供的构造函数File和Blob的关系,然后自行构建了自定义的myfile对象和myblob的对象,看得出自行构建的File对象下会多出一些文件相关的属性,而Blob对象则只是基本的size与type属性。当打印arrayBuffer函数的返回值时发现其内容也是完全一致的。

其实说到这里很多人对于Blob是个啥还是一知半解的,简单理解一下,它的构造结果是一块内存区,这块内存区以特定的格式存储我们所要上传的文件二进制数据,当我们上传文件时上传这块内存区里的数据即可。

服务端的解析文件数据

再回到之前的上传,上传文件时,其实是上传了File构造出的对象,这个对象继承于Blob,它的内部是存储了我们所要上传的文件内容数据。前面已经提到要上传成功还要看看后端是不是支持,接下来再从后端的角度看看,以这样非Form表单的形式上传的文件,后端接收到的又是什么数据。

// Nodejs服务端代码
...
if(reqUrl.pathname ==='/upload' && method === 'POST') {
  const fileName = qs.parse(reqUrl.query).name;
  req.pipe(fileStream(fileName));
  req.on('end', () => {
    res.writeHead(200, {  'Content-Type': 'text/html; charset=utf-8' });
    res.end('上传完成');
  })
}
...

这里的req.pipe其实和req.on('data', ()=> { }) 监听客户端数据然后组装完成,写进文件是一个效果,最终都是将客户端来的数据写入到了fileName的文件中,接下来看看客户端上传之后和上传前的文件有何异同。接下来执行,前端上传点击,这里就不演示了,然后打开服务端的file文件夹看看上传的内容你会发现,这TM不就是我上传的文件吗,简直一毛一样,没错,就是你上传的文件,而且不用解析。


这一篇内容写到这儿,简单总结一下,本文里面从解释Form表单规范的意义到脱离FormData规范上传文件,其中还认识浏览器提供的File和Blob两个API,并且做了简单介绍,最终完成了无Form表单编码的文件传输。

我不知道上面的介绍给大家带来比较震撼的是什么,我自己的感受是HTML5真是越来越强大了,有些牛逼闪闪的点被人忽略了而已。

接下来再提出一个问题引出下一节,当你的后端需要的数据不是ArrayBuffer这种二进制数据,而是Base64的编码数据呢?那又该怎么传输呢?或者你上传的文件要做上传进度条又要怎么去做呢?请期待下一节。


References

[1] http://javascript.ruanyifeng.com/htmlapi/file.html#toc0 [2] https://developer.mozilla.org/zh-CN/docs/Web/API/

原文发布于微信公众号 - 全栈者(fullStackEngineer)

原文发表时间:2019-09-09

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券