02- Excel文件导入导出技术实现详解:从原理到实战

02- Excel文件导入导出技术实现详解:从原理到实战

本文基于「工会预决算报表填报查询系统」项目经验,深度解析 Excel 文件在 Web 应用中的导入导出技术方案,涵盖 LuckyExcel、ExcelJS、xlsx 等主流库的使用原理与最佳实践。


目录


一、技术选型与方案对比

1.1 主流 Excel 处理库对比

在 JavaScript 生态中,处理 Excel 文件主要有以下几个库:

库名 原理 优势 劣势 适用场景
SheetJS (xlsx) 纯 JS 解析/生成 功能全面,社区活跃 样式支持有限,高级功能收费 简单数据导入导出
ExcelJS 纯 JS 生成 样式支持完善,支持流式 体积较大,导入功能弱 复杂样式导出
LuckyExcel 转换为 Luckysheet 格式 与 Luckysheet 无缝对接 只支持导入 Luckysheet 项目导入
Excel-WASM WebAssembly 封装 性能好,功能完整 体积大,兼容性问题 超大数据量

1.2 项目技术选型

在本项目中,我们采用了组合方案:

复制代码
┌─────────────┐      ┌─────────────┐      ┌──────────────┐
│  Excel 文件  │ ──→  │  LuckyExcel │ ──→  │  Luckysheet  │
│  (.xlsx)    │  导入  │  (转换)     │ 渲染  │  (在线表格)   │
└─────────────┘      └─────────────┘      └──────────────┘
                                                          │
                                                          ↓ 导出
┌─────────────┐      ┌─────────────┐      ┌──────────────┐
│  Excel 文件  │ ←──  │   ExcelJS   │ ←──  │  表格数据    │
│  (.xlsx)    │ 生成  │  (构建)     │ 获取  │  (JSON)      │
└─────────────┘      └─────────────┘      └──────────────┘

选型理由:

  • 导入用 LuckyExcel:专门为 Luckysheet 设计的转换工具,能完美还原样式、公式、合并单元格等
  • 导出用 ExcelJS:样式支持最完善,支持工作表保护、条件格式等高级特性
  • 补充 xlsx 库:用于简单的文件读取场景(如组织结构导入)

二、Excel 文件格式原理

2.1 XLSX 文件结构

.xlsx 文件本质上是一个 ZIP 压缩包,里面包含多个 XML 文件。

复制代码
workbook.xlsx (ZIP 压缩包)
├── [Content_Types].xml     // 内容类型定义
├── _rels/
│   └── .rels               // 关系定义
├── docProps/
│   ├── app.xml             // 应用属性
│   └── core.xml            // 核心属性
└── xl/
    ├── workbook.xml        // 工作簿定义(包含哪些工作表)
    ├── _rels/
    │   └── workbook.xml.rels
    ├── worksheets/
    │   ├── sheet1.xml      // 工作表1数据
    │   ├── sheet2.xml      // 工作表2数据
    │   └── ...
    ├── styles.xml          // 样式定义
    ├── sharedStrings.xml   // 共享字符串
    └── theme1.xml          // 主题

2.2 关键文件说明

sharedStrings.xml - 共享字符串表

Excel 为了节省空间,将所有文本内容集中存储在共享字符串表中,单元格只存储索引:

xml 复制代码
<!-- sharedStrings.xml -->
<sst count="100" uniqueCount="50">
  <si><t>工会决算报表</t></si>   <!-- 索引 0 -->
  <si><t>单位名称</t></si>       <!-- 索引 1 -->
  <!-- ... -->
</sst>

<!-- sheet1.xml 中引用 -->
<c r="A1" t="s">
  <v>0</v>  <!-- t="s" 表示类型为共享字符串,值 0 对应索引 0 -->
</c>

styles.xml - 样式表

所有单元格样式(字体、填充、边框、对齐等)都定义在样式表中,单元格通过 s 属性引用样式索引:

xml 复制代码
<xf xfId="0" fontId="2" fillId="1" borderId="1" applyAlignment="1">
  <alignment horizontal="center" vertical="center"/>
</xf>

2.3 为什么需要了解这些?

