前提:利用接口导出一个文件,前端会遇到的几种情况。这里主要记录一下文件流太大的情况
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. 导出接口给的是有正常响应体,result
是oss地址
,那拿到地址后再直接去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
, 轮询拿到数据再去组装。将压力给到客户端
- 第一种:使用
exceljs
和file-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
,大同小异,就不列出代码了