现代JavaScript高级小册
深入浅出Dart
现代TypeScript高级小册
前言
最近写管理端的需求,发现有一个excel导出的需求,本来是后端同学负责,但是因为他们太忙了,把这块任务交给前端了,起初产品觉得前端实现不了,一听这话,这我哪里受得了,赶紧写了个demo给她看,前端是可以实现的。enen,产品看了直夸牛逼
接下来,我来分享导出excel文件的三种实现方式
url下载
在这种方式中,我们的目标是后端生成Excel文件并提供一个地址,前端通过访问这个地址来下载导出的Excel文件。
- 后端根据前端的请求,生成需要导出的数据,并将数据转换为Excel格式的文件。
- 后端将生成的Excel文件保存到服务器的某个临时目录,并为该文件生成一个临时的访问地址。
- 后端将生成的临时地址返回给前端作为响应。
- 前端收到后端返回的地址后,可以通过创建一个隐藏的
<a>
标签,并设置其href
属性为后端返回的地址,然后触发点击该标签的操作,从而实现文件下载。 - 前端完成下载后,可以根据需求决定是否删除服务器上的临时文件。
// 后端接口:/api/export/excel
// 请求方式:GET
// 假设后端接口返回导出地址的数据格式为 { url: "https://example.com/excel_exports/exported_file.xlsx" }
export const exportExcelViaURL = () => {
// 发起后端接口请求获取导出地址
fetch('/api/export/excel')
.then((response) => response.json())
.then((data) => {
const { url } = data;
// 创建一个隐藏的<a>标签并设置href属性为后端返回的地址
const link = document.createElement('a');
link.href = url;
link.target = '_blank';
link.download = `exported_data_${dayjs().format('YYYY-MM-DD_hh.mm.ss_a')}.xlsx`;
// 触发点击操作,开始下载文件
link.click();
})
.catch((error) => {
console.error('导出Excel失败:', error);
});
};
Blob文件流
后端直接返回Blob文件流数据,前端通过接收到的Blob数据进行文件下载。
- 后端根据前端的请求,生成需要导出的数据,并将数据转换为Excel格式的文件。
- 后端将生成的Excel数据以Blob文件流的形式返回给前端,通常是通过设置响应的Content-Type和Content-Disposition头,使其以文件下载的方式呈现给用户。
- 前端通过接收到的Blob数据,可以创建一个Blob URL,然后创建一个隐藏的
<a>
标签,并将其href
属性设置为Blob URL,再触发点击该标签的操作,从而实现文件下载。
// 后端接口:/api/export/excel/blob
// 请求方式:GET
export const exportExcelViaBlob = () => {
// 发起后端接口请求获取Blob文件流数据
fetch('/api/export/excel/blob')
.then((response) => response.blob())
.then((blobData) => {
// 创建Blob URL
const blobUrl = URL.createObjectURL(blobData);
// 创建一个隐藏的<a>标签并设置href属性为Blob URL
const link = document.createElement('a');
link.href = blobUrl;
link.target = '_blank';
link.download = `exported_data_${dayjs().format('YYYY-MM-DD_hh.mm.ss_a')}.xlsx`;
// 触发点击操作,开始下载文件
link.click();
// 释放Blob URL
URL.revokeObjectURL(blobUrl);
})
.catch((error) => {
console.error('导出Excel失败:', error);
});
};
基于XLSX
XLSX是一款功能强大的JavaScript库,用于在浏览器和Node.js中读取、解析、处理和写入Excel文件。
1. 安装XLSX
首先,你需要在你的项目中安装XLSX库。你可以通过npm或yarn来安装:
npm install xlsx
或者
yarn add xlsx
2. 引入XLSX
在你的代码中,你需要引入XLSX库,以便使用其中的功能:
import * as XLSX from 'xlsx';
3. 读取Excel文件
使用XLSX库,你可以读取现有的Excel文件,提取其中的数据和元数据。例如,假设你有一个名为"data.xlsx"的Excel文件,你可以通过以下方式读取它:
import * as XLSX from 'xlsx';
const file = 'data.xlsx'; // 文件路径或URL
const workbook = XLSX.readFile(file);
const sheetName = workbook.SheetNames[0]; // 假设我们读取第一个工作表
const worksheet = workbook.Sheets[sheetName];
const data = XLSX.utils.sheet_to_json(worksheet, { header: 1 });
console.log(data);
4. 写入Excel文件
除了读取现有的Excel文件,XLSX库还允许你将数据写入到新的Excel文件中。例如,你可以将一个二维数组的数据写入到一个新的Excel文件:
import * as XLSX from 'xlsx';
const data = [
['Name', 'Age', 'City'],
['John Doe', 30, 'New York'],
['Jane Smith', 25, 'San Francisco'],
];
const worksheet = XLSX.utils.aoa_to_sheet(data);
const workbook = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1');
const fileName = 'output.xlsx'; // 导出的文件名
XLSX.writeFile(workbook, fileName);
项目实践
下面代码是我在项目中封装好的代码,有需要的同学直接copy使用就行
为了实现前述两种方式的前端导出Excel功能,我们将使用XLSX库来处理数据并导出Excel文件。
// 基于XLSX的前端导出Excel实现
import dayjs from 'dayjs';
import * as XLSX from 'xlsx';
/**
* eg: .columns = [
* { header: 'Id', key: 'id', wpx: 10 },
* { header: 'Name', key: 'name', wch: 32 },
* { header: 'D.O.B.', key: 'dob', width: 10, hidden: true }
* ]
* data: [{id: 1, name: 'John Doe', dob: new Date(1970,1,1)}]
* @param columns 定义列属性数组
* @param data 数据
* @param name 文件名
*/
export const generateExcel = (columns = [], data = [], name = '') => {
const headers = columns.map((item) => item.header);
// https://docs.sheetjs.com/docs/csf/features/#row-and-column-properties
const otherConfigs = columns.map(({ key, header, ...item }) => item);
const dataList = data.map((item) => {
let obj = {};
columns.forEach((col) => {
obj[col.header] = item[col.key];
});
return obj;
});
const workbook = XLSX.utils.book_new();
workbook.SheetNames.push(name);
const worksheet = XLSX.utils.json_to_sheet(dataList, {
header: headers,
});
worksheet['!cols'] = otherConfigs;
workbook.Sheets[name] = worksheet;
// 生成Blob数据
const excelData = XLSX.write(workbook, { type: 'array', bookType: 'xlsx' });
const blobData = new Blob([excelData], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
// 创建Blob URL
const blobUrl = URL.createObjectURL(blobData);
// 创建一个隐藏的<a>标签并设置href属性为Blob URL
const link = document.createElement('a');
link.href = blobUrl;
link.target = '_blank';
link.download = `${name}-${dayjs().format('YYYY-MM-DD_hh.mm.ss_a')}.xlsx`;
// 触发点击操作,开始下载文件
link.click();
// 释放Blob URL
URL.revokeObjectURL(blobUrl);
};
下载全部
我们可能需要一键下载所有表格的数据,这时候前端需要轮询后端的接口,拿到所有的数据,所以我们需要实现一个loopReuqest函数
export default function awaitRequest(limit = 5) {
let awaitTask = [];
let currentTaskNum = 0;
function run(event, ...args) {
return new Promise((resolve, reject) => {
function callbackEvent() {
currentTaskNum++;
event(...args)
.then((res) => {
if (awaitTask.length) {
const nextTask = awaitTask.shift();
nextTask();
}
resolve(res);
})
.catch((e) => {
console.error(e);
reject(e);
})
.finally(() => {
currentTaskNum--;
});
}
if (currentTaskNum >= limit) {
awaitTask.push(callbackEvent);
} else {
callbackEvent();
}
});
}
Object.defineProperties(run, {
clear: {
value: () => {
awaitTask = [];
},
},
});
return run;
}
/**
* 循环分页请求,获取全部数据
* @param {Function} request 请求
* @param {Number} size 页大小
* @param {Object} params 其余参数
* @param {String} listLabel.pageLable 当前页字段名。默认page
* @param {String} listLabel.sizeLabel 页大小字段名。默认page_size
* @param {String} listLabel.totalLabel 总条数字段名。默认total
* @param {String} listLabel.itemsLabel 数据列表字段名。默认list
* @returns
*/
export async function loopRequest(
request,
size,
params,
listLabel = {
totalLabel: 'total',
pageLable: 'page',
sizeLabel: 'page_size',
itemsLabel: 'list',
},
) {
const {
totalLabel = 'total',
pageLable = 'page',
sizeLabel = 'page_size',
itemsLabel = 'list'
} = listLabel;
try {
const firstRes = await request({
...params,
[sizeLabel]: size,
[pageLable]: 1,
});
let list = firstRes.data[itemsLabel] || [];
const total = firstRes.data[totalLabel];
if (total > size) {
const limit = awaitRequest();
const restRequest = Array.from({
length: Math.floor(total / size),
}).map((_, index) =>
limit(() =>
request({
...params,
[sizeLabel]: size,
[pageLable]: index + 2,
}),
),
);
const resetRes = await Promise.all(restRequest);
resetRes.forEach((res) => {
if (res.code === 0 && res.data[itemsLabel]) {
list.push(...res.data[itemsLabel]);
}
});
}
return list;
} catch (e) {
console.error(e);
}
return [];
}