简单理解一下前端文件的上传与下载,如有错误,轻喷。
http小知识
http协议是建立在tcp、ip协议的应用层规范。
http协议规范把http请求分为3个部分:状态行,请求头,请求体。所有的方法、实现,都是围绕 “如何运用和组织这三部分” 来完成的。
http里没有专门用于文件上传的请求方式,文件上传请求是在post请求基础之上定义出来的一种方式。
http协议规定 POST 请求提交的数据,必须放在消息主体(entity-body)中,但协议并没有规定消息主体的编码方式 。开发者可以自己决定消息主体的编码格式,只要最后发送的 HTTP 请求,满足上面的格式就可以。
数据发送出去,还要服务端解析成功才有意义。一般服务端语言如 php、python 等,以及它们的 framework,都内置了自动解析常见数据格式的功能。服务端通常是根据请求头(headers)中的 Content-Type 字段来获知请求中的消息主体是用何种方式编码,再对主体进行解析。
http全名超文本传输协议(Hyper Text Transfer Protocol,HTTP),顾名思义:用来传输文本的协议,那么当我们产生的传输文件的需求时,就显得“捉襟见肘”。
在发送表单时,输入的都是文本信息,但是很多人不理解,输出文件时,文件的信息根本没有我们想要的数据,那到底怎么才能传输?
在原生的html中,我们通过form表单进行提交。而当我们需要对表单数据进行二次处理再进行发送时,就需要通过JavaScript脚本进行发送,那么这时候FormData对象就派上了用场。
而实际上,通过FormData对象提交表单仅仅是form表单提交的一种方式,这与通过设置form标签的enctype进行提交是一样的效果。
FormData允许以二进制binary的方式传递文件,那么,既然是二进制数据,自然可以通过http进行传输。
客户端提交文件
通过form标签进行提交
这是最原始的提交方式。但是要注意,http请求头中有一个Content-Type属性,该属性指定数据以何种形式进行编码传递,默认是“application/x-www-form-urlencoded”,以键值对的形式进行传递:key1=val1&key2=val2。
如果我们默认以“application/x-www-form-urlencoded”作为请求头,那么显然文件不可能变成其中的value进行传递。
我们可以通过设置form标签的“enctype”属性来指定“content-type”,该属性取值如下:
- application/x-www-form-urlencoded: 初始的默认值
- multipart/form-data: 适用于使用 标签上传文件
- text/plain: HTML5 引入的类型
这是简单请求的三种请求头,当然现在还可以使用“application/json”。
给form标签添加“ enctype=“multipart/form-data””属性与属性值。
<form action="http://localhost:4000/upload/logo" method="post" target="_blank" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit" value="提交">
</form>
此时发送请求,请求标头中的“Content-Type”变成”“multipart/form-data; boundary=----WebKitFormBoundaryU9sq3PNe4bAowjQG“。
其中boundary是一个占位符,代表我们规定的分割符,可以自己任意规定,但为了避免和正常文本重复了,尽量要使用复杂一点的内容。
通过FormData提交
这可能是最常见的方式了。
通过FormData构造函数直接从form标签生成。
<form id="form" action="http://localhost:4000/upload/logo" method="post" target="_blank" enctype="multipart/form-data">
<input id="file" type="file" name="file">
<input id="button" type="button" value="提交">
</form>
<script>
function submitForm(e) {
// 通过表单直接转换
const fd = new FormData(document.querySelector('#form'));
const xhr = new XMLHttpRequest();
xhr.open('POST', 'http://localhost:4000/upload/logo', true);
xhr.send(fd);
}
document.querySelector('#button').addEventListener('click', submitForm)
</script>
服务端发送文件
当我们将文件发送至服务器之后,由服务端自行接收并保存。
当客户端需要下载文件时,服务端又得将保存的文件进行读取并返回。
这里通过nodejs的express框架进行文件的读取与返回。
app.get('/file', (req, res) => {
const file = fs.readFileSync('./upload/logo.jpg', )
res.setHeader('content-type','binary');
res.send(file)
})
app.get('/file1', (req, res) => {
res.sendFile(path.resolve(__dirname,'./upload/logo.jpg'))
})
这里res对象返回数据有多种方式,如果直接使用sendFile则不需要设置响应头,如果是使用send/end等方法需要手动设置响应头为“binary”表示二进制。
客户端接收文件
在axios响应拦截器中添加判断,如果返回data类型为“Blob”,则直接将二进制数据返回,注意此处根据具体项目配置。
发送请求
export async function queryFile(){
return await http.get('/file1',{
params: {
site_id: 4
},
responseType: 'blob'
})
}
注意此处的responseType被手动设置成“blob”,参考axios文档,该值为浏览器专属。
接收完毕开始下载文件,这里用到的技术点最多,也是最容易让人头疼的地方。
const blob = new Blob([res],{
type: 'image/jpeg'
});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'logo.jpg'
document.body.append(a);
a.click();
document.body.remove(a);
URL.revokeObjectURL(src);
首先说说Blob
Blob 对象表示一个二进制文件的数据内容,比如一个图片文件的内容就可以通过 Blob 对象读写。它通常用来读写文件,它的名字是 Binary Large Object (二进制大型对象)的缩写。
Blob构造函数接受两个参数。第一个参数是数组,成员是字符串或二进制对象,表示新生成的Blob实例对象的内容;第二个参数是可选的,是一个配置对象,目前只有一个属性type,它的值是一个字符串,表示数据的 MIME 类型,默认是空字符串。
第一个参数是一个由ArrayBuffer, ArrayBufferView, Blob, DOMString 等对象构成的 Array ,或者其他类似对象的混合体。
MIME参考
那么,此时使用Blob构造函数生成一个blob对象,用来表示二进制文件的数据内容,可通过该实例进行读写,一般我们不需要对文件进行读写,毕竟你也不会去修改图片的二进制信息。
那这个Blob对象到底有什么用?接着看下面。
再看看URL.createObjectURL
URL.createObjectURL() 静态方法会创建一个 DOMString,其中包含一个表示参数中给出的对象的 URL。
它接收一个参数,该参数可以是File对象、Blob对象或者MediaSource对象,返回一个用于指定源object的内容。
等等!Blob对象?这下就能理解,为什么要创建Blob对象了。因为要生成特定的URL用于下载。
最后需要注意的是,每次调用 createObjectURL() 方法时,都会创建一个新的 URL 对象,即使你已经用相同的对象作为参数创建过。当不再需要这些 URL 对象时,每个对象必须通过调用 URL.revokeObjectURL() 方法来释放。
这下,从发送文件到下载文件就连贯起来了,下次不用再去百度“前端怎么下载二进制文件流”啦。
其实,也不需要使用Blob
不知道有没有发现,在响应拦截器中的判断类型,是判断data是否是Blob对象,已经是Blob对象了?因为我们设置了responseType为blob,既然这样,那岂不是可以省略代码中new Blob这一步?
待考证实际情况,我这里是可以省略的
const url = URL.createObjectURL(data);
const a = document.createElement('a');
a.href = url;
a.download = 'logo.jpg'
document.body.append(a);
a.click();
document.body.remove(a);
URL.revokeObjectURL(src);
axios的responseType
如果仔细看看axios文档会发现,该属性有多种取值。
{
// ......
// `responseType` 表示浏览器将要响应的数据类型
// 选项包括: 'arraybuffer', 'document', 'json', 'text', 'stream'
// 浏览器专属:'blob'
responseType: 'json', // 默认值
}
其中有一个“arraybuffer”,那这个arraybuffer又是什么东西?
在上面服务端发送文件中,写到两种方式,第一种是通过文件系统的readFileSync进行文件的读取,参考Nodejs官网,fs.readFileSync。
可以发现,这个方法读取文件的返回值默认是Buffer对象:
那么如果把responseType改为arraybuffer是否也可以?
export async function queryFile(){
return await http.get('/file',{
params: {
site_id: 4
},
responseType: 'arraybuffer'
})
}
注意,此时responseType改为“arraybuffer“之后,拦截器中对返回值类型的判断会发生变化:
那么,在生成URL的时候,就必须先通过new Blob进行转换了。
综上
文件上传与下载并不是很难理解的东西,关键点在于其中用到的知识点比较多,从上传的FormData、form标签的enctype属性到下载时接收的数据类型等,哪怕有一步出现问题都会出错,而且无法找到错误位置,毕竟,下面两个玩意,一般人也看不懂。
参考
http上传文件
常见 MIME 类型列表
Blob对象
axios-请求配置
express-res