接口導(dǎo)出文件下載的幾種情況

前提:利用接口導(dǎo)出一個(gè)文件,前端會(huì)遇到的幾種情況。這里主要記錄一下文件流太大的情況

1. 導(dǎo)出接口直接給的文件流
  • 文件不大的時(shí)候,我們直接使用a標(biāo)簽交給瀏覽器來(lái)下載
function downOSSUrl(url) {
  const a = document.createElement('a')
  a.href = url
  a.rel = 'noreferrer'
  document.body.appendChild(a)
  a.click()
  a.remove()
}

  • 文件大的時(shí)候,直接使用以上方式就不太合適,接口處理太慢了,點(diǎn)擊后頁(yè)面沒(méi)啥反應(yīng),還以為有bug。那就獲取狀態(tài)加個(gè)loading吧
// request 是封裝的axios。 主要是注明一下 responseType: 'blob' 
     this.exportLoading = true
       request({ url:'exportURL', params, responseType: 'blob' }).then(res => {
         this.exportLoading = false
         if (res === 0) {
           this.$message.warning(提示語(yǔ))
         }
       }).catch(() => {
         this.exportLoading = false
       })

攔截一下axios。 下面的鏈接超時(shí)時(shí)間timeout以及nginx的超時(shí)時(shí)間我們都改大一些

const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
  timeout: 300000 // request timeout
})

service.interceptors.response.use(
   response => {
    if (response.config.responseType === 'blob' && response.status === 200) {
      if (response.data.size === 0) {
        return 0
      }
      const filename = response.headers['content-disposition'].split(';')[1].split('=')[1]
      const blob = new Blob([response.data])
      const downloadElement = document.createElement('a')
      const href = window.URL.createObjectURL(blob) // 創(chuàng)建下載的鏈接
      downloadElement.href = href
      downloadElement.download = filename // 下載后文件名
      document.body.appendChild(downloadElement)
      downloadElement.click() // 點(diǎn)擊下載
      document.body.removeChild(downloadElement) // 下載完成移除元素
      window.URL.revokeObjectURL(href) // 釋放掉blob對(duì)象
      return
    }
}
)

以上代碼可以看到兩個(gè)點(diǎn)

(1)response.data.size 長(zhǎng)度為0 ,我這里直接返回0.
因?yàn)楹蠖丝紤]到處理時(shí)間太長(zhǎng),機(jī)器負(fù)載問(wèn)題,限制了一分鐘內(nèi)不能同時(shí)多次下載,直接拋錯(cuò)。當(dāng)多次請(qǐng)求到后端時(shí)體現(xiàn)在接口返回內(nèi)容上,流的長(zhǎng)度就是0,此時(shí)提示用戶(hù):稍后再試
(2)當(dāng)有信息的時(shí)候呢,那就是有兩種情況。
一種是包含錯(cuò)誤信息或者提示信息, 一種是真正的文件信息。上面代碼只考慮了直接下載文件信息。
當(dāng)和后端有約定要展示特定條件的提示信息時(shí),可以增加以下判斷。將流用 filereader解析程json

if(response.headers['content-type'].includes('json')){
   const fileReader = new FileReader()
  fileReader.readAsText(response.data, 'utf-8')
 fileReader.onload = function(res){
   ....
  }
}  

(3) 文件名從響應(yīng)頭中的content-disposition拿到,沒(méi)有的話(huà),要后端導(dǎo)出

其實(shí)相比于直接用a標(biāo)簽交給瀏覽器下載,這樣的請(qǐng)求后下載的,是直接寫(xiě)入到瀏覽器內(nèi)存的,所以可以看到整個(gè)請(qǐng)求也包含了download的時(shí)間,再到磁盤(pán),才會(huì)在瀏覽器上看到進(jìn)入了下載中心

2. 導(dǎo)出接口給的是有正常響應(yīng)體,resultoss地址,那拿到地址后再直接去a標(biāo)簽下載即可
3. 接口處理時(shí)間太長(zhǎng),出現(xiàn)504 gateway time-out,很明顯nginx太久沒(méi)拿到服務(wù)端結(jié)果,給了前端504。 設(shè)置nginx代理響應(yīng)時(shí)間

(1)上面有個(gè)axios 請(qǐng)求超時(shí)的時(shí)間,以毫秒為單位。設(shè)置大點(diǎn),這里超時(shí)是主動(dòng)斷開(kāi)了,但是后端還在處理。
(2)nginx 設(shè)置超時(shí)時(shí)間,單位是秒。可以設(shè)置在http全局模塊,server模塊,或者是location特定模塊。

