提升工作效率:前端实现Excel导出的多种方案解析

617 阅读8分钟

最近要对项目做一个导出excel的功能,以前的这种功能是后端提供一个数据流,前端点击直接下载即可。可是这次跟后端沟通得到的最终结果是,后端提供数据,前端导出excel。导出的excel不仅要有数据还有美观。这可是有挺大的挑战。接下来看看下面在导出excel中使用的三种不同方法和遇见的坑吧~🫠🫠🫠 最终效果:

image.png

使用xlsx库

一、安装xlsx库

首先我们需要在vue3的项目中安装xlsx库。可以使用npm 或者 pnpm来进行安装。

npm install xlsx
pnpm install xlsx

如果需要设置excel的样式,还需要安装xlsx-style库:

pnpm install xlsx-style

二、在vue组件中引入xlsx库

需要引入xlsx库才可以在代码中使用方法和函数

import * as XLSX from 'xlsx';
// 如果需要设置样式,则引入xlsx-style
// import XLSXStyle from 'xlsx-style';

获取table元素导出

这里我们需要获取导出的excel中数据的table表格。如果需要为表格增加表头或者文字说明需要手动插入html元素。插入html元素就会造成原本页面中的表格也会被插入一个原本不存在的表头,解决方法是使用v-if控制两个相同的table并只显示其中一个。

screenshots.gif

const exportExcel = () => {
    // 选择页面中ID为'my-table'的表格元素
    var table = document.querySelector('#my-table');

    // 尝试获取表格的thead部分,如果没有thead,则创建一个新的thead元素
    var thead = table.querySelector('thead') || document.createElement('thead');

    // 创建一个新的tr(表格行)元素
    var tr = document.createElement('tr');

    // 创建一个新的th(表头单元格)元素
    var th = document.createElement('th');

    // 设置th的内容为"场所管家应用情况统计表"
    th.textContent = '场所管家应用情况统计表';

    // 设置字体加粗
    th.style.fontWeight = 'bold';

    // 设置文本水平居中显示
    th.style.textAlign = 'center';

    // 设置文本垂直居中显示(但通常th标签内的内容默认就会垂直居中,特别是在Excel中)
    th.style.verticalAlign = 'middle';

    // 将th添加到新创建的tr元素中
    tr.appendChild(th);

    // 将包含标题的tr插入到thead的第一个位置
    thead.insertBefore(tr, thead.firstChild);

    // 如果表格原先没有thead,则将创建或修改后的thead添加到表格中
    if (!table.querySelector('thead')) {
        table.appendChild(thead);
    }

    // 使用XLSX库将表格转换为工作簿对象
    var wb = XLSX.utils.table_to_book(table);

    // 将工作簿写入为二进制数组格式的xlsx文件
    var wbout = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });

    try {
        // 使用FileSaver.js保存文件,文件名为"应用情况说明.xlsx"
        FileSaver.saveAs(new Blob([wbout], { type: 'application/octet-stream' }), '应用情况说明.xlsx');
    } catch (e) {
        // 捕获并打印错误信息到控制台(如果console存在的话)
        if (typeof console !== 'undefined') console.error(e);
    }

    // 返回生成的工作簿的二进制数据
    return wbout;
};

下面是导出示例: image.png

使用表格数据导出

下面的代码中我们除了增加表头还增加了这个表格的文字说明,放在了表格的第一行。增加表格的文字说明我们需要注意的是如果需要增加表格文字说明,就需要我们在原有的数据的第一行插入一个空对象。