理解 XLSX 的内部结构有助于:

  1. 排查导入导出问题:样式丢失?可能是样式索引映射错了
  2. 性能优化:大数据量时可以流式处理,减少内存占用
  3. 高级功能定制:比如向 Excel 中注入自定义属性、宏等

三、Excel 导入:LuckyExcel 解析引擎

3.1 LuckyExcel 简介

LuckyExcel 是 Luckysheet 官方提供的 Excel 导入工具,作用是将 .xlsx 文件转换为 Luckysheet 可识别的 JSON 数据格式。

复制代码
Excel 文件 (.xlsx)
    │
    ▼ 解析
LuckyExcel
    │
    ▼ 转换
Luckysheet JSON 数据
    │
    ▼ 渲染
Luckysheet 表格

3.2 基本使用方法

javascript 复制代码
import LuckyExcel from "luckyexcel";

// 方式一:从文件对象导入
LuckyExcel.transformExcelToLucky(
  file,  // File 对象或 ArrayBuffer
  function (exportJson, luckysheetfile) {
    // 转换成功,exportJson.sheets 是工作表数据数组
    luckysheet.create({
      container: "luckysheet",
      data: exportJson.sheets,
      title: exportJson.title,
    });
  },
  function (err) {
    console.error("导入失败:", err);
  }
);

// 方式二:从文件路径导入(Electron/Node.js 环境)
import fs from "fs";
const data = fs.readFileSync("model.xlsx");
await LuckyExcel.transformExcelToLucky(data, function(exportJson) {
  // ...
});

3.3 Electron 主进程读取 + 渲染进程转换

在 Electron 项目中,推荐的模式是主进程读文件,渲染进程做转换:

typescript 复制代码
// electron/fs/xlsx.ts - 主进程
import fs from "fs";
import path from "path";

export async function readXlsx() {
  const modelXlsxPath = process.env.NODE_ENV === "development"
    ? path.join(__dirname, "../../public/static/model.xlsx")
    : path.join(process.resourcesPath, "static/model.xlsx");

  return new Promise((resolve, reject) => {
    fs.readFile(modelXlsxPath, (err, data) => {
      if (err) reject(err);
      else resolve(data);
    });
  });
}
typescript 复制代码
// 渲染进程(Vue 组件中)
import LuckyExcel from "luckyexcel";

const initXlsx = async () => {
  // 通过 IPC 从主进程获取文件内容
  const res = await window.myService.readXlsx();

  await LuckyExcel.transformExcelToLucky(
    res,
    async function (exportJson) {
      await luckysheet.create({
        container: "luckysheet",
        data: exportJson.sheets,
        title: "工会财务报表填报查询系统",
        lang: "zh",
      });
    },
    function (err) {
      console.error("导入失败:", err);
    }
  );
};

3.4 开发/生产环境路径处理

Electron 项目中,开发和生产环境的资源路径不同,需要特别处理:

typescript 复制代码
// 开发环境:项目 public 目录
const devPath = path.join(__dirname, "../../public/static/model.xlsx");

// 生产环境:打包后的 resources 目录
const prodPath = path.join(process.resourcesPath, "static/model.xlsx");

注意 :electron-builder 打包时,需要在 package.json 的 extraResources 中配置静态资源目录,否则生产环境会找不到文件。

3.5 LuckyExcel 转换能力一览

功能 支持情况 说明
单元格值 ✅ 完整支持 文本、数字、日期等
单元格样式 ✅ 大部分支持 字体、颜色、对齐、边框等
合并单元格 ✅ 支持 完整还原
公式 ✅ 支持 保留公式字符串
数据验证 ⚠️ 部分支持 基本类型支持
条件格式 ⚠️ 部分支持 简单规则支持
图表 ❌ 不支持 需要单独处理
宏/VBA ❌ 不支持 安全限制
透视表 ⚠️ 有限支持 只保留数据

四、Excel 导出:ExcelJS 构建引擎

4.1 ExcelJS 简介

ExcelJS 是一个功能强大的 Excel 生成库,支持读取、操作和写入 Excel 文件。在本项目中,我们用它来实现从 Luckysheet 数据到 Excel 文件的导出功能。

