post请求实现前端文件导出

6,309 阅读4分钟

请求库为fetch

export const exportDataByPost = async (url, params) => {
  console.log('------exportDataByPost------');
  let AFFTK = localStorage.getItem('AFFTK')
  const token = `Bearer ${AFFTK}`
  const merchant = localStorage.getItem('MID') ? localStorage.getItem('MID').split('_')[1] : ''
  try{
    const response = await fetch(baseUrl + prefix + url, {
      mode: 'cors',
      method: 'POST',
      headers: {
        "Content-Type": "application/json",
        "Accept": "application/json",
        "Authorization": token,
        merchant,
      },
      body: JSON.stringify(params),
    })
    if (response.headers.get('content-type') !== 'application/json') {
      response.blob().then((blob) => {
        const a = window.document.createElement('a');
        const downUrl = window.URL.createObjectURL(blob);// 获取 blob 本地文件连接 (blob 为纯二进制对象,不能够直接保存到磁盘上)
        let filename = "download.xls";
        if (response.headers.get('content-disposition') && response.headers.get('content-disposition').indexOf("filename=") !== -1) {
          filename = response.headers.get('content-disposition').split('filename=')[1];
          a.href = downUrl;
          a.download = `${decodeURI(filename.split('"')[1])}` || "download.xls";
          a.click();
          window.URL.revokeObjectURL(downUrl);
        }
      }).catch(error =>{
        message.error(error);
      });
    } else {
      let res = await response.json();
      message.error(res.msg);
    }
  }catch(err){
    message.error('下载超时');
  }
}

请求库采用fetch,导出方法封装的思路

  • 封装的方法为异步方法,返回一个Promise
  • 如果响应头content-type参数为application/json,返回的响应为json对象,需要通过response.json()转化为前端可以解析的json对象,转化后的json对象中有code和msg字段,用于向用户提示错误信息
  • 如果返回的响应为文件流,从原始响应到可下载到浏览器的文件的过程中,需要经历以下过程
  1. 从响应头content-disposition中获取到文件名称
  2. 通过response.blob()将原生响应转化为前端可解析的blob
  3. 在response.blob的回调函数中,以特定方法实现浏览器下载文件

注意点

通过fetch发起的请求,无法直接通过诸如response.headers['content-disposition']获取content-disposition参数,这一点与axios不同。

遇到的问题

本地测试可以实现正常下载,但是部署到测试环境出现 ”无法获取文件名“ 的问题。经追踪,发现在测试环境是因为无法从响应头获取 content-disposition 字段信息,从而导致无法获取文件名。

分析原因

代码部署到测试环境之后,页面访问的是远程测试服务器的前端文件目录,而从前端文件目录发起请求后需要经过nginx反向代理,这时后端下载请求响应头中未暴露出 content-disposition 字段信息。

解决方案

服务端代码设置header暴露出 content-disposition 字段信息(在nginx配置文件中设置无效,只能在服务端代码中添加相关代码)。

参考文章

前端axios获取二进制流下载excel并解决无法获header问题

Fetch / ajax 不能获取response中的所有headers的解决方法(适用nginx)

请求库为axios

/**
 * 对应导出请求,返回的响应为json对象,不是文件流的情况,
 * 需要重新请求一遍
 * 不过不带responseType: 'blob'
 * @param url 请求地址
 * @param params 请求表单参数
 */
function exportDataByJson (url, params) {
  axios({
    url: baseUrl + url,
    method: 'post',
    data: params
  }).then((res) => {
    Message({
      type: 'error',
      message: res.msg
    })
  }).catch((err) => {
    console.error(err)
  })
}
/**
 * 导出文件post请求
 * @param url 请求地址
 * @param params 请求表单参数
 */
export async function exportDataByPost (url, params) {
  console.log('------exportDataByPost------')
  try {
    const response = await axios({
      url: baseUrl + url,
      method: 'post',
      responseType: 'blob', // 这句话很重要
      data: params
    })
    // 当导出请求,返回的响应为json对象时,
    // 根本就不会走到这里,直接走到后面catch里面了
    // 所以下面console.log(response)根本不会打印出来
    console.log(response)
    if (response.status !== 200) {
      console.log('网络或服务器异常!')
      return
    }
    let blob = new Blob([response.data], { type: response.headers['content-type'] })
    const a = window.document.createElement('a')
    const downUrl = window.URL.createObjectURL(blob)// 获取 blob 本地文件连接 (blob 为纯二进制对象,不能够直接保存到磁盘上)
    let filename = 'download.xls'
    if (response.headers['content-disposition'] && response.headers['content-disposition'].indexOf('filename=') !== -1) {
      filename = response.headers['content-disposition'].split('filename=')[1]
      a.href = downUrl
      a.download = `${decodeURI(filename.split('"')[1])}` || 'download.xls'
      a.click()
      window.URL.revokeObjectURL(downUrl)
    }
  } catch (err) {
    console.error(err.type)
    if (err.type === 'application/json') {
      console.log('导出请求返回的是json数据啊啊啊啊啊')
      // 再次请求接口,获取错误提示信息,不过要去掉responseType: 'blob',
      exportDataByJson(url, params)
    }
  }
}

