纯前端通过xlsx.js导出Excel表格:完整示例与实战指南

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 0180 之间的数值,或 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 {对象} 属性

borderfillfont 的颜色通过一个名称/值对象来指定,可使用以下方式之一:

颜色属性 描述 示例
rgb 十六进制 RGB 值 {rgb: "FFCC00"}
theme 主题颜色索引 {theme: 4} // (0-n) // 主题颜色索引 4("蓝色,强调色 1")
tint 色调百分比 {theme: 1, tint: 0.4} // ("蓝色,强调色 1,浅 40%")

BORDER_STYLE {字符串} 属性

边框样式属性为以下值之一:

  • dashDotDot
  • dashDot
  • dashed
  • dotted
  • hair
  • mediumDashDotDot
  • mediumDashDot
  • mediumDashed
  • medium
  • slantDashDot
  • thick
  • thin

边框说明

合并区域的边框需为合并区域内的每个单元格单独指定。例如,要为一个 3x3 单元格的合并区域应用方框边框,需要为八个不同的单元格指定边框样式:

  • 左边框(左侧三个单元格)
  • 右边框(右侧单元格)
  • 上边框(顶部单元格)
  • 下边框(底部单元格)
相关推荐
光影少年1 小时前
RN的Fabric 渲染流程
运维·前端·javascript·react native·react.js·fabric
__zRainy__2 小时前
React开始:直接在网站中使用React.js
前端·react.js·前端框架
用户059540174463 小时前
大模型长记忆评测踩坑实录:2000条错位记忆,让我排查了整整3小时
前端·css
码林鼠3 小时前
webpack的基本配置
前端·webpack·node.js
用户938515635073 小时前
React Router 进阶:路由守卫、登录鉴权与状态传递
前端·javascript·全栈
kyriewen3 小时前
我重写了自己用了两年的防抖节流Hook——发现里面藏着3个隐藏bug
前端·javascript·面试
阿祖zu3 小时前
芝士就是力量!开源私有化部署与 GitHub 双向同步的个人知识笔记 App
前端·后端·ios
漂流瓶jz4 小时前
Webpack开发环境:观察模式/webpack-dev-server/HMR热更新
前端·javascript·webpack
IT_陈寒4 小时前
SpringBoot自动配置失效?这个隐藏配置坑了我一整晚
前端·人工智能·后端
小爬的老粉丝4 小时前
Vue 3 文件预览生产排障:Worker/WASM 404、鉴权 Blob 与子路径
javascript·vue.js·wasm