JS文件流或文件路径的上传及下载处理方式

5,858 阅读4分钟

一. 下载文件

  • 下载:包括但不限于导出、保存……
  • 文件:包括但不限于图片、文档、表格……

数据来源:文件路径、二进制流文件数据

  1. 二进制文件流数据

    当后端返回的是二进制流数据时,则需要借助Blob对象进行读取,才可获得文件 image.png

    • 封装完整的使用 Blob 和 URL.createObjectURL 下载文件流的 util 函数:
    /**
     * @func downloadExprotFile
     * @desc 根据文件流下载文件
     * @param {string} fileStream 文件流
     * @param {string} name 文件名
     * @param {string} extension 文件后缀
     * @param {string} [type] 文件类型(当不知道类型时可以选择不写)
     * @returns {object} undefined
     * @example 
     *        downloadExprotFile(data, '证件申办表', 'docx')
     *       .then(res => { this.$message.success('导出成功') })
     *       .catch(errMsg => { this.$message.error(errMsg || '导出失败') })
     */
    export const downloadExprotFile = (fileStream, name, extension, type = "") => {
      return new Promise((resolve, reject) => {
        const blob = new Blob([fileStream], {type: type || fileStream.type});
        const fileName = `${name}.${extension}`;
        if ("download" in document.createElement("a") && fileStream.type) { // 非IE下载
          const elink = document.createElement("a");
          elink.download = fileName;
          elink.style.display = "none";
          elink.href = URL.createObjectURL(blob);
          document.body.appendChild(elink);
          elink.click();
          URL.revokeObjectURL(elink.href);
          document.body.removeChild(elink);
          resolve(fileStream)
        } else { // IE10+下载
          navigator.msSaveBlob(blob, fileName);
          reject(fileStream)
        }
      })
    };
    
    • fileName后端定义,前端获取
      • 如果前端下载文件流时,fileName文件名由后端决定并设置在Headers中的Content-disposition字段上
      • 那么前端通过res.headers["content-disposition"]进行获取时
      • 确保后端在响应头也配置了Access-Control-Expose-Headers允许前端使用该字段,否则前端将无法获取
      // response interceptor 响应拦截
      service.interceptors.response.use(
        response => {
          // 获取headers中的filename文件名且进行解码处理
          response.headers['content-disposition'] && response.data && (response.data.filename = decodeURIComponent(response.headers['content-disposition'].split(';')[1].split('filename=')[1]))
          // ……
          
          return response.data
        },
        error => {
          return Promise.reject(error)
        }
      )
      
      image.png
  2. 文件路径

    图片资源路径分为同源(本地)文件和跨域非同源文件,而这两种路径,用不同方法保存,在PC端和移动端又有不同的表示形式 保存的最大问题就是当图片路径是跨域非同源的这种情况。

  • 图片资源路径为同源(本地):

    • PC端和移动端都表现为弹出图片新页面:以下保存方法的1和2
      • 想要保存,PC端右键或Ctrl+S保存,移动端只能进行长按操作
    • PC端和移动端都被识别为文件正常保存操作:以下保存方法的3、4、5、6
      • PC端会自动弹出保存窗口
      • 移动端不同手机、不同浏览器会有不同的保存表现形式,如预览下载,弹窗下载,……
  • 图片资源路径为跨域非同源:首先肯定得让后端设置允许跨域访问,这是保存前提。

    • PC端和移动端都表现为弹出图片新页面:以下保存方法的1、2、3、4
      • 想要保存,PC端右键或Ctrl+S保存,移动端只能进行长按操作
    • PC端和移动端都被识别为文件正常保存操作:以下保存方法的5、6
      • PC端会自动弹出保存窗口
      • 移动端不同手机、不同浏览器会有不同的保存表现形式,如预览下载,弹窗下载,……

    结束了? 没有!!! 跨域非同源的海报图用5和6的方法保存,PC端正常,但移动端还存在很多不确定因素。

    比如,在Vivo的QQ浏览器上,会自动进入预览功能,但无法看到图片,却依然可以保存成功,在相册可查看。

    比如,在rognyao play5的百度浏览器上,完全无法下载,但自带的浏览器却可以正常保存。

  • 结论:

    • 当图片路径是跨域非同源时,想办法实现为同源或本地路径的资源。
    • 比如,二维码海报图,不让后端生成了,前端自个儿生成二维码,与背景图结合,保存时用canvas转化成图片。
    • 否则,当你用以下方法的5或6时,PC端正常,移动端就祈祷甲方爸爸的手机及浏览器刚好支持吧。
    • 我也想、很想一步到位。。。
    • 当然,如果不考虑用户体验的话,那么a标签再加个保存提示,so easy。
  • 常见6种保存方法:

    // 1. 
    window.location.href = "url"
    
    // 2. 
    window.open(url)
    
    // 3.
    <a target="_blank" download="poster.jpg" :href="url">保存至相册</a>
    
    // 4. 
    saveImg(Url){
      if(!Url){
        Toast.fail('暂无海报!');
        return;
      }
      var blob=new Blob([''], {type:'application/octet-stream'});
      var url = URL.createObjectURL(blob);
      var a = document.createElement('a');
      a.href = Url;
      // a.target = '_blank';
      a.download = Url.replace(/(.*\/)*([^.]+.*)/ig,"$2").split("?")[0];
      console.log("a:", a)
      var e = document.createEvent('MouseEvents');
      e.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
      a.dispatchEvent(e);
      URL.revokeObjectURL(url);
    },
    
    // 5. 
    downloadExprotFile (fileStream, name, extension, type = "") {
      return new Promise((resolve, reject) => {
        const blob = new Blob([fileStream], {type: type || fileStream.type});
        const fileName = `${name}.${extension}`;
        if ("download" in document.createElement("a") && fileStream.type) { // 非IE下载
          const elink = document.createElement("a");
          elink.download = fileName;
          elink.style.display = "none";
          elink.href = URL.createObjectURL(blob);
          document.body.appendChild(elink);
          elink.click();
          URL.revokeObjectURL(elink.href);
          document.body.removeChild(elink);
          resolve(fileStream)
        } else { // IE10+下载
          navigator.msSaveBlob(blob, fileName);
          reject(fileStream)
        }
      })
    },
    //注意: fetch(Url, { mode: "no-cors" }); "no-cors"是无法解决跨域问题的,设置之后将无法获取响应结果
    fetch(Url).then((res) => res.blob()).then((fileStream) => {
            this.downloadExprotFile(fileStream, 'poster', 'jpg')
    .then(res => { this.$message.success('保存成功') })
    .catch(errMsg => { this.$message.error(errMsg || '保存失败') })
    })
    
    // 6. 
    downloadIamge(imgsrc, name) {
      let image = new Image();
      image.setAttribute("crossOrigin", "anonymous");
      image.onload = function() {
        let canvas = document.createElement("canvas");
        canvas.width = image.width;
        canvas.height = image.height;
        let context = canvas.getContext("2d");
        context.drawImage(image, 0, 0, image.width, image.height);
        let url = canvas.toDataURL("image/png"); //得到图片的base64编码数据
        let a = document.createElement("a"); // 生成一个a元素
        let event = new MouseEvent("click"); // 创建一个单击事件
        a.download = name || "海报"; // 设置图片名称没有设置则为默认
        a.href = url; // 将生成的URL设置为a.href属性
        a.dispatchEvent(event); // 触发a的单击事件
      };
      image.src = imgsrc;
    },
    

