最近基于网页做了一个图片处理小工具。用户上传源图片并提交需要进行的操作,后台接收该图片文件,处理完毕后返回下载文件流。因此,涉及到图片文件的前后端传递,踩了不少坑,在此记录和总结一下。
前端文件传至后端
方式一:Form + input 表单提交
<!-- 前端 --> |
触发form表单提交数据的方式有2种,一种是在页面上点击提交按钮触发,第二种是在js中执行form.submit()方法。
原理:读取 http body 部分,根据 boundary 分析出分隔符特征(这个串是唯一的,不会与body内其他数据冲突)。根据实际分隔符分段获取 body 内容后,遍历分段内容。根据 Content-Disposition 特征获取其中值。根据值中 filename 或 name 区分是否是包含二进制流还是表单数据的 k-v。根据 filename 获取原始文件名。从连续 两个 newline 字符串为起始至当前分段完毕,按照二进制流读取上传文件流信息。
后端可收到:
- 原始文件名信息
- 原始文件类型信息
- 全部文件流信息
特点:使用简单方便,兼容性好,基本所有浏览器都支持,提交后页面会跳转。
注意:若一个form表单有多个提交按钮,后端可以用每个button不同的name值区分,如下
if 'update' in request.POST: |
方式二:使用FormData提交
用js构造form表单的数据,简单高效,但最低只兼容IE10,所以需要兼容IE9的童鞋们就略过这个方法吧。
<input type='file'> |
var formData = new FormData(); |
优点:由于这种方式是ajax上传,可以准确知道什么时候上传完成,也可以方便地接收到回调数据。
缺点:兼容性差。
方式三:使用FileReader读取文件数据提交
HTML5的新api,兼容性也不是特别好,只兼容到了IE10。
<input type='file'> |
var fr = new FileReader(); |
上面获得的data可以用来实现图片上传前的本地预览,也可以用来发送base64数据给后端然后返回该数据块对应的地址。
优点: 同第二种
缺点:一次性发送大量的base64数据会导致浏览器卡顿,服务器端接收这样的数据可能也会出现问题。
后端文件传到前端下载
方式一:Form表单返回
返回HttpResponse对象
response = HttpResponse(new_file) |
方式二:请求文件下载接口的地址
<a href="后端文件下载接口地址" >下载文件</a> |
本质也是返回HttpResponse对象
方法三:利用原生的XMLHttpRequest方法实现
function request () { |
方法四:利用原生的fetch方法实现
function request() { |
小结:
方法一和二适用于服务器端的本地文件下载
方法三和四适用于POST请求、blob对象
Http文件传输
HTTP协议用于文件传输时,一般把文件内容放到消息体中。作为TCP之上的流式传输协议,发送端和接收端可以对大文件进行流式的发送和接收。
① 确定大小的文件传输
消息头部的Content-Length字段表示文件的长度,用于接收端确定文件的结束。
② Chunked编码
当文件大小无法事先确定时,无法设置Content-Length字段。此时可以用分块传输的方式,将文件分成多个部分进行发送。在分块发送方式下,头部增加Transfer-Encoding: chunked,存在这个头部时不允许再加上Content-Length头,即使有也会被忽略。
Chunked模式下,消息体分块发送,每一块头部存储数据长度,跟上CRLF,然后是具体的数据,块与块之间也是CRLF分隔。当长度头为0时,表示块的结束。
③ 使用multipart/form-data上传文件
原始的POST请求消息体中是URL编码后的表单,格式为key=value,不同的key、value之间用&分隔。上传二进制的文件时,可以用multipart/form-data的方式。
在这种方式下,基础的请求仍然是POST请求,文件内容放在消息体,只是Content-Type字段的值为multipart/form-data,并随机选择一个字符串作为分隔符(理论上需要这个分隔符不在文件内容中出现,一般随机选择的字符串出现在正文中的概率非常小,如果真的出现会导致POST失败,需要另外发起一次请求重新选择随机字符串),然后,每个字段之间用”—分隔符”进行分隔,最后一个”—分隔符—”表示结束。每个字段中都可以包含头部和消息体,头部的内容可以包含文件名称、文件路径等,也可以是文件的二进制内容本身。
④ 断点续传与多线程传输
这里仍然是运用分块传输的思想:如果传输中途中断,接下来可以从中断的地方重新开始避免从头开始的浪费;在多线程程序中,各个线程可以分别负责传输一个文件块,然后将他们合并恢复成为原始文件。
分块传输就需要确定块的边界,这里采用的是Range字段,表示从某一字节开始,如Range:bytes=100-,表示请求的是从文件的100字节开始到文件末尾,返回消息为206 Partial Content,头部字段增加Content-Range: bytes100-199/200,表示返回文件100-199字节内容,文件一共200字节。
多线程传输时,每个线程请求文件中不同的range,传输完成后由应用作合并。
踩坑之处
① href无法从前端传递文件流参数至后端。
② ajax无法回调文件流。
ajax的返回值类型是json,text,html,xml类型,或者可以说ajax的发送,接受都只能是string字符串,不能流类型,所以无法实现文件下载,强用会出现response冲突。
如果非要使用ajax的话,只能通过返回值得到生成的文件相关url。然后在回调函数里通过创建一个iframe,并设置其src值为文件url,或者一个对文件生成流的处理url,这样操作来实现文件下载且页面无刷新。
总结
我最后采用的方法是Form表单提交+返回文件流下载,三个提交按钮对应三种图片处理操作,后端用name判断用户点击的是哪一个按钮。
以上涉及后端的操作均以Django为例。