核心优势:

  • 🎨 丰富的样式支持(字体、填充、边框、对齐等)
  • 🔐 支持工作表保护(密码保护、限制编辑)
  • 📊 支持条件格式
  • 🖼️ 支持图片插入
  • 📝 支持批注和超链接
  • ⚡ 支持流式写入(大数据量友好)

4.2 导出整体架构

复制代码
Luckysheet 数据 (getluckysheetfile())
        │
        ▼
┌─────────────────────────┐
│   exportSheetExcel()    │  导出主函数
└───────────┬─────────────┘
            │
   ┌────────┼────────┬───────────┐
   ▼        ▼        ▼           ▼
 样式值   合并单元格  边框       图片
setStyle  setMerge  setBorder  setImages
   │        │        │           │
   └────────┴────────┴───────────┘
            │
            ▼
     ExcelJS Workbook
            │
            ▼
     writeBuffer() → Blob → 下载

4.3 导出主函数实现

typescript 复制代码
import ExcelJS from "exceljs";

export default function exportSheetExcel(
  tableArr = [],
  params = { name: "export" }
) {
  const { name } = params;

  // 1. 创建工作簿
  const workbook = new ExcelJS.Workbook();

  // 2. 遍历每个工作表
  tableArr.forEach(function (thesheet) {
    if (thesheet.data.length === 0) return true;

    const worksheet = workbook.addWorksheet(thesheet.name);

    // 网格线设置
    if (!thesheet.showGridLines) {
      worksheet.views = [{ showGridLines: false }];
    }

    // 设置各项属性
    setStyleAndValue(thesheet.data, worksheet);  // 样式和值
    setMerge(thesheet.config.merge, worksheet);  // 合并
    setBorder(thesheet, worksheet);              // 边框
    setImages(thesheet, worksheet, workbook);    // 图片
    setNote(thesheet.data, worksheet);           // 批注
    setHyperlink(thesheet.hyperlink, worksheet); // 超链接
    setFrozen(thesheet.frozen, worksheet);       // 冻结
    setConditions(thesheet.luckysheet_conditionformat_save, worksheet); // 条件格式
    setFilter(thesheet.filter_select, worksheet); // 筛选

    // 工作表保护
    worksheet.protect("password", {
      selectLockedCells: true,
      selectUnlockedCells: true,
      formatCells: false,
      formatColumns: false,
      formatRows: false,
      insertColumns: false,
      insertRows: false,
      deleteColumns: false,
      deleteRows: false,
      sort: false,
      autoFilter: false,
    });
  });

  // 3. 导出为 buffer
  const getBuffer = () => {
    return new Promise((resolve) => {
      workbook.xlsx.writeBuffer().then((data) => {
        const buffer = new Blob([data], { type: XLSX_BLOB_TYPE });
        resolve(buffer);
      });
    });
  };

  // 4. 触发浏览器下载
  const downloadExcel = async () => {
    const buffer = await getBuffer();
    const blob = new Blob([buffer], { type: XLSX_BLOB_TYPE });
    const downloadElement = document.createElement("a");
    const href = window.URL.createObjectURL(blob);
    downloadElement.href = href;
    downloadElement.download = name + ".xlsx";
    document.body.appendChild(downloadElement);
    downloadElement.click();
    document.body.removeChild(downloadElement);
    window.URL.revokeObjectURL(href);
  };

  return { downloadExcel, getBuffer, getFile };
}

4.4 使用方式

typescript 复制代码
// 在 Vue 组件中调用
const handleExport = async () => {
  // 先退出编辑模式,确保数据已保存
  await window.luckysheet.exitEditMode();

  try {
    // 获取 Luckysheet 全部数据
    const allSheetData = luckysheet.getluckysheetfile();
    const { downloadExcel } = exportSheetExcel(allSheetData);
    await downloadExcel();
  } catch (error) {
    ElMessage.error("导出失败: " + error.message);
  }
};

五、导出样式映射详解

5.1 颜色值转换

Luckysheet 使用 #RRGGBB 格式,ExcelJS 使用 ARGB 格式(带 Alpha 通道):

