「前端不求后端」手把手教你用 JS 导出 Excel,并精准控制单元格格式(附完整代码)

286 阅读6分钟

✅ 纯前端导出 Excel(XLSX + FileSaver)

技术栈xlsx + file-saver
优势:不依赖后端、支持样式、格式、自定义下载名
适用场景:Vue / React / 原生 JS 项目
关键词:前端导出 Excel、JavaScript、xlsx.js、单元格格式、数字格式、日期格式、样式设置、纯前端解决方案

📌 前言:为什么我要自己导出 Excel?

在日常开发中,你是否也遇到过这样的场景:

“这个表格数据能不能导出成 Excel?”
“导出的金额要带千分位和货币符号。”
“日期列要显示成 ‘2024-05-20’ 格式,别是时间戳!”
“表头要加粗、居中、背景色高亮!”

每次提需求给后端,对方一脸无奈:“又要改导出逻辑?又要加样式?前端不能自己搞吗?”

于是,我决定:前端,自己动手,丰衣足食!


📦 1. 安装依赖

npm install xlsx file-saver
  • xlsx:用于创建和格式化 Excel 文件
  • file-saver:用于在浏览器中保存文件(比 XLSX.writeFile 更可控)

📥 2. 引入模块

import * as XLSX from 'xlsx';
import { saveAs } from 'file-saver';

🧩 3. 准备数据和表头配置

const data = [
  { name: '张三', salary: 15000, joinDate: '2023-01-15', status: '在职' },
  { name: '李四', salary: 18000, joinDate: '2022-03-20', status: '离职' },
  { name: '王五', salary: 12000, joinDate: '2024-05-10', status: '在职' }
];

const headerConfig = [
  { label: '姓名', key: 'name', width: 15 },
  { label: '薪资', key: 'salary', key: 'salary', format: 'currency', width: 15 },
  { label: '入职日期', key: 'joinDate', format: 'date', width: 15 },
  { label: '状态', key: 'status', width: 10, align: 'center' }
];

🔧 4. 核心导出函数(使用 xlsx + file-saver)

function exportExcelWithFormat(data, headerConfig, filename = '导出数据.xlsx') {
  const headers = headerConfig.map(h => h.label);
  const keys = headerConfig.map(h => h.key);

  // 1. 构建二维数组:第一行为表头
  const wsData = [headers];
  data.forEach(row => {
    const rowData = keys.map(key => row[key]);
    wsData.push(rowData);
  });

  // 2. 创建工作表
  const ws = XLSX.utils.aoa_to_sheet(wsData);

  // 3. 设置列宽
  const colWidths = headerConfig.map(h => ({ wch: h.width || 12 }));
  ws['!cols'] = colWidths;

  // 4. 设置单元格格式(数字、日期等)
  data.forEach((row, rowIndex) => {
    const excelRow = rowIndex + 1; // Excel 行号从 1 开始

    headerConfig.forEach((h, colIndex) => {
      const cellRef = XLSX.utils.encode_cell({ c: colIndex, r: excelRow }); // 如 B2, C3
      const cell = ws[cellRef];
      if (!cell) return;

      // 可选:设置单元格样式(需启用 cellStyles)
      if (!cell.s) cell.s = {};
      cell.s = {
        ...cell.s,
        font: { sz: 11 },
        alignment: { horizontal: h.align || 'left' }
      };
    });
  });

  // 5. 设置表头样式
  const headerStyle = {
    font: { bold: true, color: { rgb: "FFFFFF" }, sz: 12 },
    fill: { fgColor: { rgb: "4472C4" } }, // 深蓝色背景
    alignment: { horizontal: "center", vertical: "center" }
  };

  headers.forEach((_, colIndex) => {
    const cellRef = XLSX.utils.encode_cell({ c: colIndex, r: 0 }); // 第一行
    ws[cellRef].s = headerStyle;
  });

  // 6. 创建工作簿
  const wb = XLSX.utils.book_new();
  XLSX.utils.book_append_sheet(wb, ws, '数据表');

  // 7. 生成二进制字符串(类型为 binary)
  const wbout = XLSX.write(wb, {
    bookType: 'xlsx',
    type: 'binary',           // 必须是 binary 或 array,不能是 base64
    cellStyles: true          // 启用样式支持
  });

  // 8. 转换为 Blob(FileSaver 需要 Blob)
  function s2ab(s) {
    const buf = new ArrayBuffer(s.length);
    const view = new Uint8Array(buf);
    for (let i = 0; i < s.length; i++) {
      view[i] = s.charCodeAt(i) & 0xFF;
    }
    return buf;
  }

  const blob = new Blob([s2ab(wbout)], {
    type: 'application/octet-stream'
  });

  // 9. 使用 FileSaver 下载
  saveAs(blob, filename);
}

