02- Excel文件导入导出技术实现详解:从原理到实战
本文基于「工会预决算报表填报查询系统」项目经验,深度解析 Excel 文件在 Web 应用中的导入导出技术方案,涵盖 LuckyExcel、ExcelJS、xlsx 等主流库的使用原理与最佳实践。
目录
- 一、技术选型与方案对比
- [二、Excel 文件格式原理](#二、Excel 文件格式原理)
- [三、Excel 导入:LuckyExcel 解析引擎](#三、Excel 导入:LuckyExcel 解析引擎)
- [四、Excel 导出:ExcelJS 构建引擎](#四、Excel 导出:ExcelJS 构建引擎)
- 五、导出样式映射详解
- 六、条件格式导出实现
- 七、性能优化与注意事项
- 八、完整导出工具类封装
一、技术选型与方案对比
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 的内部结构有助于:
- 排查导入导出问题:样式丢失?可能是样式索引映射错了
- 性能优化:大数据量时可以流式处理,减少内存占用
- 高级功能定制:比如向 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 导入导出是企业级报表系统的核心功能,本文从原理到实战详细介绍了:
- 技术选型:导入用 LuckyExcel,导出用 ExcelJS 的组合方案
- XLSX 格式原理:ZIP + XML 的文件结构
- 导入实现:LuckyExcel 的使用和 Electron 环境适配
- 导出示战:ExcelJS 完整实现,包括样式、合并、条件格式、工作表保护
- 性能优化:行高优化、常见问题排查
掌握这些技术,你就能在项目中实现高质量的 Excel 导入导出功能。
参考资料
关于作者:本文基于工会预决算报表填报查询系统的实际开发经验撰写,涵盖了 Excel 导入导出的完整技术方案。如果觉得有帮助,欢迎点赞、收藏、关注!