javascript 复制代码
// 颜色转换:#RRGGBB → AARRGGBB
function convertColor(color) {
  if (!color) return "FF000000"; // 默认黑色
  // 去掉 # 号,前面加 FF(不透明)
  return "FF" + color.replace("#", "");
}

// 示例
convertColor("#000000")  // → "FF000000"
convertColor("#FF0000")  // → "FFFF0000"

5.2 字体样式映射

Luckysheet 属性 ExcelJS 属性 说明
ff font.name 字体名称(需要映射索引)
fs font.size 字号
fc font.color.argb 字体颜色
bl font.bold 加粗(0/1 → false/true)
it font.italic 斜体
un font.underline 下划线

5.3 对齐方式映射

javascript 复制代码
// 水平对齐
const hAlignMap = {
  0: "general",   // 常规
  1: "left",      // 左对齐
  2: "center",    // 居中
  3: "right",     // 右对齐
};

// 垂直对齐
const vAlignMap = {
  0: "middle",    // 居中
  1: "top",       // 顶部
  2: "bottom",    // 底部
};

5.4 合并单元格

javascript 复制代码
function setMerge(mergeConfig, worksheet) {
  if (!mergeConfig) return;

  Object.values(mergeConfig).forEach((item) => {
    const { r, c, rs, cs } = item;
    // Luckysheet: r=起始行, c=起始列, rs=行数, cs=列数
    // 需要转换为 Excel 的 A1 格式范围
    const startCell = getCellAddress(r, c);
    const endCell = getCellAddress(r + rs - 1, c + cs - 1);
    const range = `${startCell}:${endCell}`;
    worksheet.mergeCells(range);
  });
}

// 行号列号转 Excel 单元格地址(如 0,0 → A1)
function getCellAddress(row, col) {
  const colLetter = columnToLetter(col + 1); // Excel 从1开始
  const rowNum = row + 1;
  return colLetter + rowNum;
}

function columnToLetter(column) {
  let letter = "";
  while (column > 0) {
    const temp = (column - 1) % 26;
    letter = String.fromCharCode(65 + temp) + letter;
    column = Math.floor((column - 1) / 26);
  }
  return letter;
}

5.5 列号转字母算法详解

Excel 列名采用 26 进制表示,但没有 0 的概念(A=1, B=2, ..., Z=26, AA=27):

复制代码
A   → 1
Z   → 26
AA  → 27  (1*26 + 1)
AZ  → 52  (1*26 + 26)
BA  → 53  (2*26 + 1)
ZZ  → 702 (26*26 + 26)
AAA → 703 (1*26*26 + 1*26 + 1)

算法核心是先减 1 再取模,以适配 0 索引:

javascript 复制代码
function columnToLetter(colIndex) {
  // colIndex 从 1 开始
  let result = "";
  while (colIndex > 0) {
    colIndex--;  // 关键:转为 0-based
    result = String.fromCharCode(65 + (colIndex % 26)) + result;
    colIndex = Math.floor(colIndex / 26);
  }
  return result;
}

六、条件格式导出实现

6.1 条件格式类型映射

Luckysheet 和 Excel 的条件格式类型有所不同,需要做映射转换:

Luckysheet type ExcelJS type 说明
default / equal cellIs / equal 等于
default / greaterThan cellIs / greaterThan 大于
default / lessThan cellIs / lessThan 小于
default / betweenness cellIs / between 介于
default / textContains containsText 文本包含
default / top10 top10 前 N 项
default / AboveAverage aboveAverage 高于平均值
dataBar dataBar 数据条
colorGradation colorScale 色阶
icons iconSet 图标集

6.2 条件格式实现代码