location /api {
proxy_read_timeout 300;   // 代理讀取超時(shí)時(shí)間
proxy_send_timeout       300;    // 代理發(fā)送超時(shí)時(shí)間
proxy_connect_timeout       300;  // 代理連接超時(shí)時(shí)間
}

4. 由前端生成excel, 輪詢(xún)拿到數(shù)據(jù)再去組裝。將壓力給到客戶(hù)端

  • 第一種:使用exceljsfile-saver
// newToExcel.js

import { isEmpty } from "element-ui/lib/utils/util";
import ExcelJS from "exceljs";
import * as FileSaver from "file-saver";

export default function createWorkBook(
  header,
  title,
  data,
  foot,
  filename,
  sheets
) {
  const letter = [
    "A",
    "B",
    "C",
    "D",
    "E",
    "F",
    "G",
    "H",
    "I",
    "J",
    "K",
    "L",
    "M",
    "N",
    "O",
    "P",
    "Q",
    "R",
    "S",
    "T",
    "U",
    "V",
    "W",
    "X",
    "Y",
    "Z"
  ];
  let lcomun = 1;
  let worksheet;

  const workBook = new ExcelJS.Workbook();
  let long = header.length;

  /**
   *  創(chuàng)建工作薄
   * @param {*} sheets
   */
  function createSheets(sheets) {
    let sheet = Array.isArray(sheets) ? sheets[0] : sheets;
    let style = Array.isArray(sheets) ? sheets[1] : {};
    worksheet = workBook.addWorksheet(sheet, style);
  }

  /**
   *  設(shè)置表名介紹等
   * @param {*} title
   * @param {*} long
   */
  function setTitle(title, long) {
    if (isEmpty(title)) return;
    title = Array.isArray(title) ? title : title.split(",");
    for (let i = 0; i < title.length; i++) {
      let ti = worksheet.getRow(i + 1);
      ti.getCell(1).value = title[i];
      ti.height = 30;
      ti.font = { bold: true, size: 20, vertAlign: "subscript" };
      ti.alignment = { vertical: "bottom", horizontal: "center" };
      ti.outlineLevel = 1;
      worksheet.mergeCells(i + 1, 1, i + 1, long);
      ti.commit();
      lcomun++;
    }
  }
  /**
   *  設(shè)置表頭行
   * @param {*} header
   */
  function setHeader(header) {
    if (isEmpty(header)) return;
    const headerRow = worksheet.getRow(lcomun);
    for (let index = 1; index <= header.length; index++) {
      headerRow.getCell(index).value = header[index - 1];
    }
    headerRow.height = 25;
    headerRow.width = 50;
    headerRow.font = { bold: true, size: 18, vertAlign: "subscript" };
    headerRow.alignment = { vertical: "bottom", horizontal: "center" };
    headerRow.outlineLevel = 1;
    headerRow.commit();
    lcomun++;
  }

  /**
   * 導(dǎo)出內(nèi)容
   * @param {*} data
   */
  function setContent(data) {
    if (isEmpty(data)) return;
    for (let h = 0; h < data.length; h++) {
      let satarLcomun = lcomun;
      let lcomunNow = worksheet.getRow(lcomun);
      let hasMerge = false;
      let starKey = 0;
      let endKey = 0;
      /** 循環(huán)列 */
      //需要操作第幾列
      let sk = 0;
      for (let l = 0; l < data[h].length; l++) {
        if (Array.isArray(data[h][l])) {
          //數(shù)組長(zhǎng)度
          starKey = sk;
          hasMerge = true;
          setArrayContent(data[h][l], sk);
          sk = sk + data[h][l][0].length;
          endKey = sk;
        } else {
          //不是數(shù)組
          lcomunNow.getCell(getLetter(sk)).value = data[h][l];
          lcomunNow.getCell(getLetter(sk)).border = {
            top: { style: "thin" },
            left: { style: "thin" },
            bottom: { style: "thin" },
            right: { style: "thin" }
          };
          lcomunNow.alignment = { vertical: "middle", horizontal: "center" };
          sk++;
        }
      }
      if (hasMerge) setMergeLcomun(satarLcomun, lcomun, starKey, endKey);
      lcomunNow.height = 25;
      lcomunNow.commit();
      lcomun++;
    }
  }
  /**
   * 占多行的數(shù)組
   * @param {*} arr
   * @param {*} sk
   */
  function setArrayContent(arr, sk) {
    /**
     *  循環(huán)二維數(shù)組,在循環(huán)行
     */
    let al = arr.length;
    let sl = al - 1;
    for (let i = 0; i < arr.length; i++) {
      let lcomunNow = worksheet.getRow(lcomun);
      for (let v = 0; v < arr[i].length; v++) {
        lcomunNow.getCell(getLetter(sk + v)).value = arr[i][v];
        lcomunNow.getCell(getLetter(sk + v)).border = {
          top: { style: "thin" },
          left: { style: "thin" },
          bottom: { style: "thin" },
          right: { style: "thin" }
        };
        lcomunNow.alignment = { vertical: "middle", horizontal: "center" };
      }
      lcomunNow.height = 25;
      lcomunNow.commit();
      if (i < sl) lcomun++;
    }
  }
  /**
   *  合并操作
   * @param {*} satarLcomun
   * @param {*} endLcomun
   * @param {*} starKey
   * @param {*} endKey
   */
  function setMergeLcomun(satarLcomun, endLcomun, starKey, endKey) {
    for (let ml = 0; ml < long; ml++) {
      if (ml < starKey || ml >= endKey) {
        worksheet.mergeCells(
          getLetter(ml) + satarLcomun + ":" + getLetter(ml) + endLcomun
        );
      }
    }
  }

  /**
   * 設(shè)置表末尾統(tǒng)計(jì)備注等
   * @param {*} footData
   */
  function setFoot(footData) {
    if (isEmpty(footData)) return;
    if (Array.isArray(footData)) {
      for (let f = 0; f < footData.length; f++) {
        let lcomunNow = worksheet.getRow(lcomun);
        lcomunNow.getCell(1).value = footData[f];
        lcomunNow.getCell(1).border = {
          top: { style: "thin" },
          left: { style: "thin" },
          bottom: { style: "thin" },
          right: { style: "thin" }
        };
        lcomunNow.alignment = { vertical: "middle", horizontal: "left" };
        worksheet.mergeCells("A" + lcomun + ":" + getLetter(long - 1) + lcomun);
        lcomun++;
      }
    } else {
      let lcomunNow = worksheet.getRow(lcomun);
      lcomunNow.getCell(1).value = footData[f];
      lcomunNow.getCell(1).border = {
        top: { style: "thin" },
        left: { style: "thin" },
        bottom: { style: "thin" },
        right: { style: "thin" }
      };
      lcomunNow.alignment = { vertical: "middle", horizontal: "left" };
      worksheet.mergeCells("A" + lcomun + ":" + getLetter(long - 1) + lcomun);
    }
  }

  /**
   *  處理超過(guò)26個(gè)字母的列
   * @param {*} number
   * @returns
   */
  function getLetter(number) {
    if (number < 26) {
      return letter[number];
    } else {
      let n = number % 26;
      let l = Math.floor(number % 26);
      return letter[l] + letter[n];
    }
  }

  /**
   *  導(dǎo)出下載
   * @param {*} filename
   */
  function saveAndDowloade(filename) {
    if (!filename) filename = new Date().getTime();
    workBook.xlsx.writeBuffer().then(data => {
      const blob = new Blob([data], { type: "application/octet-stream" });
      FileSaver.saveAs(blob, filename + ".xlsx");
    });
  }

  createSheets(sheets);
  setTitle(title, long);
  setHeader(header);
  setContent(data);
  setFoot(foot);
  saveAndDowloade(filename);
}

