这是我参与更文挑战的第8天,活动详情查看: 更文挑战
最近正好在做一个需求,关于文件的上传,下载功能,中间也遇到一些坑,记录总结一下,在写这个功能之前,需要大家先了解一下前置知识。前端发送请求,附带参数,服务端响应请求,接收参数,一般会根据content-type字段来获取消息主体的编码方式,然后对应解码。
1 ,前话
在最早的HTTP 协议POST请求中,参数都是通过浏览器的url传递,只支持:application/x-www-form-urlencoded,其实是不支持文件上传的,为了能够支持文件上传,content-type扩充了multipart/form-data类型 ,用于支持向服务器发送二进制数据,再后来,随和web应用的发展,增加了application/json的类型,最常用,适合RESTFUL的接口
2, Content-Type类型
(1) application/x-www-form-urlencoded
原生的表单,如果不设置enctype属性,默认为application/x-www-form-urlencoded 方式进行数据提交,提交的表达数据会转换为键值对,按照 key=value1&key=value3&key=value3的方式进行编码,如果是中文或者特殊符号会自动进行URL转码。 注意: 不支持文件,一般用于表单提交
(2) multipart/form-data
表单上传文件时,必须让设置form的enctype等于multipart/form-data这个类型。此种方式多用于文件上传,各个表单项之间用boundary分开,boundary分界一般会放在Content-Type后面,传到服务器,服务器根据这个边界解析数据,划分段。
Connection: keep-alive
Content-Length: 9020
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryujJLO5p0jTkaNNbC
注意: Content-Type 里指明了数据是以 mutipart/form-data 来编码,本次请求的 boundary 是什么内容。消息主体里按照字段个数又分为多个结构类似的部分,每部分都是以 boundary 开始,消息主体最后以 boundary 标示结束
<form action="/" method="post" enctype="multipart/form-data">
<input type="text" name="description">
<input type="file" name="myFile">
<button type="submit">Submit</button>
</form>
(3) application/json
序列话后的JSON字符串,目前是最常用的,用来告诉服务端消息主体是序列化后的 JSON 字符串,其中一个好处就是JSON 格式支持比键值对复杂得多的结构化数据,而且服务器端都能很好的处理JSON数据,由于json规范的流行,各大浏览器都开始原生支持JSON.stringfy。 RESTFUL API 接收的大部分都是application/json类型,
3, 上传下载
这个需求比较特别:后端会返回我不同的数据结构,如果文件上传成功并且后端校验成功之后,接口返回我的数据是:JSON,如果文件上传成功,后端验证失败,接口返回我的数据是个二进制流,前端需要以.xlsx的方式下载下来,如果文件上传失败,接口会返回二进制流,如果解析失败,也会给出错误提示
首先是下载excel模版,这个比较简单:
1, 从服务器上获取到模版数据:请求设置:
responseType: 'blob',
2,然后封装一个下载方法
/**
* 描述:blob对象 第一个参数 response.data是代表后端返回的文件流 , 第二个参数设置文件类型
* response : Blob对象数据
* filename : 文件名称
*/
downloadFileExport(response, filename) {
const contentType = response.type // 文件类型
const blob = new Blob([response], {
type: contentType
})
if ('download' in document.createElement('a')) { // 非IE下载
const linkElement = document.createElement('a') // 创建a标签
// 生成下载链接,这个链接放在a标签上是直接下载,放在img上可以直接显示图片问价,视频同理
const url = window.URL.createObjectURL(blob)
linkElement.setAttribute('href', url)
linkElement.setAttribute('target', '_blank')
linkElement.setAttribute('download', filename)
// 模拟点击a标签
if (typeof MouseEvent === 'function') {
var event = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: false
})
linkElement.dispatchEvent(event)
}
} else {
navigator.msSaveBlob(blob, filename)
}
}
3,模版下载
data: 是从后端拿到的Blob数据流,
const textValue = '用户表格.xlsx'
this.downLoadFileExport(data, textValue)
4,文件上传
1,第一步是对上传文件的校验
// 点击下载按钮,
changeFile(file) {
const fileData = file.raw // 获取文件内容
if(!fileData) return; // 如果文件内容为空
// 1,中间是对文件内容的解析拼接 此处省略
// 把拼接好参数发给后端接口进行校验 此处省略, 每个人的需求都一样 checkExcel(param)
// 3, 根据校验的结果进行展示
checkExcel(form).then(res => {
let data = res.data // 拿到服务器返回的校验结果的blob数据流
// 如果后端返回的类型是json,说明不需要下载excel
if(data.type.includes('application/json')) {
var reader = new FileReader(); // FileReader 把blod流转换成JSON对象
reader.onload = (e) => { // 异步函数,回调解析拿到的JOSN对象
let result = JSON.parse(reader.result);
if (result && result.code == 200) {
// 让上传成功的文件显示出来, 并且只能上传一个
this.fileList[0]= { name: file.name, uid: file.uid }
this.$message.success('文件上传成功')
} else {
this.fileList = []
this.$message.error(result.message)
}
}
reader.readAsText(data, 'utf-8');
}
// 如果后端返回的类型是application/vnd.ms-excel,说明需要下载excel
if(data.type.includes('application/vnd.ms-excel')) { // 如果是excel流
this.fileList = []
this.$message.error('文件上传失败')
const textValue = file.name
const blob = new Blob([res.data], {type: "application/vnd.ms-excel;charset=utf-8"})
this.downLoadFileExport(blob, textValue)
}
}).catch(err => {
this.fileList = []
console.log(err)
})
}
2,第二步是对文件内容审批链的校验
/**
*对文件内容进行校验
*/
checkContent(params).then(res => {
let checkData = res.data // 后端返回服务器校验文件的结果
// 如果校验的结果是JSON对象,
if(checkData.type.includes('application/json')) {
var reader = new FileReader();
reader.onload =(e) => {
let result = JSON.parse(reader.result);
if (result.data && result.code == '200') {
this.approveChainForSubmit = result.data.approvalChain
this.cacheKey = result.data.cacheKey
// 获取到数据之后的其他操作,大家可以忽略
} else {
this.$message.error(result.message)
}
}
reader.readAsText(checkData, 'utf-8'); // 注意顺序
}
// 如果后端返回的是application/vnd.ms-excel, 说明验证失败,需要下载excel
if(checkData.type.includes('application/vnd.ms-excel')) {
this.$message.error('审批链验证失败')
const textValue = this.fileList[0].name
const blob = new Blob([checkData], {type: "application/vnd.ms-excel;charset=utf-8"})
this.downLoadFileExport(blob, textValue)
return new Promise(()=>{}) // 终止掉.then的链式调用,有切只有返回一个 pendding状态的promise
}
}).catch(err => {
console.log(err)
})
4, 踩坑地方
这里有三点需要之一:
1, fileReader对象的事件先后顺序
reader.readAsText(checkData, 'utf-8') 和 reader.onload =(e) =>{ }的先后顺序
官方给出的:先:reader.onload
后: reader.readAsText
必须在执行完reader.onload之后再执行想要请求的方法才能取到想要的参数
2,return new Promise(() =>{})
多个链式请求, 只有要一个报错了,就要终止。想要终止掉.then的链式调用, 有且只有一个方法,返回一个pendding状态的promise
3, blob = new Blob([data], filename)
blob, 二进制大对象,是一个可以存储二进制文件的容器,
Blob对象指的是字节序列,并且具有size属性,是字节序列中的字节总数,和一个type属性,它是小写的ASCII编码的字符串表示的媒体类型字节序列。
使用Blob() 的构造函数来进行创建。 构造函数接受两个参数
第一个参数为一个数据序列,格式可以是ArrayBuffer, ArrayBufferView, Blob, DOMString
第二个参数是一个包含以下两个属性的对象
5, DOM代码
简单贴一下DOM代码
<el-button class="downLoad" @click="downloadTemplate">模板下载</el-button>
<div class="ElFormItem-right">
<el-upload
:auto-upload="false"
:show-file-list="true"
:on-change="changeFile"
:action="upload"
:file-list="fileList"
:on-remove="handleRemove"
accept=".xlsx"
>
<el-button size="mini">批量上传</el-button>
</el-upload>
</div>
6, 总结
下载的方法有很多,大家也可以借助第三方工作,这篇文章主要是记录一下我在开发工程中遇到的问题,主要是踩坑的地方耽误了不少时间,顺序不对,reader.onload的异步事件一直进去不,如果有疑问,大家可以一起沟通,学习