typescript 复制代码
export const setConditions = function (conditions, worksheet) {
  if (!conditions) return;

  conditions.forEach((item) => {
    const ruleObj = {
      ref: createCellRange(item.cellrange[0].row, item.cellrange[0].column),
      rules: []
    };

    // 1. 突出显示单元格规则
    if (item.type === "default") {
      if (["equal", "greaterThan", "lessThan", "betweenness"].includes(item.conditionName)) {
        ruleObj.rules = [{
          type: "cellIs",
          operator: item.conditionName === "betweenness" ? "between" : item.conditionName,
          formulae: item.conditionValue,
          style: setStyle([item.format.cellColor, item.format.textColor])
        }];
        worksheet.addConditionalFormatting(ruleObj);
      }

      // 文本包含
      if (item.conditionName === "textContains") {
        ruleObj.rules = [{
          type: "containsText",
          operator: "containsText",
          text: item.conditionValue[0],
          style: setStyle([item.format.cellColor, item.format.textColor])
        }];
        worksheet.addConditionalFormatting(ruleObj);
      }

      // 前N项 / 后N项
      if (["top10", "top10%", "last10", "last10%"].includes(item.conditionName)) {
        ruleObj.rules = [{
          type: "top10",
          rank: item.conditionValue[0],
          percent: item.conditionName.includes("%"),
          bottom: item.conditionName.startsWith("last"),
          style: setStyle([item.format.cellColor, item.format.textColor])
        }];
        worksheet.addConditionalFormatting(ruleObj);
      }

      // 高于/低于平均值
      if (["AboveAverage", "SubAverage"].includes(item.conditionName)) {
        ruleObj.rules = [{
          type: "aboveAverage",
          aboveAverage: item.conditionName === "AboveAverage",
          style: setStyle([item.format.cellColor, item.format.textColor])
        }];
        worksheet.addConditionalFormatting(ruleObj);
      }
    }

    // 2. 数据条
    if (item.type === "dataBar") {
      ruleObj.rules = [{ type: "dataBar", style: {} }];
      worksheet.addConditionalFormatting(ruleObj);
    }

    // 3. 色阶
    if (item.type === "colorGradation") {
      ruleObj.rules = [{
        type: "colorScale",
        color: item.format,
        style: {}
      }];
      worksheet.addConditionalFormatting(ruleObj);
    }

    // 4. 图标集
    if (item.type === "icons") {
      ruleObj.rules = [{
        type: "iconSet",
        iconSet: item.format.len
      }];
      worksheet.addConditionalFormatting(ruleObj);
    }
  });
};

// 样式构建辅助函数
function setStyle(colorArr) {
  return {
    fill: {
      type: "pattern",
      pattern: "solid",
      bgColor: { argb: colorArr[0].replace("#", "") }
    },
    font: {
      color: { argb: colorArr[1].replace("#", "") }
    }
  };
}

6.3 单元格范围转换

Luckysheet 使用行/列索引表示范围,ExcelJS 使用 A1 格式:

typescript 复制代码
function createCellRange(rowRange, columnRange) {
  // rowRange: [startRow, endRow]
  // columnRange: [startCol, endCol]
  const startCell = getCellAddress(rowRange[0], columnRange[0]);
  const endCell = getCellAddress(rowRange[1], columnRange[1]);
  return `${startCell}:${endCell}`;
}

七、性能优化与注意事项

7.1 大数据量导出优化

优化手段 原理 效果
避免计算行高 写死默认行高,不遍历计算 减少 30%+ 时间
分批处理 工作表数据分段写入 降低内存峰值
流式写入 ExcelJS 的流模式 支持超大数据量
Web Worker 将导出逻辑移到 Worker 不阻塞 UI
javascript 复制代码
// 默认行高写死,避免大数据量时遍历计算
const DEFAULT_ROW_HEIGHT = 19;

7.2 常见问题与解决方案

问题1:导出后样式丢失

原因:样式索引映射错误,或者 Luckysheet 的样式属性与 ExcelJS 不一一对应。