const exportExcel = () => {
    //设置表格文字说明
    const excelHead = ref('场所管家应用情况统计表 至' + nowDate());
        const data = tableData.value; // tableData.value就是需要导出的表格数据
        const ws = XLSX.utils.json_to_sheet(data);

        // 添加标题行(A1:R1),并准备合并它们
        XLSX.utils.sheet_add_aoa(ws, [[excelHead.value]], { origin: 'A1' });
        // 合并从 A1 到 R1 的单元格
        if (!ws['!merges']) ws['!merges'] = [];
        ws['!merges'].push({ s: { r: 0, c: 0 }, e: { r: 0, c: 17 } });

        // 添加表头行(A2:R2)
        XLSX.utils.sheet_add_aoa(
            ws,
            [
                [
                    '表头','表头','表头','表头','表头','表头',
                ],
            ],
            { origin: 'A2' }
        );

        const wb = XLSX.utils.book_new();

        XLSX.utils.book_append_sheet(wb, ws, 'Sheet1');
        // 这里我是将从第三行开始每三行的第一列合并成一行,如果你没有这个需求可以忽略
        for (let rowIndex = 2; rowIndex < ws['!ref'].split(':')[1].replace(/[0-9]/g, '').length * tableData.value.length; rowIndex += 3) {
            // 注意:这里我们假设你的数据不会超过Z列(即tableData数据的长度列),你可能需要根据你的实际数据调整这个逻辑
            // 第一列是A列,对应的列索引是0(因为索引从0开始)
            const startCell = XLSX.utils.encode_cell({ r: rowIndex, c: 0 }); // 起始单元格
            const endCell = XLSX.utils.encode_cell({ r: rowIndex + 2, c: 0 }); // 结束单元格(每三行的最后一行)

            // 合并单元格
            if (!ws['!merges']) ws['!merges'] = [];
            ws['!merges'].push({ s: startCell, e: endCell });

            // 可选:如果你想要在合并后的单元格中显示某些数据,你可以在合并之前将数据写入起始单元格
            // 例如,你可以将三行中的第一行的数据写入起始单元格(这里省略了,因为你已经有了数据)
            // 但请注意,合并后的单元格中只会显示起始单元格的内容
        }

        // 输出版本号以确认 console.log(XLSX.version);

        // 写入Excel文件
        XLSX.writeFile(wb, excelHead.value + '.xlsx');
    return;
};

下面是导出示例: image.png

如果你需要修改excel样式,需要导入xlsx-style库并引用。 引入之后你会发现下面这样的错误。

import * as XLSX from "xlsx-style";

image.png 按照其他博主的解决办法:修改源代码

// 在\node_modules\xlsx-style\dist\cpexcel.js  807行
var cpt = require('./cpt' + 'able');  改为   var cpt = cptable; 

image.png 改完后重启发现了新的问题: image.png 经过多次修改始终没有办法解决这种方法放弃了🥹🥹🥹

查找资料发现网上还有针对veu3 vite的xlsx-style-vite,我们尝试下载并引用它

pnpm i xlsx-style-vite
import * as XLSX from 'xlsx-style-vite';

重启发现还是存在问题果断方式,尝试下一种方法。

image.png

使用exceljs

一、安装exceljs库

首先我们需要在vue3的项目中安装exceljs库和file-saver

👇👇👇👇👇👇

exceljs 中文文档

pnpm install xlsx
pnpm install file-saver

二、创建工作簿

// 配合exceljs使用
import FileSaver from 'file-saver';
import ExcelJS from 'exceljs';

// 创建工作簿 
const workbook = new ExcelJS.Workbook(); 
// 添加工作表,名为sheet1 
const sheet1 = workbook.addWorksheet('sheet1'); 
// 整理数据 (data是需要导出的excel数据)
const data = transformData(tableData.value);

三、自定义表头和表格文字说明

// 整理数据
const data = transformData(tableData.value);
// 获取表头所有键
const headers = Object.keys(data[0]);
// 初始化每列的最大宽度为标题行的宽度
let columnWidths = headers.map((header) => header.length);
// 使用 Array.from 方法生成数组
const describe = Array.from({ length: headers.length }, () => '场所管家');

// 将文字说明插入到第一行
const firstRow = sheet1.addRow(describe);
// 将标题写入第二行
const headerRow = sheet1.addRow(headers);
// 合并第一行的所有列
sheet1.mergeCells(`A1:${String.fromCharCode(64 + headers.length)}1`);
// 设置合并后的单元格样式
firstRow.eachCell((cell, index) => {
    if (index === 1) {
        // 只对合并后的第一个单元格设置样式
        cell.font = { bold: true, size: 18 }; // 加粗字体
        cell.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true }; // 居中对齐
        }
});

这步做完导出就可以看见表头和文字说明了

image.png

四、根据需求设置样式

设置标题样式

// 设置标题行样式
headerRow.eachCell((cell, index) => {
    cell.font = { bold: true };
    cell.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true }; // 居中对齐,并启用自动换行
    // 增加边框
    cell.border = {
        top: { style: 'thin' },
        left: { style: 'thin' },
        bottom: { style: 'thin' },
        right: { style: 'thin' },
    };
});

