React中编辑Excel如何保留样式、公式及引用?
解决Excel模板填充时保留公式与样式的替代方案
核心问题
你基于React+Kotlin开发的应用,需要从用户提供的Excel模板填充数据后返回,但使用XLSX、ExcelJS等前端库时,生成的文件会丢失公式、引用及样式(ExcelJS仅保留部分样式),且认为Aspose Cloud成本过高,寻求免费/低成本替代方案。
可行替代方案
1. 后端使用Apache POI(Kotlin兼容)
- 优势:Apache POI是Java/Kotlin生态中最成熟的Excel处理库,对
.xlsx格式的公式、样式、单元格引用支持完整,开源免费无成本。 - 实现方式:Kotlin可直接调用POI的API,加载模板文件后仅修改数据单元格,原有公式和样式会完整保留。
- 注意点:操作复杂样式时需复用样式对象,避免创建过多样式导致文件体积异常增大。
2. 后端使用JXLS+JXLS-Transformer
- 原理:基于Apache POI封装,提供模板占位符语法,直接在Excel模板中标记数据位置,填充时自动保留所有原有格式和公式。
- 优势:简化模板填充逻辑,无需手动定位单元格,适合包含大量复杂公式和样式的模板场景。
- 使用方式:在Excel模板中用
${数据字段名}标记占位,后端通过JXLS加载模板并绑定数据集合即可。
3. 前端优化ExcelJS使用(有限改善)
如果必须在前端处理,可尝试以下优化:
- 加载模板时显式开启
preserveStyles选项:await workbook.xlsx.load(arrayBuffer, { preserveStyles: true }) - 公式需以
=开头写入,ExcelJS会保留公式计算逻辑(注意不要覆盖原有公式单元格) - 局限性:复杂数组公式、跨表引用仍可能丢失,仅适合样式和公式相对简单的场景。
4. 自建开源Excel处理服务
部署开源的OnlyOffice Document Server,通过API调用完成模板填充,完全保留Excel的所有格式和公式,成本仅为服务器部署费用,适合多实例或分布式场景。
后端Kotlin示例(Apache POI)
import org.apache.poi.xssf.usermodel.XSSFWorkbook import java.io.FileInputStream import java.io.FileOutputStream fun fillExcelTemplate(templatePath: String, outputPath: String, dataList: List<Map<String, Any>>) { // 加载模板文件 val templateStream = FileInputStream(templatePath) val workbook = XSSFWorkbook(templateStream) val sheet = workbook.getSheetAt(0) // 从第3行开始填充数据(对应前端代码的nuPonto=3) var currentRow = 2 dataList.forEach { data -> val row = sheet.createRow(currentRow++) // 填充数据,原有公式和样式自动保留 row.createCell(4).setCellValue(data["idEquipeEps"] as Int) row.createCell(5).setCellValue(data["noEncarregado"] as String) // 其他单元格依次填充... } // 输出文件 val outputStream = FileOutputStream(outputPath) workbook.write(outputStream) // 关闭资源 outputStream.close() workbook.close() templateStream.close() }
前端ExcelJS代码优化参考
import React, { useState, useEffect } from "react"; import { useMsal } from "@azure/msal-react"; import { Client } from "@microsoft/microsoft-graph-client"; import ExcelJS from "exceljs"; import { saveAs } from "file-saver"; import "isomorphic-fetch"; import { useGetMsalTokenQuery } from "../components/store/service/UsersData"; const ModelInventarioCopy = () => { const { instance, accounts } = useMsal(); const [fileId, setFileId] = useState(""); const [siteId, setSiteId] = useState(""); const [accessToken, setAccessToken] = useState(""); const { data: dataToken, error: errorToken, isLoading: loadingToken, } = useGetMsalTokenQuery(); useEffect(() => { if (dataToken) setAccessToken(dataToken); else if (errorToken) console.error("令牌获取失败:", errorToken); }, [dataToken, errorToken]); useEffect(() => { setFileId("idexample"); setSiteId("idexample"); }, []); const getAuthenticatedClient = (token) => { return Client.init({ authProvider: (done) => done(null, token), }); }; const downloadAndModifyExcel = async () => { if (!accessToken) return; try { const client = getAuthenticatedClient(accessToken); const response = await client .api(`/sites/${siteId}/drive/items/${fileId}/content`) .responseType("arraybuffer") .get(); await modifyExcelTemplate(response); } catch (error) { console.error("SharePoint文件下载失败:", error); } }; const modifyExcelTemplate = async (arrayBuffer) => { try { const workbook = new ExcelJS.Workbook(); // 开启样式保留 await workbook.xlsx.load(arrayBuffer, { preserveStyles: true }); const worksheet = workbook.getWorksheet(1); if (!worksheet) return console.error("未找到工作表"); let startRow = 3; const dataList = [ { idEquipeEps: 1, noEncarregado: "Lucas B", dtOperacao: "01/09/2024", dtSituacao: "01/09/2024", cdMedicao: 123, dsDivisaoDois: "testenoDiv2", noDivisaoTres: "testenoDiv3", nuParcela: 111, nuLine: 1, nuTree: 1, idCodTreeOne: 1234, vlCapFirstLevel: 1, vlHeigthFirstLevel: 1, dsObservation: "nothing", noOperacao: "testenoopp", vlCapRated: 1, vlHeigthRated: 1, }, { idEquipeEps: 2, noEncarregado: "Lucas C", dtOperacao: "01/09/2024", dtSituacao: "01/09/2024", cdMedicao: 123, dsDivisaoDois: "testenoDiv2", noDivisaoTres: "testenoDiv3", nuParcela: 111, nuLine: 1, nuTree: 1, idCodTreeOne: 12345, vlCapFirstLevel: 1, vlHeigthFirstLevel: 1, dsObservation: "wtf", noOperacao: "testenoopp", vlCapRated: 2, vlHeigthRated: 2, }, ]; dataList.forEach((item) => { if (item) { const row = worksheet.getRow(startRow); row.getCell(5).value = item.idEquipeEps || ""; row.getCell(6).value = item.noEncarregado || ""; row.getCell(7).value = item.dtOperacao || ""; row.getCell(8).value = item.dtSituacao || ""; row.getCell(10).value = item.cdMedicao || ""; row.getCell(11).value = item.dsDivisaoDois || ""; row.getCell(12).value = item.noDivisaoTres || ""; row.getCell(13).value = item.nuParcela || ""; row.getCell(14).value = item.nuLine || ""; row.getCell(15).value = item.nuTree || ""; row.getCell(17).value = item.idCodTreeOne || ""; row.getCell(18).value = item.vlCapFirstLevel || ""; row.getCell(20).value = item.vlHeigthFirstLevel || ""; row.getCell(23).value = item.dsObservation || ""; row.getCell(24).value = item.noOperacao || ""; row.getCell(25).value = item.vlCapRated || ""; row.getCell(26).value = item.vlHeigthRated || ""; row.commit(); } startRow++; }); const buffer = await workbook.xlsx.writeBuffer(); const blob = new Blob([buffer], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", }); saveAs(blob, "已更新模板.xlsx"); } catch (error) { console.error("Excel文件修改失败:", error); } }; return ( <div> <button onClick={downloadAndModifyExcel} disabled={!accessToken}> 下载并修改文件 </button> </div> ); }; export default ModelInventarioCopy;
内容的提问来源于stack exchange,提问作者Lucas Barbosa
相关产品推荐
相关产品推荐