axios实现导出与fetch实现导出的不同在于:
  1. 前者的请求参数中需要加 responseType: 'blob'
  2. 原生响应转化为blob的方法不一样


其中,responseType: 'blob',这句代码很重要。
如果没有这句代码,虽然可以成功将文件下载下来,不过下载下来的文件内容乱码,如下图所示:

反之,下载下来的文件正常,如下图所示:


如果遇到导出请求返回响应为json对象数据,axios相对于fetch需要再请求一次接口获取错误信息,提示给用户,网络消耗增大。
除了以上方法之外,下述postExportFile方法也是一种解决方案,该方法是通过构建表单的方式实现下载。但不是所有post请求导出都可以通过下述postExportFile方法正确导出文件,因此遇到导出,最好还是采用上述方案。

postExportFile (params, url) {
  // params是post请求需要的参数,url是请求url地址
  let form = document.createElement('form')
  form.style.display = 'none'
  form.action = url
  form.method = 'post'
  document.body.appendChild(form)
  for (let key in params) {
    let input = document.createElement('input')
    input.type = 'hidden'
    input.name = key
    input.value = params[key]
    form.appendChild(input)
  }
  let inputToken = document.createElement('input')
  inputToken.type = 'hidden'
  inputToken.name = 'token'
  inputToken.value = localStorage.getItem('token')
  form.appendChild(inputToken)
  form.submit()
  form.remove()
},
exportData () {
  let startDate = ''
  let endDate = ''
  if (this.queryValues.date) {
    let [sDate, eDate] = this.queryValues.date
    startDate = moment(sDate).format('YYYY-MM-DD')
    endDate = moment(eDate).format('YYYY-MM-DD')
  }
  let qDate = deepCopy(this.queryValues)
  delete qDate.date
  let params = Object.assign(
    {},
    {
      start_date: startDate,
      end_date: endDate,
      page: this.currentPageNo,
      page_size: this.pageSize
    },
    qDate
  )
  exportDataByPost('/report/order/export', params)
  /* this.$store.dispatch('reportOrderExport', params).then(res => {
    if (res.data) {
      this.postExportFile(params, baseUrl + '/report/order/export')
    }
  }) */
}

timeout支持

由于fetch无法像axios一样通过timeout配置项实现前端请求超时控制,需要自己封装,以下为自己封装的带超时控制的导出功能。

参考文章

ES6 fetch(input, init) 设置超时(timeout)

让fetch也可以timeout

/**
 * 导出文件公共函数
 * 由于fecth请求,没有超时timeout配置项,
 * 需要封装实现类似于axios timeout的功能
 * 在此背景下,特封装了一个带前端请求超时控制的fecth导出文件公共函数
 */
import fetch from 'dva/fetch'
import { message } from 'antd'
import { baseUrl } from './config'

const prefix =  '/api'

const _fetch = (fetchRequest, timeout) => {
  timeout = timeout > 0 ? timeout : 0;
  let breaker = new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new Error('请求超时'))
    }, timeout);
  });
  /*
  * Promise.race(iterable)方法返回一个promise,
  * 这个promise在iterable中的任意一个promise被解决或拒绝后,
  * 立刻以相同的解决值被解决或以相同的拒绝原因被拒绝。
  */
  return timeout === 0 ? fetchRequest : Promise.race([fetchRequest, breaker]);
};

const exportDataByPostFn = async (url, params) => {
  let AFFTK = localStorage.getItem('AFFTK')
  const token = `Bearer ${AFFTK}`
  const merchant = localStorage.getItem('MID') ? localStorage.getItem('MID').split('_')[1] : ''
  /* 'http://localhost:8081/user/login' */
  const response = await fetch(baseUrl + prefix + url, {
    mode: 'cors',
    method: 'POST',
    headers: {
      "Content-Type": "application/json",
      "Accept": "application/json",
      "Authorization": token,
      merchant,
    },
    body: JSON.stringify(params),
  })
  // response.headers.get('content-type') 可能是 application/json
  // 也可能是 application/json; charset=utf-8
  if (!response.headers.get('content-type').includes('application/json')) {
    response.blob().then((blob) => {
      const a = window.document.createElement('a');
      const downUrl = window.URL.createObjectURL(blob);// 获取 blob 本地文件连接 (blob 为纯二进制对象,不能够直接保存到磁盘上)
      let filename = "download.xls";
      if (response.headers.get('content-disposition') && response.headers.get('content-disposition').indexOf("filename=") !== -1) {
        filename = response.headers.get('content-disposition').split('filename=')[1];
        a.href = downUrl;
        a.download = `${decodeURI(filename.split('"')[1])}` || "download.xls";
        a.click();
        window.URL.revokeObjectURL(downUrl);
      }
    }).catch(error =>{
      message.error(error);
    });
  } else {
    let res = await response.json();
    message.error(res.msg);
  }
}

/**
 * 导出excel文件post请求
 * @param url 请求地址
 * @param url 请求地址样例:'/commission/extra-model/export'
 * @param params 请求表单参数
 * @param params 格式
 * {
 *   a: 1,
 *   b: 2
 * }
 */
export const exportDataByPost = async (url, params) => {
  console.log('------exportDataByPost------');
  let fetchRequest = exportDataByPostFn(url, params)
  let res = await _fetch(fetchRequest, 10 * 1000)
  return res;
}