将导出的excel每三行合并的第一列合并,并设置自动换行和边框

//将数据写入工作表
const mergeArr = []; // 记录要合并的单元格
data.forEach((row, rowIndex) => {
    // 遍历数据数组,row为当前行的数据,rowIndex为当前行的索引
    const rowPosition = rowIndex + 3; // 计算实际的行位置,因为前面有描述和标题两行,所以从第3行开始计算
    const excelRow = sheet1.addRow(Object.values(row)); // 将当前行的数据(对象值)添加到工作表中,并获取该行的引用

    // 每三行的第一列进行合并
    if (rowIndex % 3 === 0) {
        // 如果当前行是每组三行的第一个(即rowIndex能被3整除)
        const startRow = rowPosition; // 定义合并区域的起始行号,等于当前行的实际位置
        // 计算合并区域的结束行号,确保不超过数据的最大范围
        const endRow = Math.min(rowPosition + 2, rowPosition + (data.length - rowIndex > 2 ? 2 : data.length - rowIndex - 1));
        mergeArr.push({ startRow, endRow });
        // 对合并后的单元格设置样式
        // excelRow.getCell(1).alignment = { vertical: 'middle', wrapText: true }; // 设置合并后的单元格内容垂直居中对齐
    }

    // 为当前行的所有单元格设置自动换行和边框
    excelRow.eachCell((cell) => {
        cell.alignment = { vertical: 'middle' }; // 确保每个单元格都启用了自动换行
        cell.border = {
            top: { style: 'thin' },
            left: { style: 'thin' },
            bottom: { style: 'thin' },
            right: { style: 'thin' },
        };

        // 设置字体样式
        cell.font = { size: 12 }; // 设置字体大小
    });
});

内容居中显示

// 遍历整个工作表的所有单元格,确保第一列的单元格都启用了自动换行
sheet1.eachRow((row) => {
    row.eachCell((cell, colNumber) => {
        if (colNumber === 1) {
            // 第一列(注意:colNumber从1开始)
            cell.alignment = { ...cell.alignment, wrapText: true }; // 确保每个单元格都启用了自动换行
        }
        // 设置从第三列开始的所有单元格的内容居中显示
        if (colNumber >= 3) {
            cell.alignment = { ...cell.alignment, horizontal: 'center', vertical: 'middle' };
        }
    });
});

自适应宽度

// 根据计算的最大宽度设置列宽
columnWidths.forEach((width, index) => {
  sheet1.columns[index].width = width < 10 ? 10 : width; // 最小宽度设为10
});

导出表格文件

// 导出表格文件
workbook.xlsx
    .writeBuffer()
    .then((buffer) => {
        let file = new Blob([buffer], { type: 'application/octet-stream' });
        FileSaver.saveAs(file, 'ExcelJS.xlsx');
    })
    .catch((error) => console.log('Error writing excel export', error));

完整代码演示及导出实例

