专栏首页全栈者揭秘前端文件上传原理(一)

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

在平时工作中,总是会接触过很多文件上传的功能,因为用惯了各种操作库来处理,所以总有一种云里雾里的感觉,没有清晰的思路,归根到底还是没有理解文件上传的原理。接下来将揭起工具库的面纱,看看文件上传到底是怎么一回事,深入了解文件上传的本质。

先解释一下文件上传这个过程是怎么一回事。因为浏览器本身的限制,浏览器是不能直接操作文件系统的,需要通过浏览器所暴露出来的统一接口,由用户主动授权发起来访问文件动作,然后读取文件内容进指定内存里,最后执行提交请求操作,将内存里的文件内容数据上传到服务端,最后服务端解析前端传来的数据信息后存入文件里。

最简单的上传实现

这里利用form表单标签和类型为file的Input标签来完成上传,要将表单数据编码格式置为 multipart/form-data 类型,这个编码类型会对文件内容在上传时进行处理,以便服务端处理程序解析文件类型与内容,完成上传操作。

<form method="POST" enctype="multipart/form-data">
  <input type="file" name="file" value="请选择文件"><br />
  <input type="submit">
</form>

如果单单对于前端来讲,上面的代码就够了,但是为了了解上传的本质,这里从全栈的角度来看看文件上传。先以 Nodejs作为服务端,提供一个上传接口给前端,来看看上面的前端代码与后端是怎么传递文件数据的。

...
//上传接口逻辑
if(url ==='/upload' && method === 'POST') {
  // 定义一个缓存区
  const arr = []
  req.on('data', buffer => {
    // 将前端传来的数据进行存储进缓存区
    arr.push(buffer);
  })
  
  req.on('end', () => {
    // 前端请求结束后进行数据解析 处理
    const buffer = Buffer.concat(arr);
    // 将数据变成string类型
    const content = buffer.toString();
    // 从传来的数存进test的文件里
    fileStream('test').write(buffer);
    // 返回前端请求完成
    res.writeHead(200, {  'Content-Type': 'text/html; charset=utf-8' });
    res.end('上传完成');
  })
}
...

这里的服务端代码先将前端上传的数据内容毫不处理直接写入一个名为test的文件内,以便我们查看前端到底传来了什么样的数据。

在前端发起一次上传操作请求,获取部分请求头信息。

再来看看从前端传来的被上传到服务端的文件数据。

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

<html>
  <head>
    <title>上传文件</title>
  </head>
  <body>
    <form method="POST" enctype="multipart/form-data">
      <input type="file" name="file" value="请选择文件"><br />
      <input type="submit">
    </form>
  </body>
</html>
------WebKitFormBoundary7YGEQ1Wf4VuKd0cE--

从上面被上传到服务端的数据可以看出相比于客户端本地的文件中多了几行内容,先是第一行和最后一行的WebKitFormBoundary 码,第二行的ContentDisposition,该行包含一些文件基本信息,还有第三行文件内容类型,所以后端如果要获取到正确的文件内容则需要自己去除由浏览器在上传时所添加的进来的几行内容,而保留有效文件内容后进行写文件操作,完成上传目的。

从上面的最简单的实现中可以看出以下几个点 。

  1. 前端文件上传实际是文件内容的传递,是数据的传递,并非我们最常使用的文件拷贝与复制操作。
  2. 传递过程中要进行编码来制定发送的文件数据规则,以便于后端能够实现一套对应的解析规则。
  3. 传递的数据规则里包含所传递文件的基本信息 ,如文件名与文件类型,以便后端写出正确格式的文件。

上面的代码可以在这里查看,有兴趣的同学可自行调试。

https://github.com/FantasyGao/Practice-book/tree/master/upload/upload1

最常用的上传实现

上面利用了form表单的能力来上传本地文件,但是由于form表单提交操作网页会造成整体刷新,所以一般比较少用,而是利用熟悉的异步请求操作AJAX来完成上传动作,而一个新的问题出现了,不使用form表单,那文件编码该怎么处理呢?接下来看看下面的代码。

<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', true);
  xhr.onreadystatechange = function() {
    if (xhr.readyState == 4 && xhr.status == 200) {
      alert(xhr.responseText);
    }
  };
  xhr.send(file);
}
</script>

