post請求實現前端文件導出

請求庫爲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)ios

請求庫爲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',這句代碼很重要。
若是沒有這句代碼,雖然能夠成功將文件下載下來,不過下載下來的文件內容亂碼,以下圖所示:

反之,下載下來的文件正常,以下圖所示:
nginx


若是遇到導出請求返回響應爲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配置項實現前端請求超時控制,須要本身封裝,如下爲本身封裝的帶超時控制的導出功能。web

參考文章

ES6 fetch(input, init) 設置超時(timeout)
ajax

讓fetch也能夠timeout
json

/**
 * 導出文件公共函數
 * 因爲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;
}複製代碼
相關文章
相關標籤/搜索