UNPKG

exceljs-xlsx-template

Version:

Generate .xlsx file from .xlsx template. Support for Browser and Node.js

445 lines (431 loc) 16.6 kB
"use strict"; const ExcelJS = require("exceljs"); const { isBrowser, fetchUrlFile } = require("./helpers.js"); /** * 加载工作簿 * @param {string|ArrayBuffer|Blob|Buffer} input - 输入数据,可以是本地路径、URL地址、ArrayBuffer、Blob、Buffer * @returns {Promise<ExcelJS.Workbook>} */ async function loadWorkbook(input) { const workbook = new ExcelJS.Workbook(); const httpRegex = /^https?:\/\//; if (isBrowser) { if (typeof input === "string" && httpRegex.test(input)) { await workbook.xlsx.load(await fetchUrlFile(input)); } else if (input instanceof Blob || input instanceof ArrayBuffer) { await workbook.xlsx.load(input); } else { throw new Error("Unsupported input type in browser environment. Expected Blob, File, ArrayBuffer."); } } else { if (typeof input === "string") { if (httpRegex.test(input)) { await workbook.xlsx.load(await fetchUrlFile(input)); } else { await workbook.xlsx.readFile(input); } } else if (input instanceof Buffer || input instanceof ArrayBuffer) { await workbook.xlsx.load(input); } else if (typeof input.pipe === "function") { await workbook.xlsx.read(input); } else { throw new Error( "Unsupported input type in Node.js environment. Expected file path, Buffer, ArrayBuffer, or Stream." ); } } return workbook; } /** * 填充Excel模板 * @param {ExcelJS.Workbook} workbook * @param {Array<Record<string, any>>} workbookData - 包含模板数据的数组对象 * @param {boolean} parseImage - 是否解析图片,默认为 false * @returns {Promise<ExcelJS.Workbook>} */ async function fillTemplate(workbook, workbookData, parseImage = false) { // 第一步:复制行并替换占位符 // 工作表待合并单元格信息 const sheetDynamicMerges = {}; // NOTE 工作簿的sheetId是按工作表创建的顺序从1开始递增 let sheetIndex = 0; workbook.eachSheet((worksheet, sheetId) => { const sheetData = workbookData[sheetIndex++]; if (!(sheetData && typeof sheetData === "object" && !Array.isArray(sheetData))) { return; } // 普通标签替换和迭代标签信息收集 const sheetIterTags = []; worksheet.eachRow((row, rowNumber) => { // 行迭代字段 const rowIterFields = []; row.eachCell((cell, colNumber) => { const cellType = cell.type; // 字符串值 if (cellType === ExcelJS.ValueType.String) { cell.value = processCellTags(cell.value, sheetData, sheetIterTags, rowIterFields, rowNumber); } // 富文本值 else if (cellType === ExcelJS.ValueType.RichText) { cell.value.richText.forEach((item) => { item.text = processCellTags(item.text, sheetData, sheetIterTags, rowIterFields, rowNumber); }); } }); }); // 迭代标签处理 if (sheetIterTags.length === 0) { return; } // 合并单元格信息 // NOTE 合并信息是静态的,不会随着行增加而实时更新 const sheetMerges = sheetMergeInfo(worksheet); // 迭代行并替换迭代字段占位符 let iterOffset = 0; sheetIterTags.forEach(({ iterStartRow, iterFieldNames, iterFieldName }, iterTagIndex) => { // 调整后的起始行 const adjustedStartRow = iterStartRow + iterOffset; // 多行的情况下,需要复制多行 if (sheetData[iterFieldName].length > 1) { // 一次性复制多行 // NOTE 复制的行不会复制合并信息 worksheet.duplicateRow(adjustedStartRow, sheetData[iterFieldName].length - 1, true); // 筛选出与当前模板行相关的合并单元格信息,并应用到其复制的行 const merges = sheetMerges.filter((merge) => { return merge.start.row <= iterStartRow && merge.end.row >= iterStartRow; }); if (merges.length > 0) { if (!sheetDynamicMerges[sheetId]) { sheetDynamicMerges[sheetId] = []; } // NOTE 在浏览器环境,动态增加的行会使其后面的行取消合并单元格 const startFixIndex = isBrowser ? (iterTagIndex === 0 ? 1 : 0) : 1; for (let i = startFixIndex; i < sheetData[iterFieldName].length; i++) { for (const merge of merges) { sheetDynamicMerges[sheetId].push([ merge.start.row + i + iterOffset, merge.start.col, merge.end.row + i + iterOffset, merge.end.col, ]); } } } } // 替换迭代行中的占位符 for (let i = 0; i < sheetData[iterFieldName].length; i++) { const currentRow = worksheet.getRow(adjustedStartRow + i); currentRow.eachCell((cell, colNumber) => { // 字符串值 if (cell.type === ExcelJS.ValueType.String) { for (const iterField of iterFieldNames) { const iterFieldData = sheetData[iterField]; if (cell.value.includes(`{{@@${iterField}\.`)) { if (iterFieldData[i] !== undefined) { // 迭代字段数据 for (const key in iterFieldData[i]) { const placeholder = `{{@@${iterField}.${key}}}`; if (cell.value.includes(placeholder)) { if (cell.value.length === placeholder.length && typeof iterFieldData[i][key] !== "object") { cell.value = iterFieldData[i][key]; } else { cell.value = cell.value.replace(new RegExp(placeholder, "g"), iterFieldData[i][key] ?? ""); } } } } else { cell.value = null; } } } } // TODO 迭代标签单元格为富文本值 }); } // 更新行号偏移量 iterOffset += sheetData[iterFieldName].length - 1; }); // 修正在浏览器环境,动态增加的行会使其后面的行取消合并单元格 if (isBrowser) { const iterRows = sheetIterTags.map(({ iterStartRow }) => iterStartRow); sheetMerges.forEach((merge) => { // 迭代后的偏移行 let mergeOffset = 0; sheetIterTags.forEach(({ iterStartRow, iterFieldName }) => { if (Array.isArray(sheetData[iterFieldName])) { if (!iterRows.includes(merge.start.row) && merge.start.row > iterStartRow) { mergeOffset += sheetData[iterFieldName].length - 1; } } }); if (mergeOffset) { if (!sheetDynamicMerges[sheetId]) { sheetDynamicMerges[sheetId] = []; } sheetDynamicMerges[sheetId].push([ merge.start.row + mergeOffset, merge.start.col, merge.end.row + mergeOffset, merge.end.col, ]); } }); } }); // 第二步:动态行单元格合并处理 if (Object.keys(sheetDynamicMerges).length > 0) { // 将工作簿保存到内存中的缓冲区 const buffer = await workbook.xlsx.writeBuffer(); // 从缓冲区重新加载工作簿 await workbook.xlsx.load(buffer); // 处理合并单元格 workbook.eachSheet((worksheet, sheetId) => { if (sheetDynamicMerges[sheetId]) { sheetDynamicMerges[sheetId].forEach((merge) => { try { worksheet.mergeCells(merge); } catch (error) { console.warn(`Fail to merge cells ${merge}`); } }); } }); } // 第三步:填充图片 parseImage && (await fillImage(workbook)); return workbook; } /** * 保存工作簿到文件 * @param {ExcelJS.Workbook} workbook * @param {string} output - 输出文件路径或文件名 * @returns {Promise<void>} */ async function saveWorkbook(workbook, output) { if (isBrowser) { const buffer = await workbook.xlsx.writeBuffer(); const blob = new Blob([buffer], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }); const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = output; link.click(); URL.revokeObjectURL(link.href); } else { await workbook.xlsx.writeFile(output); } } /** * 获取自定义占位符单元格范围 * @param {ExcelJS.Worksheet} worksheet * @param {string} placeholder - 占位符字符串,默认为 "{{#placeholder}}" * @param {boolean} clearMatch - 是否清除占位符,默认为 true * @returns {{start: {row: number; col: number}; end: {row: number; col: number}}|null} */ function placeholderRange(worksheet, placeholder = "{{#placeholder}}", clearMatch = true) { let result = null; const sheetMerges = sheetMergeInfo(worksheet); // 遍历每一行 outer: for (let rowNumber = 1; rowNumber <= worksheet.rowCount; rowNumber++) { const row = worksheet.getRow(rowNumber); // 遍历每个单元格 for (let colNumber = 1; colNumber <= row.cellCount; colNumber++) { const cell = row.getCell(colNumber); // 单元格值中是否包含占位符 if (typeof cell.value === "string" && cell.value.includes(`${placeholder}`)) { const info = sheetMerges.find((merge) => { return merge.start.row === rowNumber && merge.start.col === colNumber; }); result = info ?? { start: { row: rowNumber, col: colNumber }, end: { row: rowNumber, col: colNumber }, }; // 去除占位符 if (clearMatch) { cell.value = cell.value.replace(new RegExp(`${placeholder}`, "g"), ""); } // 跳出循环 break outer; } } } return result; } /** * 处理单元格标签(普通标签替换和迭代标签信息收集) * @param {string} target * @param {Record<string, any>} worksheetData * @param {Array<{iterStartRow: number; iterFieldNames: string[]; iterFieldName: string}>} iterationTags * @param {string[]} iterFieldNames * @param {number} rowNumber * @returns {string} */ function processCellTags(target, worksheetData, iterationTags, iterFieldNames, rowNumber) { // 普通标签占位符替换 if (/{{\w+(\.\w+)*}}/.test(target)) { // 允许单元格中有多个普通标签占位符 const placeholders = target.match(/{{\w+(\.\w+)*}}/g); placeholders.forEach((placeholder) => { const fields = placeholder.slice(2, -2).split("."); let value = worksheetData; let isMatched = true; for (let i = 0; i < fields.length; i++) { if (fields[i] in value) { value = value[fields[i]]; } else { isMatched = false; break; } } // 数据匹配成功 if (isMatched) { if (target.length === placeholder.length && typeof value !== "object") { target = value; } else { target = target.replace(placeholder, value ?? ""); } } }); } // 迭代标签信息搜集 else if (/{{@@\w+\.\w+}}/.test(target)) { // TODO 单元格含多个迭代标签 // 单元格中仅匹配一个迭代标签占位符 const iterFieldName = target.match(/{{@@(\w+)\.\w+}}/)[1]; if ( iterFieldName in worksheetData && Array.isArray(worksheetData[iterFieldName]) && worksheetData[iterFieldName].length > 0 ) { if (iterFieldNames.length === 0) { iterFieldNames.push(iterFieldName); iterationTags.push({ iterStartRow: rowNumber, iterFieldNames: iterFieldNames, iterFieldName: iterFieldName }); } else { if (!iterFieldNames.includes(iterFieldName)) { iterFieldNames.push(iterFieldName); const lastIterationTag = iterationTags[iterationTags.length - 1]; if (worksheetData[iterFieldName].length > worksheetData[lastIterationTag.iterFieldName].length) { lastIterationTag.iterFieldName = iterFieldName; } } } } } return target; } /** * 获取工作表合并信息 * @param {ExcelJS.Worksheet} worksheet * @returns {Array<{start: {row: number; col: number}; end: {row: number; col: number}}>} */ function sheetMergeInfo(worksheet) { return worksheet.model.merges.map((merge) => { // C30:D30 const [startCell, endCell] = merge.split(":"); return { start: { row: worksheet.getCell(startCell).row, col: worksheet.getCell(startCell).col }, end: { row: worksheet.getCell(endCell).row, col: worksheet.getCell(endCell).col }, }; }); } /** * 填充图片 * @param {ExcelJS.Workbook} workbook */ async function fillImage(workbook) { const filledImageMap = new Map(); const invalidImageSet = new Set(); const urlRegex = /https?:\/\/[^\s]+(?:\.(jpe?g|png|gif))?/i; const base64Regex = /data:image\/(jpeg|gif|png);base64,[^\s]+/i; // NOTE eachSheet、eachRow、eachCell都是同步方法,不会等待异步操作完成 // 遍历每个工作表 for (let i = 0; i < workbook.worksheets.length; i++) { const worksheet = workbook.worksheets[i]; const sheetMerges = sheetMergeInfo(worksheet); // 遍历每一行 for (let rowNumber = 1; rowNumber <= worksheet.rowCount; rowNumber++) { const row = worksheet.getRow(rowNumber); // 遍历每个单元格 for (let colNumber = 1; colNumber <= row.cellCount; colNumber++) { const cell = row.getCell(colNumber); if (typeof cell.value !== "string") { continue; } let targetRegex = null; let imageId = 0; // URL图片 if (urlRegex.test(cell.value)) { targetRegex = urlRegex; const matches = cell.value.match(urlRegex); const imageUrl = matches[0]; const imageExt = matches[1] ?? "png"; if (invalidImageSet.has(imageUrl)) { continue; } if (filledImageMap.has(imageUrl)) { imageId = filledImageMap.get(imageUrl); } else { let fileContent = null; try { fileContent = await fetchUrlFile(imageUrl); } catch { invalidImageSet.add(imageUrl); console.warn(`Fail to load image ${imageUrl}`); continue; } // 将图片添加到工作簿中 imageId = workbook.addImage({ buffer: fileContent, extension: imageExt === "jpg" ? "jpeg" : imageExt, }); filledImageMap.set(imageUrl, imageId); } } // Base64图片 else if (base64Regex.test(cell.value)) { targetRegex = base64Regex; const matches = cell.value.match(base64Regex); const imageData = matches[0]; const imageExt = matches[1]; if (filledImageMap.has(imageData)) { imageId = filledImageMap.get(imageData); } else { imageId = workbook.addImage({ base64: imageData, extension: imageExt, }); filledImageMap.set(imageData, imageId); } } if (!targetRegex) { continue; } // 将图片添加到工作表中 const merge = sheetMerges.find((merge) => { return merge.start.row === rowNumber && merge.start.col === colNumber; }); // 坐标系基于零,A1 的左上角将为 {col:0,row:0},右下角为 {col:1,row:1} worksheet.addImage(imageId, { // 左上角 tl: { col: merge ? merge.start.col - 1 : colNumber - 1, row: merge ? merge.start.row - 1 : rowNumber - 1, }, // 右下角 br: { col: merge ? merge.end.col : colNumber, row: merge ? merge.end.row : rowNumber, }, }); // 去除图片地址 cell.value = cell.value.replace(targetRegex, ""); } } } return workbook; } module.exports = { loadWorkbook, fillTemplate, saveWorkbook, placeholderRange, };