// 导出excel文件
const exportExcel = () => {
    // 创建工作簿
    const workbook = new ExcelJS.Workbook();
    // 添加工作表,名为sheet1
    const sheet1 = workbook.addWorksheet('sheet1');
    // 整理数据(data为导出的excel数据)
    const data = transformData(tableData.value);
    // 获取表头所有键
    const headers = Object.keys(data[0]);
    // 初始化每列的最大宽度为标题行的宽度
    let columnWidths = headers.map((header) => header.length);
    // 使用 Array.from 方法生成数组
    const describe = Array.from({ length: headers.length }, () => '场所管家');

    // 将文字说明插入到第一行
    const firstRow = sheet1.addRow(describe);
    // 将标题写入第二行
    const headerRow = sheet1.addRow(headers);
    // 合并第一行的所有列
    sheet1.mergeCells(`A1:${String.fromCharCode(64 + headers.length)}1`);
    // 设置合并后的单元格样式
    firstRow.eachCell((cell, index) => {
        if (index === 1) {
            // 只对合并后的第一个单元格设置样式
            cell.font = { bold: true, size: 18 }; // 加粗字体
            cell.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true }; // 居中对齐
        }
    });
    // 设置标题行样式
    headerRow.eachCell((cell, index) => {
        cell.font = { bold: true };
        cell.alignment = { horizontal: 'center', vertical: 'middle', wrapText: true }; // 居中对齐,并启用自动换行
        // 增加边框
        cell.border = {
            top: { style: 'thin' },
            left: { style: 'thin' },
            bottom: { style: 'thin' },
            right: { style: 'thin' },
        };
        // 更新列宽
        if (cell.text && cell.text.length > columnWidths[index - 1]) {
            columnWidths[index - 1] = cell.text.length;
        }
    });

    //将数据写入工作表
    const mergeArr = []; // 记录要合并的单元格
    data.forEach((row, rowIndex) => {
        // 遍历数据数组,row为当前行的数据,rowIndex为当前行的索引
        const rowPosition = rowIndex + 3; // 计算实际的行位置,因为前面有描述和标题两行,所以从第3行开始计算
        const excelRow = sheet1.addRow(Object.values(row)); // 将当前行的数据(对象值)添加到工作表中,并获取该行的引用

        // 每三行的第一列进行合并
        if (rowIndex % 3 === 0) {
            // 如果当前行是每组三行的第一个(即rowIndex能被3整除)
            const startRow = rowPosition; // 定义合并区域的起始行号,等于当前行的实际位置
            // 计算合并区域的结束行号,确保不超过数据的最大范围
            const endRow = Math.min(rowPosition + 2, rowPosition + (data.length - rowIndex > 2 ? 2 : data.length - rowIndex - 1));
            mergeArr.push({ startRow, endRow });
            // 对合并后的单元格设置样式
            // excelRow.getCell(1).alignment = { vertical: 'middle', wrapText: true }; // 设置合并后的单元格内容垂直居中对齐
        }

        // 为当前行的所有单元格设置自动换行和边框
        excelRow.eachCell((cell) => {
            cell.alignment = { vertical: 'middle' }; // 确保每个单元格都启用了自动换行
            cell.border = {
                top: { style: 'thin' },
                left: { style: 'thin' },
                bottom: { style: 'thin' },
                right: { style: 'thin' },
            };

            // 设置字体样式
            cell.font = { size: 12 }; // 设置字体大小
        });
    });
    // 遍历整个工作表的所有单元格,确保第一列的单元格都启用了自动换行
    sheet1.eachRow((row) => {
        row.eachCell((cell, colNumber) => {
            if (colNumber === 1) {
                // 第一列(注意:colNumber从1开始)
                cell.alignment = { ...cell.alignment, wrapText: true }; // 确保每个单元格都启用了自动换行
            }
            // 设置从第三列开始的所有单元格的内容居中显示
            if (colNumber >= 3) {
                cell.alignment = { ...cell.alignment, horizontal: 'center', vertical: 'middle' };
            }
        });
    });
    // 开始遍历要合并的数组,进行遍历
    mergeArr.forEach((item) => {
        sheet1.mergeCells(`A${item.startRow}:A${item.endRow}`);
    });
    sheet1.getColumn(2).width = 15;
    sheet1.getColumn(3).width = 10;

    // 导出表格文件
    workbook.xlsx
        .writeBuffer()
        .then((buffer) => {
            let file = new Blob([buffer], { type: 'application/octet-stream' });
            FileSaver.saveAs(file, 'ExcelJS.xlsx');
        })
        .catch((error) => console.log('Error writing excel export', error));
};

image.png

使用 xlsx 库和使用 exceljs 库各有优缺点

  • 使用 xlsx 库:

    • 优点:如果您的项目中已经熟悉并使用了相关的库和技术,可能更容易上手。
    • 缺点:在修改样式时可能会遇到一些难以解决的问题。
  • 使用 exceljs 库:

    • 优点:提供了更丰富和灵活的样式设置选项,对于需要复杂样式的导出需求可能更适用。
    • 缺点:安装和配置相对复杂一些。

具体哪种方法更好取决于您的项目需求和个人技术偏好。如果您对样式要求不高,且项目中已经有相关依赖,xlsx 库可能是个不错的选择;如果您需要更精细的样式控制,exceljs 库可能更适合。

文章就到此结束了,大家如果感兴趣可以留言关注,我们一起学习😄😄😄

👋👋👋👋👋👋👋👋👋👋👋👋👋👋👋👋👋👋👋👋👋👋👋👋👋👋👋👋👋