在前端开发中,导出 Excel 是一项非常常见的需求。传统方式通常由后端生成 Excel 文件,但这种方式存在明显的缺点:增加服务器压力、网络传输耗时、无法实时预览数据。随着 JavaScript 生态的发展,我们完全可以借助优秀的开源库在前端直接完成 Excel 的生成与导出工作。本文将基于实际项目代码,详细介绍如何使用 xlsx.js 插件以及如何编写自定义导出逻辑,实现纯前端导出功能。
一、为什么选择前端导出 Excel
前端导出 Excel 相比后端导出具有显著优势。首先是减轻服务器压力,Excel 生成是计算密集型操作,放在前端执行可以节省服务器资源。其次是提升用户体验,用户点击导出按钮后,数据已经存在于前端,无需等待服务器处理和文件下载的过程。第三是实现灵活性,前端可以轻松实现复杂的表头合并、单元格样式定制、冻结窗格等功能。最后是架构简化,后端无需维护专门的 Excel 生成接口,前端可以复用已有的 RESTful API。
xlsx.js(SheetJS)是目前最流行的前端 Excel 操作库,它支持读取和写入多种电子表格格式,功能强大且稳定。本文示例使用的是 0.18.2 版本,通过 CDN 引入即可使用。
二、基础用法:纯 JavaScript 实现 Excel 导出
让我们从最简单的例子开始,了解前端导出 Excel 的核心流程。首先需要引入 xlsx.js 库:
<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/xlsx/0.18.2/xlsx.full.min.js"></script>
然后定义导出的数据结构和表头配置。以下是一个简单的示例,展示如何导出包含嵌套结构的表格数据:
// 模拟导出数据,包含子元素
var selectionData = [
{
name: '张三',
js: '熟练',
css: '一般',
child: [
{ js2: '张三', css2: '熟练', mycss: '一般' },
{ js2: '张三2', css2: '熟练', mycss: '一般' }
]
}
];
// 定义表头结构,支持多级嵌套
var revealList = [
{
name: '姓名',
prop: 'name'
},
{
name: '专业技能',
child: [
{
name: '前端',
prop: 'js'
},
{
name: '后端',
prop: 'css'
},
{
name: '后端2',
child: [
{ name: '前端2', prop: 'js2' },
{ name: '后端2', prop: 'css2' },
{ name: '后端3', prop: 'mycss' }
]
}
]
}
];
核心的导出函数 exportExcel 完成了以下工作:构建 Excel 表头、提取数据、计算单元格合并、生成工作簿、触发下载。
function exportExcel() {
if (!Array.isArray(selectionData) || selectionData.length < 1) {
return;
}
let sheetName = '数据导出';
// 构建 Excel 表头
let excelHeader = buildHeader(revealList);
let headerRows = excelHeader.length;
// 提取数据
let dataList = extractData(selectionData, revealList);
excelHeader.push(...dataList, []);
// 计算合并单元格
let merges = doMerges(excelHeader);
// 生成 Sheet
let ws = aoa_to_sheet(excelHeader, headerRows);
// 设置单元格合并
ws['!merges'] = merges;
// 设置头部冻结
ws["!freeze"] = {
xSplit: "1",
ySplit: "" + headerRows,
topLeftCell: "B" + (headerRows + 1),
activePane: "bottomRight",
state: "frozen"
};
// 设置列宽
ws['!cols'] = [{ wpx: 165 }];
let workbook = {
SheetNames: [sheetName],
Sheets: {}
};
workbook.Sheets[sheetName] = ws;
let wopts = {
bookType: 'xlsx',
bookSST: false,
type: 'binary',
cellStyles: true
};
let wbout = XLSX.write(workbook, wopts);
let blob = new Blob([s2ab(wbout)], { type: "application/octet-stream" });
openDownloadXLSXDialog(blob, sheetName + '.xlsx');
}
下载功能的实现使用了 HTML5 的 Blob 和 URL.createObjectURL API,创建一个隐藏的 <a> 标签并模拟点击事件:
function openDownloadXLSXDialog(url, saveName) {
if (typeof url == 'object' && url instanceof Blob) {
url = URL.createObjectURL(url);
}
var aLink = document.createElement('a');
aLink.href = url;
aLink.download = saveName || '';
var event;
if (window.MouseEvent) {
event = new MouseEvent('click');
} else {
event = document.createEvent('MouseEvents');
event.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false,
false, false, false, 0, null);
}
aLink.dispatchEvent(event);
}
三、表头构建:多级嵌套表头的实现
在前端导出 Excel 时,复杂的表头结构是常见的需求。例如,一个项目信息表格可能包含“项目基本信息”、“保证金缴纳信息”、“保证金支付信息”等多个分组,每个分组下又有多个具体字段。这就是多级表头的应用场景。
表头配置使用嵌套的 JavaScript 对象结构,通过 child 属性表示子层级,通过 prop 属性绑定数据字段。顶层配置只需要 name 表示列名,二级及以下层级需要同时指定 name 和 prop。
构建多级表头的核心算法采用了递归思想,通过 getHeader 函数遍历配置的嵌套结构,生成二维数组形式的表头:
function buildHeader(revealList) {
let excelHeader = [];
getHeader(revealList, excelHeader, 0, 0);
return excelHeader;
}
function getHeader(headers, excelHeader, deep, perOffset) {
let offset = 0;
let cur = excelHeader[deep];
if (!cur) {
cur = excelHeader[deep] = [];
}
// 填充行合并占位符
pushRowSpanPlaceHolder(cur, perOffset - cur.length);
for (let i = 0; i < headers.length; i++) {
let head = headers[i];
cur.push(head.name);
if (head.hasOwnProperty('child') && Array.isArray(head.child)
&& head.child.length > 0) {
// 递归处理子层级
let childOffset = getHeader(head.child, excelHeader, deep + 1,
cur.length - 1);
// 填充列合并占位符
pushColSpanPlaceHolder(cur, childOffset - 1);
offset += childOffset;
} else {
offset++;
}
}
return offset;
}
在构建表头的过程中,使用了占位符来标记需要合并的单元格。!$ROW_SPAN_PLACEHOLDER 表示该单元格需要与上一行合并,!$COL_SPAN_PLACEHOLDER 表示该单元格需要与左侧单元格合并。
四、数据提取:处理嵌套数据结构
实际项目中的数据往往具有复杂的嵌套结构,比如一个项目可能包含多条支付记录、退还记录等二级数据,每条支付记录下又可能包含多个支付详情。处理这种嵌套数据是前端导出的难点之一。
extractData 函数负责将嵌套的原始数据转换为适合 Excel 导出的平面结构,它首先将配置的所有字段扁平化,然后通过 flatData 函数处理嵌套关系。
function extractData(selectionData, revealList) {
let headerList = flat(revealList); // 扁平化表头配置
let excelRows = [];
let dataKeys = new Set(Object.keys(selectionData[0]));
// 识别子层级字段,从父级字段中排除
selectionData.some(e => {
if (e.child && e.child.length > 0) {
let childKeys = Object.keys(e.child[0]);
for (let i = 0; i < childKeys.length; i++) {
dataKeys.delete(childKeys[i]);
}
return true;
}
});
flatData(selectionData, (list) => {
excelRows.push(...buildExcelRow(dataKeys, headerList, list));
});
return excelRows;
}
flatData 函数是处理嵌套数据的关键,它根据数据的 child 属性判断是否存在子级,将父级数据与子级数据进行横向合并,并为每条记录添加 rowSpan 标记,用于后续的单元格合并计算。
function flatData(list, eachDataCallBack) {
let resultList = [];
for (let i = 0; i < list.length; i++) {
let data = list[i];
let rawDataList = [];
if (data.child && data.child.length > 0) {
// 存在子级数据,需要展开
for (let j = 0; j < data.child.length; j++) {
let copy = Object.assign({}, data, data.child[j]);
// rowSpan = 2 表示首行,rowSpan = 3 表示后续行
copy['rowSpan'] = (j == 0 ? 2 : 3);
rawDataList.push(copy);
}
} else {
data['rowSpan'] = 1;
rawDataList.push(data);
}
resultList.push(...rawDataList);
if (typeof eachDataCallBack === 'function') {
eachDataCallBack(rawDataList);
}
}
return resultList;
}
对于更复杂的三级嵌套结构(如项目 -> 支付记录 -> 支付详情),flatData 函数需要进行三层级的递归处理,每一层级都需要计算 rowSpan 值来确定单元格合并关系。
五、单元格合并计算
Excel 导出中的单元格合并是一个重要功能,它能够让表格更加美观和易读。doMerges 函数负责计算所有需要合并的单元格区域:
function doMerges(arr) {
let deep = arr.length;
let merges = [];
// 先处理横向合并(列合并)
for (let y = 0; y < deep; y++) {
let row = arr[y];
let colSpan = 0;
for (let x = 0; x < row.length; x++) {
if (row[x] === '!$COL_SPAN_PLACEHOLDER') {
row[x] = undefined;
if(x + 1 === row.length){
merges.push({ s: { r: y, c: x - colSpan - 1 }, e: { r: y, c: x } });
}
colSpan++;
} else if (colSpan > 0 && x > colSpan) {
merges.push({ s: { r: y, c: x - colSpan - 1 }, e: { r: y, c: x - 1 } });
colSpan = 0;
} else {
colSpan = 0;
}
}
}
// 再处理纵向合并(行合并)
let colLength = arr[0].length;
for (let x = 0; x < colLength; x++) {
let rowSpan = 0;
for (let y = 0; y < deep; y++) {
if (arr[y][x] === '!$ROW_SPAN_PLACEHOLDER') {
arr[y][x] = undefined;
if(y + 1 === deep){
merges.push({ s: { r: y - rowSpan, c: x }, e: { r: y, c: x } });
}
rowSpan++;
} else if (rowSpan > 0 && y > rowSpan) {
merges.push({ s: { r: y - rowSpan - 1, c: x }, e: { r: y - 1, c: x } });
rowSpan = 0;
} else {
rowSpan = 0;
}
}
}
return merges;
}
合并区域的表示使用 Excel 的单元格坐标,格式为 { s: { r: 起始行, c: 起始列 }, e: { r: 结束行, c: 结束列 } },其中行和列的索引都是从 0 开始的数字。
六、完整企业级示例:AJAX 数据源与复杂表头
在实际的企业级应用中,导出功能通常需要从后端 API 获取数据,并处理更加复杂的业务逻辑。以下是一个完整的示例,展示如何从 API 获取数据并导出为包含多级表头的 Excel 文件。
首先在 HTML 中定义导出按钮:
<button onclick="exportSummaryOfStaticsExcel()">导出保证金汇总统计</button>
<button onclick="exportPaidItemsExcel()">导出已缴纳保证金项目</button>
然后定义 API 请求和表头配置:
var baseUrl = "http://localhost:5000";
// 导出保证金汇总统计
function exportSummaryOfStaticsExcel() {
let requestData = JSON.stringify({
city: "",
area: "",
itemCategory: 0,
itemStatus: 0
});
// 定义复杂的多级表头
let revealList = [
{
name: "项目明细",
child: [
{ name: "序号", prop: "serialNo" },
{ name: "区域", prop: "displayName" },
{ name: "项目总数", prop: "itemTotalNumber" },
{ name: "已缴纳保证金项目数", prop: "PaidItemCountSummary" },
{ name: "未缴纳保证金项目数", prop: "unPaidItemCount" },
{ name: "保证金落实率", prop: "implementationRate" }
]
},
{
name: "保证金缴纳类别",
prop: "paymentDepositType"
},
{
name: "落实详情",
child: [
{ name: "缴纳项目数", prop: "paidItemCount" },
{ name: "累计缴纳金额", prop: "totalPaidAmount" }
]
},
{
name: "使用详情",
child: [
{ name: "使用项目数", prop: "usedItemCount" },
{ name: "累计使用金额", prop: "totalUsedItemAmount" }
]
},
{
name: "退还详情",
child: [
{ name: "退还项目数", prop: "returnedItemCount" },
{ name: "退还总金额(元)", prop: "returnedAmount" }
]
},
{
name: "保证金剩余详情",
child: [
{ name: "保证金有效项目数", prop: "bjzValidItemCount" },
{ name: "保证金总余额(元)", prop: "totalBzjAmout" }
]
}
];
$.ajax({
url: baseUrl + "/api/BEBzjStatistics/GetSummaryOfStatics",
type: "post",
data: requestData,
contentType: "application/json",
success: function (res) {
let selectionData = res.data.list.items;
let sheetName = "保证金汇总统计";
exportExcel(selectionData, revealList, sheetName);
},
error: function (y) {}
});
}
这个示例展示了企业级应用的典型场景:后端 API 返回的复杂嵌套数据、多级分组的表头结构、包含文件路径的字段处理等。
七、Vue 3 + Element UI 集成示例
如果项目使用的是 Vue 3 框架,可以将导出功能封装为 Vue 组件的方法。以下是结合 Element UI 表格的完整示例:
<div id="app">
<el-button @click="exportData">导出</el-button>
<el-table @selection-change="handleSelectionChange" :data="list" style="width: 100%">
<el-table-column type="selection" width="55"></el-table-column>
<el-table-column label="姓名" prop="name" align="center"></el-table-column>
<el-table-column label="专业技能" align="center">
<el-table-column label="前端" align="center">
<el-table-column label="JavaScript" prop="js" align="center"></el-table-column>
<el-table-column label="CSS" prop="css" align="center"></el-table-column>
</el-table-column>
<el-table-column label="后端" align="center">
<el-table-column label="java" align="center">
<el-table-column label="nio" prop="nio" align="center"></el-table-column>
<el-table-column label="基础" prop="basic" align="center"></el-table-column>
</el-table-column>
</el-table-column>
</el-table-column>
</el-table>
</div>
Vue 组件中的导出方法实现与纯 JavaScript 版本类似,主要区别在于使用 this 访问组件的数据和方法:
const app = new Vue({
el: "#app",
data() {
return {
selectionData: [],
list: [
{ name: '张三', js: '熟练', css: '一般', nio: '了解', basic: '精通' }
],
revealList: [
{ name: '姓名', prop: 'name' },
{
name: '专业技能',
child: [
{
name: '前端',
child: [
{ name: 'JavaScript', prop: 'js' },
{ name: 'CSS', prop: 'css' }
]
},
{
name: '后端',
child: [
{ name: 'java', prop: 'nio' }
]
}
]
}
]
}
},
methods: {
handleSelectionChange(selection) {
this.selectionData = selection;
},
exportData() {
if (!Array.isArray(this.selectionData) || this.selectionData.length < 1) {
this.$message({ type: 'error', message: '请选择需要导出的数据!' });
return;
}
let sheetName = '数据导出';
let excelHeader = this.buildHeader(this.revealList);
let headerRows = excelHeader.length;
let dataList = this.extractData(this.selectionData, this.revealList);
excelHeader.push(...dataList, []);
let merges = this.doMerges(excelHeader);
let ws = this.aoa_to_sheet(excelHeader, headerRows);
ws['!merges'] = merges;
let workbook = {
SheetNames: [sheetName],
Sheets: {}
};
workbook.Sheets[sheetName] = ws;
let wopts = {
bookType: 'xlsx',
bookSST: false,
type: 'binary',
cellStyles: true
};
let wbout = XLSX.write(workbook, wopts);
let blob = new Blob([this.s2ab(wbout)], { type: "application/octet-stream" });
this.openDownloadXLSXDialog(blob, sheetName + '.xlsx');
}
}
});
八、Excel 样式定制
为了让导出的 Excel 文件更加美观,可以对单元格的字体、边框、背景色、对齐方式等样式进行定制。aoa_to_sheet 函数中包含了样式定义的核心逻辑:
function aoa_to_sheet(data, headerRows) {
const ws = {};
const range = { s: { c: 10000000, r: 10000000 }, e: { c: 0, r: 0 } };
for (let R = 0; R !== data.length; ++R) {
for (let C = 0; C !== data[R].length; ++C) {
// 更新数据范围
if (range.s.r > R) range.s.r = R;
if (range.s.c > C) range.s.c = C;
if (range.e.r < R) range.e.r = R;
if (range.e.c < C) range.e.c = C;
// 定义单元格样式
const cell = {
v: data[R][C] || '',
s: {
font: { name: "宋体", sz: 11, color: { auto: 1 } },
alignment: {
wrapText: 1, // 自动换行
horizontal: "center", // 水平居中
vertical: "center" // 垂直居中
}
}
};
// 表头样式:添加边框和背景色
if (R < headerRows) {
cell.s.border = {
top: { style: 'thin', color: { rgb: "000000" } },
left: { style: 'thin', color: { rgb: "000000" } },
bottom: { style: 'thin', color: { rgb: "000000" } },
right: { style: 'thin', color: { rgb: "000000" } }
};
cell.s.fill = {
patternType: 'solid',
fgColor: { rgb: 'DDD9C4' },
bgColor: { rgb: '8064A2' }
};
}
// 设置单元格类型
const cell_ref = XLSX.utils.encode_cell({ c: C, r: R });
if (typeof cell.v === 'number') {
cell.t = 'n';
} else if (typeof cell.v === 'boolean') {
cell.t = 'b';
} else {
cell.t = 's';
}
ws[cell_ref] = cell;
}
}
if (range.s.c < 10000000) {
ws['!ref'] = XLSX.utils.encode_range(range);
}
return ws;
}
通过修改 cell.s 对象中的属性,可以自定义字体(font)、边框(border)、填充色(fill)、对齐方式(alignment)等样式。表头行可以通过判断行索引 R < headerRows 来应用特殊的样式。
九、常见问题与解决方案
在前端导出 Excel 的实践中,可能会遇到一些常见问题。首先是数据量过大导致的性能问题,当导出数万条数据时,JavaScript 的处理速度可能变慢。解决方案是分页导出或者使用 Web Worker 在后台线程处理数据。其次是中文文件名的乱码问题,在某些浏览器中,直接使用中文文件名可能导致下载时出现乱码,可以对文件名进行 URL 编码处理。第三是单元格内容过长导致显示不全,需要开启自动换行功能并适当调整列宽。
对于包含文件路径的字段,需要特殊处理。例如,后端返回的附件路径可能是多个路径用分隔符连接的字符串,需要解析后拼接成完整的下载链接:
function buildFilePath(fileNames, header, rawData) {
let value = "";
if (fileNames.indexOf(header.prop) > -1) {
var originFilePath = rawData[header.prop];
var files = originFilePath.split("?");
let finalFilePaths = "";
for (let j = 0; j < files.length; j++) {
finalFilePaths += baseUrl + "/" + files[j] + "\n";
}
value = finalFilePaths;
}
return value;
}
十、总结
前端导出 Excel 是一项非常实用的技术,能够显著提升用户体验并减轻服务器压力。通过 xlsx.js 库,我们可以轻松实现各种复杂的导出需求,包括多级表头、单元格合并、样式定制等功能。
本文详细介绍了从基础到高级的前端 Excel 导出方案,包括纯 JavaScript 实现、Vue 3 集成、企业级 AJAX 数据处理等场景。核心要点包括:使用嵌套对象配置定义多级表头、递归算法构建表头和数据、处理嵌套数据结构实现单元格合并、通过样式定制提升导出文件的美观度。
掌握这些技术后,前端开发者完全可以独立完成各种复杂的 Excel 导出需求,无需依赖后端实现。