接口导出文件下载的几种情况

前提:利用接口导出一个文件,前端会遇到的几种情况。这里主要记录一下文件流太大的情况

1. 导出接口直接给的文件流
  • 文件不大的时候,我们直接使用a标签交给浏览器来下载
function downOSSUrl(url) {
  const a = document.createElement('a')
  a.href = url
  a.rel = 'noreferrer'
  document.body.appendChild(a)
  a.click()
  a.remove()
}

  • 文件大的时候,直接使用以上方式就不太合适,接口处理太慢了,点击后页面没啥反应,还以为有bug。那就获取状态加个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(提示语)
         }
       }).catch(() => {
         this.exportLoading = false
       })

拦截一下axios。 下面的链接超时时间timeout以及nginx的超时时间我们都改大一些

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) // 创建下载的链接
      downloadElement.href = href
      downloadElement.download = filename // 下载后文件名
      document.body.appendChild(downloadElement)
      downloadElement.click() // 点击下载
      document.body.removeChild(downloadElement) // 下载完成移除元素
      window.URL.revokeObjectURL(href) // 释放掉blob对象
      return
    }
}
)

以上代码可以看到两个点

(1)response.data.size 长度为0 ,我这里直接返回0.
因为后端考虑到处理时间太长,机器负载问题,限制了一分钟内不能同时多次下载,直接抛错。当多次请求到后端时体现在接口返回内容上,流的长度就是0,此时提示用户:稍后再试
(2)当有信息的时候呢,那就是有两种情况。
一种是包含错误信息或者提示信息, 一种是真正的文件信息。上面代码只考虑了直接下载文件信息。
当和后端有约定要展示特定条件的提示信息时,可以增加以下判断。将流用 filereader解析程json

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

(3) 文件名从响应头中的content-disposition拿到,没有的话,要后端导出

其实相比于直接用a标签交给浏览器下载,这样的请求后下载的,是直接写入到浏览器内存的,所以可以看到整个请求也包含了download的时间,再到磁盘,才会在浏览器上看到进入了下载中心

2. 导出接口给的是有正常响应体,resultoss地址,那拿到地址后再直接去a标签下载即可
3. 接口处理时间太长,出现504 gateway time-out,很明显nginx太久没拿到服务端结果,给了前端504。 设置nginx代理响应时间

(1)上面有个axios 请求超时的时间,以毫秒为单位。设置大点,这里超时是主动断开了,但是后端还在处理。
(2)nginx 设置超时时间,单位是秒。可以设置在http全局模块,server模块,或者是location特定模块。

location /api {
proxy_read_timeout 300;   // 代理读取超时时间
proxy_send_timeout       300;    // 代理发送超时时间
proxy_connect_timeout       300;  // 代理连接超时时间
}

4. 由前端生成excel, 轮询拿到数据再去组装。将压力给到客户端

  • 第一种:使用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;

  /**
   *  创建工作薄
   * @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);
  }

  /**
   *  设置表名介绍等
   * @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++;
    }
  }
  /**
   *  设置表头行
   * @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++;
  }

  /**
   * 导出内容
   * @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;
      /** 循环列 */
      //需要操作第几列
      let sk = 0;
      for (let l = 0; l < data[h].length; l++) {
        if (Array.isArray(data[h][l])) {
          //数组长度
          starKey = sk;
          hasMerge = true;
          setArrayContent(data[h][l], sk);
          sk = sk + data[h][l][0].length;
          endKey = sk;
        } else {
          //不是数组
          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++;
    }
  }
  /**
   * 占多行的数组
   * @param {*} arr
   * @param {*} sk
   */
  function setArrayContent(arr, sk) {
    /**
     *  循环二维数组,在循环行
     */
    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
        );
      }
    }
  }

  /**
   * 设置表末尾统计备注等
   * @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);
    }
  }

  /**
   *  处理超过26个字母的列
   * @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];
    }
  }

  /**
   *  导出下载
   * @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);
}

数据处理

import createWorkBook from './newToExcel.js';

/**
 * @description 导出数据接口封装
 * @param fn 请求数据的接口名
 * @param params 下载的其他参数
 */
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);
    });
  });
}

以上代码可以看到,每次请求100条数据,进行组装后使用createWorkBook生成excel。再页面上直接使用exportExcel方法,传入接口名称接口参数即可

返回的数据结构可直接使用

data :{
filename: '文件名',
header: ['用户编号','用户名'], // 表头数组
export:[
['1', '张三'],
['2', '李四']
]
}
  • 第二种:使用xlsx ,大同小异,就不列出代码了
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容