數(shù)據(jù)處理

import createWorkBook from './newToExcel.js';

/**
 * @description 導(dǎo)出數(shù)據(jù)接口封裝
 * @param fn 請(qǐng)求數(shù)據(jù)的接口名
 * @param params 下載的其他參數(shù)
 */
export const exportExcel = async (fn, params) => {
  const excelData = {
    page: 1,
    limit: 100,
    ...params,
  };
   let data = [],
    lebData = {};
  for (let i = 1; i < excelData.page + 1; i++) {
    lebData = await getExcelData(fn, excelData);
    if (lebData.export.length) {
      data = data.concat(lebData.export);
      if (lebData.export.length >= excelData.limit) excelData.page++;
    }
  }
  createWorkBook(lebData.header, lebData.filename, data, '', lebData.filename);
}

const getExcelData = (fn, excelData) =>  {
  return new Promise((resolve, reject) => {
    fn(excelData).then((res) => {
      resolve(res.data);
    });
  });
}

以上代碼可以看到,每次請(qǐng)求100條數(shù)據(jù),進(jìn)行組裝后使用createWorkBook生成excel。再頁(yè)面上直接使用exportExcel方法,傳入接口名稱(chēng)接口參數(shù)即可

返回的數(shù)據(jù)結(jié)構(gòu)可直接使用

data :{
filename: '文件名',
header: ['用戶(hù)編號(hào)','用戶(hù)名'], // 表頭數(shù)組
export:[
['1', '張三'],
['2', '李四']
]
}
  • 第二種:使用xlsx ,大同小異,就不列出代碼了
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容