1. 前言:为什么选择纯前端导出Excel?
在Web应用中,导出Excel表格是常见的需求。传统方案依赖后端生成文件并返回,但存在以下痛点:
- 服务器压力大:每次导出都需要后端处理,消耗服务器资源。
- 响应延迟:数据需要往返服务器,用户体验不佳。
- 依赖网络:网络不稳定时导出可能失败。
纯前端导出方案(如使用xlsx.js)完美解决了这些问题:
- 完全在浏览器中完成:数据生成、格式处理、文件下载均在客户端进行。
- 零服务器依赖:减轻后端负担,提升系统扩展性。
- 即时响应:用户点击后立即下载,体验流畅。
本文将详细介绍如何使用xlsx.js实现功能丰富的Excel导出,涵盖基础导出、合并单元格、样式设置及其他常用高级功能。
2. 环境准备与xlsx.js引入
2.1 安装xlsx.js
有多种方式引入xlsx.js库:
xlsx基础库不提供样式设置,可以使用xlsx-js-style库,这个库是xlsx库的fork版本,并添加了样式定义
详细样式可以查看https://github.com/gitbrent/xlsx-js-style/
方式一:CDN引入(推荐初学者)
html
<!-- 在HTML文件中引入 -->
<script src="https://unpkg.com/xlsx/dist/xlsx.full.min.js"></script>
方式二:NPM安装(适用于现代前端项目)
bash
# 在项目目录中执行
npm install xlsx
# 或使用yarn
yarn add xlsx
javascript
// 在JavaScript/TypeScript文件中导入
import * as XLSX from 'xlsx';
// 或按需导入
import { utils, writeFile } from 'xlsx';
2.2 基础HTML结构
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>前端Excel导出示例</title>
<script src="https://unpkg.com/xlsx/dist/xlsx.full.min.js"></script>
</head>
<body>
<button id="exportBtn">导出Excel</button>
<script src="app.js"></script>
</body>
</html>
3. 基础导出:从数据到Excel文件
3.1 最简单的二维数组导出
javascript
// 示例数据:二维数组
const data = [
['姓名', '年龄', '城市', '入职日期'],
['张三', 28, '北京', '2023-01-15'],
['李四', 32, '上海', '2022-08-22'],
['王五', 25, '广州', '2024-03-10']
];
function exportSimpleExcel() {
// 1. 创建工作簿
const wb = XLSX.utils.book_new();
// 2. 创建工作表
const ws = XLSX.utils.aoa_to_sheet(data);
// 3. 将工作表添加到工作簿
XLSX.utils.book_append_sheet(wb, ws, '员工信息');
// 4. 生成Excel文件并下载
XLSX.writeFile(wb, '员工信息表.xlsx');
}
// 绑定按钮点击事件
document.getElementById('exportBtn').addEventListener('click', exportSimpleExcel);
3.2 从JSON对象数组导出(更常用)
javascript
const employees = [
{ id: 1, name: '张三', department: '技术部', salary: 15000, joinDate: '2023-01-15' },
{ id: 2, name: '李四', department: '市场部', salary: 12000, joinDate: '2022-08-22' },
{ id: 3, name: '王五', department: '人事部', salary: 10000, joinDate: '2024-03-10' }
];
function exportJsonExcel() {
// 1. 将JSON数组转换为工作表
const ws = XLSX.utils.json_to_sheet(employees);
// 2. 创建工作簿并添加工作表
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, '员工数据');
// 3. 导出文件
XLSX.writeFile(wb, '员工数据表.xlsx');
}
3.3 设置表头(自定义列顺序和标题)
javascript
function exportWithCustomHeaders() {
// 定义表头映射:对象属性 -> 显示标题
const headerMap = [
{ key: 'name', title: '姓名' },
{ key: 'department', title: '部门' },
{ key: 'salary', title: '薪资(元)' },
{ key: 'joinDate', title: '入职日期' },
];
// 1. 提取表头标题
const headers = headerMap.map((item) => item.title);
// 2. 提取数据行(按映射顺序)
const rows = employees.map((emp) => headerMap.map((item) => emp[item.key]));
// 3. 合并表头和数据
const sheetData = [headers, ...rows];
// 4. 创建并导出
const ws = XLSX.utils.aoa_to_sheet(sheetData);
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, '自定义表头');
XLSX.writeFile(wb, '自定义表头.xlsx');
}
4. 合并单元格实战示例
4.1 基础合并:合并表头区域
javascript
function exportWithMergedHeader() {
const data = [
['员工信息统计表'], // 第一行:标题
[], // 第二行:空行(用于样式)
['姓名', '年龄', '部门', '薪资'],
];
const ws = XLSX.utils.aoa_to_sheet(data);
// 关键:设置合并单元格
// 语法:ws['!merges'] = [{ s: {r: 开始行, c: 开始列}, e: {r: 结束行, c: 结束列} }]
ws['!merges'] = [
// 合并第一行的A-D列作为标题
{ s: { r: 0, c: 0 }, e: { r: 0, c: 3 } },
// 合并第二行的A-D列作为空行(用于样式间隔)
{ s: { r: 1, c: 0 }, e: { r: 1, c: 3 } },
];
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, '合并表头示例');
XLSX.writeFile(wb, '合并表头.xlsx');
}
4.2 复杂合并:分组数据合并
javascript
function exportGroupedData() {
// 模拟分组数据:按部门分组
const data = [
['部门', '姓名', '职位', '工龄'],
['技术部', '张三', '前端工程师', 3],
['', '李四', '后端工程师', 5], // 部门列留空,准备合并
['', '王五', '测试工程师', 2],
['市场部', '赵六', '市场专员', 4],
['', '钱七', '销售经理', 6],
['人事部', '孙八', 'HR专员', 2],
];
const ws = XLSX.utils.aoa_to_sheet(data);
// 设置合并:将相同部门的单元格合并
ws['!merges'] = [
// 技术部:A2-A4(行索引从0开始)
{ s: { r: 1, c: 0 }, e: { r: 3, c: 0 } },
// 市场部:A5-A6
{ s: { r: 4, c: 0 }, e: { r: 5, c: 0 } },
// 人事部:A7单独一行,不需要合并
];
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, '分组合并示例');
XLSX.writeFile(wb, '分组合并.xlsx');
}
4.3 动态合并:根据数据自动合并
javascript
function exportDynamicMerged() {
const salesData = [
['月份', '产品', '销售额', '增长率'],
['一月', '产品A', 150000, '15%'],
['', '产品B', 120000, '8%'],
['', '产品C', 180000, '22%'],
['二月', '产品A', 165000, '10%'],
['', '产品B', 130000, '8%'],
['', '产品C', 200000, '11%'],
['三月', '产品A', 180000, '9%'],
['', '产品B', 140000, '8%'],
['', '产品C', 220000, '10%'],
];
const ws = XLSX.utils.aoa_to_sheet(salesData);
const merges = [];
let startRow = 1; // 数据从第2行开始(索引1)
// 动态计算合并区域
for (let i = 1; i < salesData.length; i++) {
if (salesData[i][0] !== '') {
// 月份不为空,是新月份的开始
if (i > startRow) {
// 合并上一个月份的月份列
merges.push({ s: { r: startRow, c: 0 }, e: { r: i - 1, c: 0 } });
}
startRow = i;
}
}
// 合并最后一个月份
if (startRow < salesData.length - 1) {
merges.push({
s: { r: startRow, c: 0 },
e: { r: salesData.length - 1, c: 0 },
});
}
ws['!merges'] = merges;
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, '动态合并示例');
XLSX.writeFile(wb, '动态合并.xlsx');
}
5. 单元格样式设置详解
5.1 基础样式:字体、颜色、对齐
javascript
function exportWithBasicStyles() {
const data = [
['样式演示表'],
['项目', '数值', '状态'],
['收入', 50000, '良好'],
['支出', 30000, '正常'],
['利润', 20000, '优秀'],
];
const ws = XLSX.utils.aoa_to_sheet(data);
// 设置单元格样式
// 单元格地址:A1, B2, C3 等
const cellStyles = {
// 标题样式:A1单元格
A1: {
s: {
font: { bold: true, sz: 16, color: { rgb: 'FF0000' } }, // 红色加粗16号字
alignment: { horizontal: 'center', vertical: 'center' },
fill: { fgColor: { rgb: 'FFFF00' } }, // 黄色背景
},
},
// 表头样式:A2:C2
A2: { s: { font: { bold: true }, alignment: { horizontal: 'center' } } },
B2: { s: { font: { bold: true }, alignment: { horizontal: 'center' } } },
C2: { s: { font: { bold: true }, alignment: { horizontal: 'center' } } },
// 数值列右对齐
B3: { s: { alignment: { horizontal: 'right' } } },
B4: { s: { alignment: { horizontal: 'right' } } },
B5: { s: { alignment: { horizontal: 'right' } } },
// 状态列根据值设置颜色
C3: { s: { font: { color: { rgb: '008000' } } } }, // 绿色
C4: { s: { font: { color: { rgb: '0000FF' } } } }, // 蓝色
C5: { s: { font: { color: { rgb: 'FFA500' } } } }, // 橙色
};
// 应用样式到工作表
Object.keys(cellStyles).forEach((cell) => {
if (!ws[cell]) ws[cell] = { v: '' };
ws[cell] = { ...ws[cell], ...cellStyles[cell] };
});
// 设置列宽
ws['!cols'] = [
{ wpx: 100 }, // A列宽度100像素
{ wpx: 80 }, // B列宽度80像素
{ wpx: 100 }, // C列宽度100像素
];
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, '基础样式');
XLSX.writeFile(wb, '基础样式.xlsx');
}
5.2 条件格式:根据数值设置样式
javascript
function exportWithConditionalFormatting() {
const scores = [
['学生', '语文', '数学', '英语', '总分'],
['张三', 85, 92, 78, 255],
['李四', 62, 75, 80, 217],
['王五', 95, 88, 92, 275],
['赵六', 70, 65, 72, 207],
];
const ws = XLSX.utils.aoa_to_sheet(scores);
// 遍历数据行(跳过表头)
for (let r = 1; r < scores.length; r++) {
const total = scores[r][4]; // 总分在E列(索引4)
// 根据总分设置行样式
let rowStyle = {};
if (total >= 250) {
rowStyle = {
fill: { fgColor: { rgb: 'C6EFCE' } }, // 浅绿色背景
font: { bold: true, color: { rgb: '006100' } },
};
} else if (total < 220) {
rowStyle = {
fill: { fgColor: { rgb: 'FFC7CE' } }, // 浅红色背景
font: { color: { rgb: '9C0006' } },
};
}
// 应用样式到整行
if (Object.keys(rowStyle).length > 0) {
['A', 'B', 'C', 'D', 'E'].forEach((col, cIndex) => {
const cellAddr = col + (r + 1); // Excel行号从1开始
if (!ws[cellAddr]) ws[cellAddr] = { v: scores[r][cIndex] };
ws[cellAddr].s = rowStyle;
});
}
}
const wb = XLSX.utils.book_new();
XLSX.utils.book_append_sheet(wb, ws, '条件格式');
XLSX.writeFile(wb, '条件格式.xlsx');
}
5.3 数字格式:货币、百分比、日期
javascript
function exportWithNumberFormat() {
const financialData = [
['项目', '金额', '增长率', '日期'],
['收入', 1500000, 0.15, new Date('2024-01-15')],
['成本', 800000, 0.08, new Date('2024-01-15')],
['利润', 700000, 0.25, new Date('2024-01-15')]
];
const ws = XLSX.utils.aoa_to_sheet(financialData);
// 设置数字格式
const formats = {
// B列:货币格式(人民币,千位分隔符,两位小数)
'B2': { s: { numFmt: '¥#,##0.00' } },
'B3': { s: { numFmt: '¥#,##0.00' } },
'B4': { s: { numFmt: '¥#,##0.00' } },
// C列:百分比
最终版整理了工具
javascript
import * as XLSX from 'xlsx-js-style'
export function exportExcel(excelName, data, columns) {
const newData = []
const keys = columns.map((item) => item['prop'])
const headers = columns.map((item) => item['label'])
data.forEach((item) => {
const row = []
for (let key of keys) {
row.push(item[key])
}
newData.push(row)
})
// 添加表头
newData.unshift(headers)
const workbook = XLSX.utils.book_new()
const worksheet = XLSX.utils.json_to_sheet(newData, {
skipHeader: true,
})
// 设置列宽
const columnsWidth = columns.map((item) => {
return {
wpx: item.width,
}
})
worksheet['!cols'] = columnsWidth
// 设置表头样式
columns.forEach((item, index) => {
if (item['excelStyle']) {
const cell = indexToLetters(index) + 1
worksheet[cell].s = item['excelStyle']
}
})
XLSX.utils.book_append_sheet(workbook, worksheet, 'Sheet1')
XLSX.writeFile(workbook, `${excelName}.xlsx`)
}
function indexToLetters(i) {
if (i >= 0 && i <= 25) {
return String.fromCharCode(65 + i)
} else {
return undefined
}
}

单元格样式示例
// STEP 1: Create a new workbook
const wb = XLSX.utils.book_new();
// STEP 2: Create data rows and styles
let row = [
{ v: "Courier: 24", t: "s", s: { font: { name: "Courier", sz: 24 } } },
{ v: "bold & color", t: "s", s: { font: { bold: true, color: { rgb: "FF0000" } } } },
{ v: "fill: color", t: "s", s: { fill: { fgColor: { rgb: "E9E9E9" } } } },
{ v: "line\nbreak", t: "s", s: { alignment: { wrapText: true } } },
];
// STEP 3: Create worksheet with rows; Add worksheet to workbook
const ws = XLSX.utils.aoa_to_sheet([row]);
XLSX.utils.book_append_sheet(wb, ws, "readme demo");
// STEP 4: Write Excel file to browser
XLSX.writeFile(wb, "xlsx-js-style-demo.xlsx");
单元格样式属性
- 单元格样式由一个大致与 OpenXML 结构对应的样式对象指定。
- 当前支持的样式属性包括:
alignment(对齐方式)、border(边框)、fill(填充)、font(字体)、numFmt(数字格式)。
| 样式属性 | 子属性 | 默认值 | 描述/取值 |
|---|---|---|---|
alignment |
vertical |
bottom |
"top"(顶部对齐)或 "center"(居中对齐)或 "bottom"(底部对齐) |
horizontal |
left |
"left"(左对齐)或 "center"(居中对齐)或 "right"(右对齐) |
|
wrapText |
false |
true(自动换行)或 false(不自动换行) |
|
textRotation |
0 |
0 到 180 之间的数值,或 255 // 180 表示向下旋转 180 度,255 为特殊值,表示垂直对齐 |
|
border |
top |
{ style: BORDER_STYLE, color: COLOR_STYLE }(上边框样式和颜色) |
|
bottom |
{ style: BORDER_STYLE, color: COLOR_STYLE }(下边框样式和颜色) |
||
left |
{ style: BORDER_STYLE, color: COLOR_STYLE }(左边框样式和颜色) |
||
right |
{ style: BORDER_STYLE, color: COLOR_STYLE }(右边框样式和颜色) |
||
diagonal |
{ style: BORDER_STYLE, color: COLOR_STYLE, diagonalUp: true/false, diagonalDown: true/false }(对角线样式、颜色及方向) |
||
fill |
patternType |
"none" |
"solid"(纯色填充)或 "none"(无填充) |
fgColor |
前景色:参见 COLOR_STYLE |
||
bgColor |
背景色:参见 COLOR_STYLE |
||
font |
bold |
false |
字体加粗 true(是)或 false(否) |
color |
字体颜色 COLOR_STYLE |
||
italic |
false |
字体斜体 true(是)或 false(否) |
|
name |
"Calibri" |
字体名称 | |
strike |
false |
字体删除线 true(是)或 false(否) |
|
sz |
"11" |
字体大小(磅) | |
underline |
false |
字体下划线 true(是)或 false(否) |
|
vertAlign |
"superscript"(上标)或 "subscript"(下标) |
||
numFmt |
0 |
示例:"0" // 内置格式的整数索引,参见 StyleBuilder.SSF 属性 |
|
示例:"0.00%" // 匹配内置格式的字符串,参见 StyleBuilder.SSF |
|||
示例:"0.0%" // 指定自定义格式的字符串 |
|||
示例:"0.00%;\\(0.00%\\);\\-;@" // 指定自定义格式的字符串,其中特殊字符需转义 |
|||
示例:"m/dd/yy" // 使用 Excel 格式表示法的日期格式字符串 |
COLOR_STYLE {对象} 属性
border、fill、font 的颜色通过一个名称/值对象来指定,可使用以下方式之一:
| 颜色属性 | 描述 | 示例 |
|---|---|---|
rgb |
十六进制 RGB 值 | {rgb: "FFCC00"} |
theme |
主题颜色索引 | {theme: 4} // (0-n) // 主题颜色索引 4("蓝色,强调色 1") |
tint |
色调百分比 | {theme: 1, tint: 0.4} // ("蓝色,强调色 1,浅 40%") |
BORDER_STYLE {字符串} 属性
边框样式属性为以下值之一:
dashDotDotdashDotdasheddottedhairmediumDashDotDotmediumDashDotmediumDashedmediumslantDashDotthickthin
边框说明
合并区域的边框需为合并区域内的每个单元格单独指定。例如,要为一个 3x3 单元格的合并区域应用方框边框,需要为八个不同的单元格指定边框样式:
- 左边框(左侧三个单元格)
- 右边框(右侧单元格)
- 上边框(顶部单元格)
- 下边框(底部单元格)