Blob对象转JSON格式

  1. 应用场景:文件下载请求时,后端接口成功时响应数据是文件流Blob对象,错误时响应数据是json格式,因为响应头设置为responseTypeblob, 针对下载失败提示处理,需将blob对象转为json数据格式
  2. BlobToJson函数封装:

    参数字段res.typedata.code,根据自身实际需要,进行相应调整

/**
 * @func: blobToJson
 * @description: 将blob对象转为json数据
 * @param {Object} res Blob对象
 * @return {Boolean} true/false false表示导出有误,中断执行
 * @example: blobToJson(res)
 */
function blobToJson(res) {
  if (res && res.type === 'application/json') {
    var enc = new TextDecoder('utf-8')
    res.arrayBuffer().then(buffer => {
      const data = JSON.parse(enc.decode(new Uint8Array(buffer))) || {}
      if (data.code == 500) {
        Message({
          message: data.msg || '导出失败',
          type: 'error',
          duration: 2 * 1000
        })
        return false
      }
      return true
    })
  } else {
    return true
  }
}

二. 上传文件:包括但不限于图片、文档、表格

上传文件流数据: FormData & multipart/form-data

  • 上传文件功能,可以借助各种组件库插件,也可以自己手写原生form表单指定enctype为multipart/form-data,继而进行提交
  • 提交方式有两种:
    1. 通过插件或form的action或相应属性,设置好接口路径,这样submit提交时会自动封装文件数据上传到后端
    2. 手动 new一个FormData对象,用来装载文件流数据,再将数据作为接口入参

      Post请求接口的contentType必须指定为multipart/form-data,否则将无法上传文件流数据

      let _formData = new FormData();
      _formData.append("file", this.importFile[0].raw); // this.importFile[0].raw 就是binary文件流数据
      // 所以入参都append到_formData 中,再将_formData作为接口参数data传给后端
      
      image.png

延伸知识:Method 与 Headers.ContentType的

  • 无论是原生js的ajax,还是vue的axios,封装请求接口时,有两个参数特别注意优雅设置:Method 和 Headers.ContentType。

    • Method :请求方法,表明要对给定资源执行的操作
    • Headers.ContentType:指定服务器与客户端发送或接收的数据类型
    • 注意get请求方式与contentType数据类型无关
      • 它的请求不存在请求实体部分,入参的键值对会放置在 URL 尾部,浏览器自动把数据转换成一个字串
      • 如果有特殊符号,最好自行先预编码,预防不同浏览器编码方式差异,导致后端接收有误。
    • 请求报文和响应报文的contentTyp:

      封装接口时,前端与后端约定好接收的数据类型,以免开发结束后联调时出错 image.png

  • 用post提交方式,常用ContentType数据类型有三种:

    1. ‘application/x-www-form-urlencoded’
      • $.ajax、form等常见的默认数据类型,它的默认行为表现形式是自动将入参数据编码为键值对,这是标准的编码格式。
    2. ‘application/json’
      • 设置该数据类型后,需要用JSON.stringify将入参数据序列化成JSON 字符串
    3. ‘multipart/form-data’
      • 设置该数据类型,则需要将数据封装进new FormData对象中,或用form表单指定enctype为multipart/form-data,再提交数据 image.png

Blob下载文件参考文档