前提:利用接口導(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)體,result是oss地址,那拿到地址后再直接去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ù)端
- 第一種:使用
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;
/**
* 創(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,大同小異,就不列出代碼了