🎯 5. 调用导出

exportExcelWithFormat(data, headerConfig, '员工信息表.xlsx');

🧠 关键点解析

问题解决方案
file-saver 无法直接使用 XLSX.writeFile改用 XLSX.write 生成二进制数据,再转为 Blob
样式不生效?必须设置 cellStyles: truetype: 'binary'
中文乱码?file-saverxlsx 都支持 UTF-8,一般无问题
s2ab 是什么?将 binary 字符串转为 ArrayBuffer,用于创建 Blob

📌 6. 为什么推荐 xlsx + file-saver

对比项XLSX.writeFilexlsx + file-saver
灵活性高(可自定义 Blob、类型、触发时机)
框架兼容性一般好(React/Vue 中更可控)
样式支持需配置同样需 cellStyles: true
是否需要额外库是(需 file-saver

✅ 推荐在 Vue/React 项目中使用 file-saver,更符合现代开发习惯。



✅ 总结

通过 xlsx + file-saver,我们可以:

  • 🔹 完全在前端生成带格式的 Excel
  • 🔹 精准控制 数字、日期、货币格式
  • 🔹 自定义 字体、颜色、对齐、背景色
  • 🔹 实现 跨框架通用 的导出能力

🎉 再也不用求后端!前端也能导出“专业级”Excel!


💾 附:完整代码供参考

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <title>前端导出Excel</title>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js"></script>
  <style>
    body {
      font-family: Arial, sans-serif;
      padding: 20px;
      background-color: #f5f5f5;
    }
    .container {
      max-width: 800px;
      margin: 0 auto;
      background-color: white;
      padding: 20px;
      border-radius: 8px;
      box-shadow: 0 2px 10px rgba(0,0,0,0.1);
    }
    h1 {
      text-align: center;
      color: #333;
    }
    .table-container {
      overflow-x: auto;
      margin-bottom: 20px;
    }
    table {
      width: 100%;
      border-collapse: collapse;
      margin-top: 10px;
    }
    th, td {
      border: 1px solid #ccc;
      padding: 8px;
      text-align: left;
    }
    th {
      background-color: #4472C4;
      color: white;
      font-weight: bold;
    }
    button {
      display: block;
      margin: 20px auto;
      padding: 10px 20px;
      font-size: 16px;
      background-color: #4CAF50;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
    button:hover {
      background-color: #45a049;
    }
  </style>
</head>
<body>

  <div class="container">
    <h1>前端导出Excel示例</h1>
    
    <div class="table-container">
      <table id="dataTable">
        <thead>
          <tr>
            <th>姓名</th>
            <th>薪资</th>
            <th>入职日期</th>
            <th>状态</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>张三</td>
            <td>15000</td>
            <td>2023-01-15</td>
            <td>在职</td>
          </tr>
          <tr>
            <td>李四</td>
            <td>18000</td>
            <td>2022-03-20</td>
            <td>离职</td>
          </tr>
          <tr>
            <td>王五</td>
            <td>12000</td>
            <td>2024-05-10</td>
            <td>在职</td>
          </tr>
        </tbody>
      </table>
    </div>

    <button id="exportBtn">导出为Excel</button>
  </div>

  <script>
    // 模拟数据源
    const data = [
      { name: '张三', salary: 15000, joinDate: '2023-01-15', status: '在职' },
      { name: '李四', salary: 18000, joinDate: '2022-03-20', status: '离职' },
      { name: '王五', salary: 12000, joinDate: '2024-05-10', status: '在职' }
    ];

    // 表头配置
    const headerConfig = [
      { label: '姓名', key: 'name', width: 15 },
      { label: '薪资', key: 'salary', format: 'currency', width: 15},
      { label: '入职日期', key: 'joinDate', format: 'date', width: 15 },
      { label: '状态', key: 'status', width: 15 }
    ];

    /**
     * 导出Excel核心函数
     */
    function exportExcelWithFormat(data, headerConfig, filename = '导出数据.xlsx') {
      const headers = headerConfig.map(h => h.label);
      const keys = headerConfig.map(h => h.key);

      // 构建二维数组
      const wsData = [headers];
      data.forEach(row => {
        const rowData = keys.map(key => row[key]);
        wsData.push(rowData);
      });

      // 创建工作表
      const ws = XLSX.utils.aoa_to_sheet(wsData);

      // 设置列宽
      const colWidths = headerConfig.map(h => ({ wch: h.width || 12 }));
      ws['!cols'] = colWidths;

      // 遍历表头列,设置格式和样式
      headerConfig.forEach((h, colIndex) => {
        const cellRef = XLSX.utils.encode_cell({ c: colIndex, r: 0 }); // A1, B1, C1...
        const cell = ws[cellRef];

        // 设置表头样式(加粗、背景色、居中)
        cell.s = {
          font: { bold: true, color: { rgb: "FFFFFF" }, sz: 12 },
          fill: { fgColor: { rgb: "4472C4" } },
          alignment: { horizontal: "center", vertical: "center" }
        };
      });

      data.forEach((row, rowIndex) => {
        const excelRow = rowIndex + 1; // 数据从第1行开始
        headerConfig.forEach((h, colIndex) => {
          const cellRef = XLSX.utils.encode_cell({ c: colIndex, r: excelRow });
          const cell = ws[cellRef];
          if (!cell) return;

          // 设置数字格式
          if (h.format === 'currency') {
			      cell.z = '#,##0.000_);[Red]\\(#,##0.000\\)';
          } else if (h.format === 'date') {
            // 将字符串转为 Date 对象
            const dateStr = row[h.key];
            if (dateStr) {
              const dateObj = new Date(dateStr);
              cell.v = "2025-10-22";
              cell.t = 'd';
              cell.z = 'yyyy-mm-dd';
            }
          }

          // 设置数据行样式
          if (!cell.s) cell.s = {};
          cell.s = {
            ...cell.s,
            font: { sz: 12 },
            alignment: { horizontal: h.align || 'right' }
          };
        });
      });

      // 创建工作簿
      const wb = XLSX.utils.book_new();
      XLSX.utils.book_append_sheet(wb, ws, '数据表');

      // 生成二进制字符串
      const wbout = XLSX.write(wb, {
        bookType: 'xlsx',
        type: 'binary',
        cellStyles: true // 启用样式支持
      });

      // 转换为 ArrayBuffer
      function s2ab(s) {
        const buf = new ArrayBuffer(s.length);
        const view = new Uint8Array(buf);
        for (let i = 0; i < s.length; i++) {
          view[i] = s.charCodeAt(i) & 0xFF;
        }
        return buf;
      }

      // 创建 Blob 并下载
      const blob = new Blob([s2ab(wbout)], {
        type: 'application/octet-stream'
      });
      saveAs(blob, filename);
    }

    // 绑定按钮事件
    document.getElementById('exportBtn').addEventListener('click', () => {
      exportExcelWithFormat(data, headerConfig, '员工信息表.xlsx');
    });
  </script>
</body>
</html>

附:示例截图:

image.png

导出效果

image.png

⚠️ 注意:file-saver 的新版(2.x)API 更稳定,推荐使用。