上面的相比于Form表单的提交,使用了浏览器的XMLHttpRequest自定义的提交方式,也就是俗称的AJAX技术。但是使用这种提交方式没有设置编码 enctype="multipart/form-data" 类型,如果直接将文件内容上传,会导致后端在解析Form表单上传的文件时与Ajax上传的不一致,所以为了后端能够使用相同的代码就能解析前端这两种提交方式,所以前端需要自行格式化文件内容。在格式化的过程中,则需要通过浏览器自身提供的FormData构造函数来实例化的一个文件fd,然后使用实例的append方法将文件内容插入进去,最后利用XMLHttpRequest的实例做出发送动作。所以最终上传部分应为如下代码:

function uploadFile() {
  const file = document.getElementById('file').files[0];
  const xhr = new XMLHttpRequest();
  const fd = new FormData();
  fd.append('file', file);
  xhr.open('POST', 'http://127.0.0.1:8000/upload', true);
  xhr.onreadystatechange = function() {
    if (xhr.readyState == 4 && xhr.status == 200) {
      alert(xhr.responseText);
    }
  };
  xhr.send(fd);
}

从前端发起一次请求观察请求信息,可以看出已经成功的变成了FormData的编码类型。

在后端也收到的数据和上面Form表单一样的内容,为了能够真正的体验上传过程,接下来我们在服务端做一个解析器来解出正在的文件内容,并且写进文件里,完成上传目标。

先看看上传的文件内容,它的特点前面已经描述过了,其实在真正的文件内容外多了几行文件信息,所以我们解析器的目的就是去掉这几行内容,并且在这几行简要信息里摘出文件名,以便写文件。

实现思路:将前端传来的文件按行分成数组,数组的第一个第二第三个和最后一个元素删除,并且在第二个元素里匹配出文件名。代码如下:

/**
 * @step1 过滤第一行
 * @step2 过滤最后一行
 * @step3 过滤最先出现Content-Disposition的一行
 * @step4 过滤最先出现Content-Type:的一行
 */
const decodeContent = content => {
  let lines = content.split('\n');
  const findFlagNo = (arr, flag) => arr.findIndex(o => o.includes(flag));
  // 查找 ----- Content-Disposition Content-Type 位置并且删除
  const startNo = findFlagNo(lines, '------');
  lines.splice(startNo, 1);
  const ContentDispositionNo = findFlagNo(lines, 'Content-Disposition');
  lines.splice(ContentDispositionNo, 1);
  const ContentTypeNo = findFlagNo(lines, 'Content-Type');
  lines.splice(ContentTypeNo, 1);
  // 最后的 ----- 要在数组末往前找
  const endNo = lines.length - findFlagNo(lines.reverse(), '------') - 1;
  // 先反转回来
  lines.reverse().splice(endNo, 1);
  return Buffer.from(lines.join('\n'));
}

一个简单的解析器完成了,一般情况下你所使用的框架会解决解码这一部分问题,无论是Nodejs或是Java,他们的本质都是摘出有效的文件内容然后写进新文件里,从而达到文件上传的目的。

最终的服务端代码如下:

if(url ==='/upload' && method === 'POST') {
    //文件类型
    const arr = []
    req.on('data', (buffer) => {
      arr.push(buffer);
    })
    req.on('end', () => {
      const buffer = Buffer.concat(arr);
      const content = buffer.toString();
      const result = decodeContent(content);
      const fileName = content.match(/(?<=filename=").*?(?=")/)[0];
      fileStream(fileName).write(result);
      res.writeHead(200, {  'Content-Type': 'text/html; charset=utf-8' });
      res.end('上传完成')
    })
  }

通过上面的代码,便可以完成一个以FormData类型提交的文件上传的操作了,代码在下面欢迎下载体验。

https://github.com/FantasyGao/Practice-book/tree/master/upload/upload2

上面先从最基础的角度去实现了文件上传,然后从最常用的角度了解了文件上传以及后端对于前端上传的Form表单类型的文件所解析要经过的步骤,那除了以Form表单类型,前端还有没有其他方式上传呢?当然是有的,如现在比较常用的Blob数据方式,它又是怎么做的呢?还有在需要上传的文件体积太大的时候,需要将内容切片成一个一个小块的来上传又是怎么实现呢?下一节将来梳理和讲解这些内容,敬请期待。


如上内容均为自己总结,难免会有错误或者认识偏差,如有问题,希望大家留言指正,以免误人,若有什么问题请留言,会尽力回答之。如果对你有帮助不要忘了分享给你的朋友或者点击右下方的“在看”哦!也可以关注作者,查看历史文章并且关注最新动态,助你早日成为一名全栈工程师!