解决方案:

  • 检查颜色格式是否正确转换(#RRGGBB → FFRRGGBB)
  • 确认字体名称映射正确
  • 打印中间结果,对比导入导出差异

问题2:导出文件打不开

原因:XML 结构错误,通常是非法字符或格式问题。

解决方案:

  • 使用 workbook.xlsx.writeBuffer() 而不是手动拼接 XML
  • 检查单元格值是否包含特殊字符
  • 用 Excel 的修复功能查看具体错误信息

问题3:公式导出后不计算

原因:导出时只保存了公式字符串,没有缓存结果。

解决方案:

  • 导出前调用 luckysheet.refreshFormula() 确保公式已计算
  • 在 ExcelJS 中设置 cell.value = { formula: "=SUM(A1:A10)", result: 100 }

问题4:中文文件名乱码

原因:浏览器下载时编码问题。

解决方案:

javascript 复制代码
// 使用 encodeURIComponent 编码文件名
downloadElement.download = encodeURIComponent(filename);

7.3 工作表保护的注意事项

ExcelJS 的 worksheet.protect() 功能强大,但有一些坑:

javascript 复制代码
worksheet.protect("password", {
  // 注意:这些选项控制"允许用户做什么"
  selectLockedCells: true,     // ✅ 允许选择锁定单元格
  selectUnlockedCells: true,   // ✅ 允许选择未锁定单元格
  formatCells: false,          // ❌ 不允许设置单元格格式
  formatColumns: false,        // ❌ 不允许设置列格式
  formatRows: false,           // ❌ 不允许设置行格式
  insertColumns: false,        // ❌ 不允许插入列
  insertRows: false,           // ❌ 不允许插入行
  insertHyperlinks: false,     // ❌ 不允许插入超链接
  deleteColumns: false,        // ❌ 不允许删除列
  deleteRows: false,           // ❌ 不允许删除行
  sort: false,                 // ❌ 不允许排序
  autoFilter: false,           // ❌ 不允许自动筛选
  pivotTables: false,          // ❌ 不允许数据透视表
});

重要 :工作表保护只对设置了 locked: true 的单元格生效。默认所有单元格都是锁定的。如果想让某些单元格可编辑,需要单独设置 protection: { locked: false }。


八、完整导出工具类封装

8.1 导出函数返回值设计

typescript 复制代码
interface ExportResult {
  downloadExcel: () => Promise<void>;  // 直接下载
  getBuffer: () => Promise<Blob>;      // 获取 Blob
  getFile: () => Promise<{ raw: File; blob: Blob }>; // 获取 File 对象
}

8.2 调用示例

typescript 复制代码
// 场景1:直接下载
const { downloadExcel } = exportSheetExcel(data, { name: "报表" });
await downloadExcel();

// 场景2:上传到服务器
const { getBuffer } = exportSheetExcel(data);
const buffer = await getBuffer();
await uploadToServer(buffer);

// 场景3:预览
const { getFile } = exportSheetExcel(data);
const file = await getFile();
previewExcel(file.raw);

总结

Excel 导入导出是企业级报表系统的核心功能,本文从原理到实战详细介绍了:

  1. 技术选型:导入用 LuckyExcel,导出用 ExcelJS 的组合方案
  2. XLSX 格式原理:ZIP + XML 的文件结构
  3. 导入实现:LuckyExcel 的使用和 Electron 环境适配
  4. 导出示战:ExcelJS 完整实现,包括样式、合并、条件格式、工作表保护
  5. 性能优化:行高优化、常见问题排查

掌握这些技术,你就能在项目中实现高质量的 Excel 导入导出功能。


参考资料


关于作者:本文基于工会预决算报表填报查询系统的实际开发经验撰写,涵盖了 Excel 导入导出的完整技术方案。如果觉得有帮助,欢迎点赞、收藏、关注!

相关推荐
程序员清风17 小时前
CSV、Excel 与数据库数据读取实践
数据库·oracle·excel
泡海椒21 小时前
JQuick-Excel JContext 与 trans 实战:将字典值带入行转换
excel
泡海椒2 天前
JQuick-Excel dateFormat 转换实战:日期值与 FORMAT 显示格式的边界
开发语言·python·excel
2501_933670792 天前
2027风控策略岗秋招准备:SQL、Excel、建模的优先级与项目路径
人工智能·sql·excel
流形填表2 天前
在线考试平台导入题库:Excel格式与字段对照
excel
开开心心就好3 天前
办公软件卸载不干净?专用工具一键清残留
java·前端·人工智能·智能手机·kafka·excel·memcache
全速向光3 天前
Excel函数系列09:公式又长又乱?LET+LAMBDA+MAP+REDUCE,自定义函数一次讲透
excel
鲲穹AI种草3 天前
办公表格批量处理:多款 Excel 工具能力客观记录
excel
开开心心就好3 天前
二维码批量生成导出工具,离线可用完全免费
java·前端·人工智能·智能手机·github·excel·visual studio