vue excel文件上传下载

2,368 阅读6分钟

这是我参与更文挑战的第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的异步事件一直进去不,如果有疑问,大家可以一起沟通,学习