本文分享自微信公众号 - 全栈者(fullStackEngineer),作者:TingRongGao

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-09-06

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

我来说两句

0 条评论
登录 后参与评论

相关文章

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

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

    用户1462769
  • 前端开发---异步上传文件

    有一个名为ajaxFileUpload的JQuery插件可以利用iframe来实现前端页面中异步上传文件。

    MiaoGIS
  • 前端如何分片上传文件?

    分片上传好处:可以断点续传,针对较大文件传输有明显好处,以免中途传输中断还需从头开始,借助哈希算法计算每片文件的哈希值,最后计算单个文件的哈希值。

    城市中的游牧民族
  • 文件上传的原理及条件

    通过伪装成正常文件上传,并获得合法的格式通过后实现后端server的执行 前端:js 后端:动态语解析[php、.net、asp、JSP] 攻击者可以上传...

    行云博客
  • 前端限制上传文件的类型

    今天在工作中遇到一个需求,需要在上传文件的时候限制上传文件的类型,比如上传图片的就只能上传图片类型的文件。 现将自己在开发中的代码放到我的博客里,以备在以后的开...

    用户1187932
  • 前端限制上传文件的类型

      今天在工作中遇到一个需求,需要在上传文件的时候限制上传文件的类型,比如上传图片的就只能上传图片类型的文件。 现将自己在开发中的代码放到我的博客里,以备在以后...

    用户1174387
  • 前端本地文件操作与上传

    前端无法像原生APP一样直接操作本地文件,否则的话打开个网页就能把用户电脑上的文件偷光了,所以需要通过用户触发,用户可通过以下三种方式操作触发:

    IT派
  • 前端 文件夹上传 解决方案

      今天在改功能的时候,居然有一个批量挂接电子文件的这样的一个功能,前端要求选择文件夹?

    彼岸舞
  • Dart 服务端开发 文件上传 原

    南郭先生
  • 聊一聊前端上传大文件的几种方式。

    使用PHP来展示常规的表单上传是一个不错的选择。首先构建文件上传的表单,并指定表单的提交内容类型为enctype="multipart/form-data",表...

    用户6835371
  • 文件切片上传原理解析

    前端上传文件时如果文件很大,上传时会出现各种问题,比如连接超时了,网断了,都会导致上传失败。

    挥刀北上
  • 文件上传踩坑记及文件清理原理探究

    最近搞一个文件上传功能,由于文件太大,或者说其中包含了比较多的内容,需要大量逻辑处理。为了优化用户体验,自然想到使用异步来做这件事。也就是说,用户上传完文件后,...

    烂猪皮
  • 文件上传的最佳前端体验做法

    网页开发者们想了很多办法,试图提升文件上传的功能和操作体验,在各种Javascript库的基础上,开发了五花八门的插件。可是,由于不同浏览器之间的差异,缺乏统一...

    javascript.shop
  • Hadoop之HDFS02【上传下载文件原理】

    原理步骤:   客户端要向HDFS写数据,首先要跟namenode通信以确认可以写文件并获得接收文件block的datanode,然后,客户端按顺序将文件逐个...

    用户4919348
  • 精选腾讯技术干货200+篇,云加社区全年沙龙PPT免费下载!

    “看一看”推荐模型揭秘!微信团队提出实时Look-alike算法,解决推荐系统多样性问题;

    风间琉璃
  • 如何开发一款堪比APP的微信小程序(腾讯内部团队分享)

    一夜之间,微信小程序刷爆了行业网站和朋友圈,小程序真的能如张小龙所说让用户“即用即走”吗?其功能能和动辄几十兆安装文件的APP相比吗?开发小程序,是不是意味着...

    顶级程序员
  • 一文带你揭开Redis复制原理的神秘面纱

    墨墨导读:本文在依托Redis主从环境下,针对访问的数据一致性进行分析,解开Redis复制原理的神秘面纱。‍

    数据和云
  • 前端上传文件到腾讯云(对象存储)

    好吧,没写之前简单的说一下为什么要写,我还是怀着比较沉重的心情写的这篇教程,主要是心里没底,不知道能写明白不,不过既然提笔了,那就硬着头皮写吧,没办法,毕竟跌跌...

    何处锦绣不灰堆
  • 补习系列(11)-springboot 文件上传原理

    RFC1867 定义了HTML表单文件上传的处理机制。 通常一个文件上传的请求内容格式如下:

    美码师

扫码关注云+社区

领取腾讯云代金券