前端直接导出 Excel 完全指南 使用JS插件与自定义实现

在前端开发中,导出 Excel 是一项非常常见的需求。传统方式通常由后端生成 Excel 文件,但这种方式存在明显的缺点:增加服务器压力、网络传输耗时、无法实时预览数据。随着 JavaScript 生态的发展,我们完全可以借助优秀的开源库在前端直接完成 Excel 的生成与导出工作。本文将基于实际项目代码,详细介绍如何使用 xlsx.js 插件以及如何编写自定义导出逻辑,实现纯前端导出功能。

一、为什么选择前端导出 Excel

前端导出 Excel 相比后端导出具有显著优势。首先是减轻服务器压力,Excel 生成是计算密集型操作,放在前端执行可以节省服务器资源。其次是提升用户体验,用户点击导出按钮后,数据已经存在于前端,无需等待服务器处理和文件下载的过程。第三是实现灵活性,前端可以轻松实现复杂的表头合并、单元格样式定制、冻结窗格等功能。最后是架构简化,后端无需维护专门的 Excel 生成接口,前端可以复用已有的 RESTful API。

xlsx.js(SheetJS)是目前最流行的前端 Excel 操作库,它支持读取和写入多种电子表格格式,功能强大且稳定。本文示例使用的是 0.18.2 版本,通过 CDN 引入即可使用。

二、基础用法:纯 JavaScript 实现 Excel 导出

让我们从最简单的例子开始,了解前端导出 Excel 的核心流程。首先需要引入 xlsx.js 库:

<script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/xlsx/0.18.2/xlsx.full.min.js"></script>

然后定义导出的数据结构和表头配置。以下是一个简单的示例,展示如何导出包含嵌套结构的表格数据:

// 模拟导出数据,包含子元素
var selectionData = [
    { 
        name: '张三', 
        js: '熟练',
        css: '一般',
        child: [
            { js2: '张三', css2: '熟练', mycss: '一般' },
            { js2: '张三2', css2: '熟练', mycss: '一般' }
        ]
    }
];

// 定义表头结构,支持多级嵌套
var revealList = [
    {
        name: '姓名',
        prop: 'name'
    },
    {
        name: '专业技能',
        child: [
            {
                name: '前端',
                prop: 'js'
            },
            {
                name: '后端',
                prop: 'css'
            },
            {
                name: '后端2',
                child: [
                    { name: '前端2', prop: 'js2' },
                    { name: '后端2', prop: 'css2' },
                    { name: '后端3', prop: 'mycss' }
                ]
            }
        ]
    }
];

核心的导出函数 exportExcel 完成了以下工作:构建 Excel 表头、提取数据、计算单元格合并、生成工作簿、触发下载。

function exportExcel() {
    if (!Array.isArray(selectionData) || selectionData.length < 1) {
        return;
    }
    
    let sheetName = '数据导出';
    // 构建 Excel 表头
    let excelHeader = buildHeader(revealList);
    let headerRows = excelHeader.length;
    // 提取数据
    let dataList = extractData(selectionData, revealList);
    excelHeader.push(...dataList, []);
    // 计算合并单元格
    let merges = doMerges(excelHeader);
    // 生成 Sheet
    let ws = aoa_to_sheet(excelHeader, headerRows);
    // 设置单元格合并
    ws['!merges'] = merges;
    // 设置头部冻结
    ws["!freeze"] = {
        xSplit: "1",
        ySplit: "" + headerRows,
        topLeftCell: "B" + (headerRows + 1),
        activePane: "bottomRight",
        state: "frozen"
    };
    // 设置列宽
    ws['!cols'] = [{ wpx: 165 }];
    
    let workbook = {
        SheetNames: [sheetName],
        Sheets: {}
    };
    workbook.Sheets[sheetName] = ws;
    
    let wopts = {
        bookType: 'xlsx',
        bookSST: false,
        type: 'binary',
        cellStyles: true
    };
    
    let wbout = XLSX.write(workbook, wopts);
    let blob = new Blob([s2ab(wbout)], { type: "application/octet-stream" });
    openDownloadXLSXDialog(blob, sheetName + '.xlsx');
}

下载功能的实现使用了 HTML5 的 Blob 和 URL.createObjectURL API,创建一个隐藏的 <a> 标签并模拟点击事件:

function openDownloadXLSXDialog(url, saveName) {
    if (typeof url == 'object' && url instanceof Blob) {
        url = URL.createObjectURL(url);
    }
    var aLink = document.createElement('a');
    aLink.href = url;
    aLink.download = saveName || '';
    var event;
    if (window.MouseEvent) {
        event = new MouseEvent('click');
    } else {
        event = document.createEvent('MouseEvents');
        event.initMouseEvent('click', true, false, window, 0, 0, 0, 0, 0, false,
            false, false, false, 0, null);
    }
    aLink.dispatchEvent(event);
}

三、表头构建:多级嵌套表头的实现

在前端导出 Excel 时,复杂的表头结构是常见的需求。例如,一个项目信息表格可能包含“项目基本信息”、“保证金缴纳信息”、“保证金支付信息”等多个分组,每个分组下又有多个具体字段。这就是多级表头的应用场景。

表头配置使用嵌套的 JavaScript 对象结构,通过 child 属性表示子层级,通过 prop 属性绑定数据字段。顶层配置只需要 name 表示列名,二级及以下层级需要同时指定 nameprop

构建多级表头的核心算法采用了递归思想,通过 getHeader 函数遍历配置的嵌套结构,生成二维数组形式的表头:

function buildHeader(revealList) {
    let excelHeader = [];
    getHeader(revealList, excelHeader, 0, 0);
    return excelHeader;
}

function getHeader(headers, excelHeader, deep, perOffset) {
    let offset = 0;
    let cur = excelHeader[deep];
    if (!cur) {
        cur = excelHeader[deep] = [];
    }
    // 填充行合并占位符
    pushRowSpanPlaceHolder(cur, perOffset - cur.length);
    
    for (let i = 0; i < headers.length; i++) {
        let head = headers[i];
        cur.push(head.name);
        if (head.hasOwnProperty('child') && Array.isArray(head.child)
            && head.child.length > 0) {
            // 递归处理子层级
            let childOffset = getHeader(head.child, excelHeader, deep + 1,
                cur.length - 1);
            // 填充列合并占位符
            pushColSpanPlaceHolder(cur, childOffset - 1);
            offset += childOffset;
        } else {
            offset++;
        }
    }
    return offset;
}

在构建表头的过程中,使用了占位符来标记需要合并的单元格。!$ROW_SPAN_PLACEHOLDER 表示该单元格需要与上一行合并,!$COL_SPAN_PLACEHOLDER 表示该单元格需要与左侧单元格合并。

四、数据提取:处理嵌套数据结构

实际项目中的数据往往具有复杂的嵌套结构,比如一个项目可能包含多条支付记录、退还记录等二级数据,每条支付记录下又可能包含多个支付详情。处理这种嵌套数据是前端导出的难点之一。

extractData 函数负责将嵌套的原始数据转换为适合 Excel 导出的平面结构,它首先将配置的所有字段扁平化,然后通过 flatData 函数处理嵌套关系。

function extractData(selectionData, revealList) {
    let headerList = flat(revealList); // 扁平化表头配置
    let excelRows = [];
    
    let dataKeys = new Set(Object.keys(selectionData[0]));
    // 识别子层级字段,从父级字段中排除
    selectionData.some(e => {
        if (e.child && e.child.length > 0) {
            let childKeys = Object.keys(e.child[0]);
            for (let i = 0; i < childKeys.length; i++) {
                dataKeys.delete(childKeys[i]);
            }
            return true;
        }
    });
    
    flatData(selectionData, (list) => {
        excelRows.push(...buildExcelRow(dataKeys, headerList, list));
    });
    return excelRows;
}

flatData 函数是处理嵌套数据的关键,它根据数据的 child 属性判断是否存在子级,将父级数据与子级数据进行横向合并,并为每条记录添加 rowSpan 标记,用于后续的单元格合并计算。

function flatData(list, eachDataCallBack) {
    let resultList = [];
    for (let i = 0; i < list.length; i++) {
        let data = list[i];
        let rawDataList = [];
        
        if (data.child && data.child.length > 0) {
            // 存在子级数据,需要展开
            for (let j = 0; j < data.child.length; j++) {
                let copy = Object.assign({}, data, data.child[j]);
                // rowSpan = 2 表示首行,rowSpan = 3 表示后续行
                copy['rowSpan'] = (j == 0 ? 2 : 3);
                rawDataList.push(copy);
            }
        } else {
            data['rowSpan'] = 1;
            rawDataList.push(data);
        }
        
        resultList.push(...rawDataList);
        if (typeof eachDataCallBack === 'function') {
            eachDataCallBack(rawDataList);
        }
    }
    return resultList;
}

对于更复杂的三级嵌套结构(如项目 -> 支付记录 -> 支付详情),flatData 函数需要进行三层级的递归处理,每一层级都需要计算 rowSpan 值来确定单元格合并关系。

五、单元格合并计算

Excel 导出中的单元格合并是一个重要功能,它能够让表格更加美观和易读。doMerges 函数负责计算所有需要合并的单元格区域:

function doMerges(arr) {
    let deep = arr.length;
    let merges = [];
    
    // 先处理横向合并(列合并)
    for (let y = 0; y < deep; y++) {
        let row = arr[y];
        let colSpan = 0;
        for (let x = 0; x < row.length; x++) {
            if (row[x] === '!$COL_SPAN_PLACEHOLDER') {
                row[x] = undefined;
                if(x + 1 === row.length){
                    merges.push({ s: { r: y, c: x - colSpan - 1 }, e: { r: y, c: x } });
                }
                colSpan++;
            } else if (colSpan > 0 && x > colSpan) {
                merges.push({ s: { r: y, c: x - colSpan - 1 }, e: { r: y, c: x - 1 } });
                colSpan = 0;
            } else {
                colSpan = 0;
            }
        }
    }
    
    // 再处理纵向合并(行合并)
    let colLength = arr[0].length;
    for (let x = 0; x < colLength; x++) {
        let rowSpan = 0;
        for (let y = 0; y < deep; y++) {
            if (arr[y][x] === '!$ROW_SPAN_PLACEHOLDER') {
                arr[y][x] = undefined;
                if(y + 1 === deep){
                    merges.push({ s: { r: y - rowSpan, c: x }, e: { r: y, c: x } });
                }
                rowSpan++;
            } else if (rowSpan > 0 && y > rowSpan) {
                merges.push({ s: { r: y - rowSpan - 1, c: x }, e: { r: y - 1, c: x } });
                rowSpan = 0;
            } else {
                rowSpan = 0;
            }
        }
    }
    return merges;
}

合并区域的表示使用 Excel 的单元格坐标,格式为 { s: { r: 起始行, c: 起始列 }, e: { r: 结束行, c: 结束列 } },其中行和列的索引都是从 0 开始的数字。

六、完整企业级示例:AJAX 数据源与复杂表头

在实际的企业级应用中,导出功能通常需要从后端 API 获取数据,并处理更加复杂的业务逻辑。以下是一个完整的示例,展示如何从 API 获取数据并导出为包含多级表头的 Excel 文件。

首先在 HTML 中定义导出按钮:

<button onclick="exportSummaryOfStaticsExcel()">导出保证金汇总统计</button>
<button onclick="exportPaidItemsExcel()">导出已缴纳保证金项目</button>

然后定义 API 请求和表头配置:

var baseUrl = "http://localhost:5000";

// 导出保证金汇总统计
function exportSummaryOfStaticsExcel() {
    let requestData = JSON.stringify({
        city: "",
        area: "",
        itemCategory: 0,
        itemStatus: 0
    });
    
    // 定义复杂的多级表头
    let revealList = [
        {
            name: "项目明细",
            child: [
                { name: "序号", prop: "serialNo" },
                { name: "区域", prop: "displayName" },
                { name: "项目总数", prop: "itemTotalNumber" },
                { name: "已缴纳保证金项目数", prop: "PaidItemCountSummary" },
                { name: "未缴纳保证金项目数", prop: "unPaidItemCount" },
                { name: "保证金落实率", prop: "implementationRate" }
            ]
        },
        {
            name: "保证金缴纳类别",
            prop: "paymentDepositType"
        },
        {
            name: "落实详情",
            child: [
                { name: "缴纳项目数", prop: "paidItemCount" },
                { name: "累计缴纳金额", prop: "totalPaidAmount" }
            ]
        },
        {
            name: "使用详情",
            child: [
                { name: "使用项目数", prop: "usedItemCount" },
                { name: "累计使用金额", prop: "totalUsedItemAmount" }
            ]
        },
        {
            name: "退还详情",
            child: [
                { name: "退还项目数", prop: "returnedItemCount" },
                { name: "退还总金额(元)", prop: "returnedAmount" }
            ]
        },
        {
            name: "保证金剩余详情",
            child: [
                { name: "保证金有效项目数", prop: "bjzValidItemCount" },
                { name: "保证金总余额(元)", prop: "totalBzjAmout" }
            ]
        }
    ];
    
    $.ajax({
        url: baseUrl + "/api/BEBzjStatistics/GetSummaryOfStatics",
        type: "post",
        data: requestData,
        contentType: "application/json",
        success: function (res) {
            let selectionData = res.data.list.items;
            let sheetName = "保证金汇总统计";
            exportExcel(selectionData, revealList, sheetName);
        },
        error: function (y) {}
    });
}

这个示例展示了企业级应用的典型场景:后端 API 返回的复杂嵌套数据、多级分组的表头结构、包含文件路径的字段处理等。

七、Vue 3 + Element UI 集成示例

如果项目使用的是 Vue 3 框架,可以将导出功能封装为 Vue 组件的方法。以下是结合 Element UI 表格的完整示例:

<div id="app">
    <el-button @click="exportData">导出</el-button>
    <el-table @selection-change="handleSelectionChange" :data="list" style="width: 100%">
        <el-table-column type="selection" width="55"></el-table-column>
        <el-table-column label="姓名" prop="name" align="center"></el-table-column>
        <el-table-column label="专业技能" align="center">
            <el-table-column label="前端" align="center">
                <el-table-column label="JavaScript" prop="js" align="center"></el-table-column>
                <el-table-column label="CSS" prop="css" align="center"></el-table-column>
            </el-table-column>
            <el-table-column label="后端" align="center">
                <el-table-column label="java" align="center">
                    <el-table-column label="nio" prop="nio" align="center"></el-table-column>
                    <el-table-column label="基础" prop="basic" align="center"></el-table-column>
                </el-table-column>
            </el-table-column>
        </el-table-column>
    </el-table>
</div>

Vue 组件中的导出方法实现与纯 JavaScript 版本类似,主要区别在于使用 this 访问组件的数据和方法:

const app = new Vue({
    el: "#app",
    data() {
        return {
            selectionData: [],
            list: [
                { name: '张三', js: '熟练', css: '一般', nio: '了解', basic: '精通' }
            ],
            revealList: [
                { name: '姓名', prop: 'name' },
                {
                    name: '专业技能',
                    child: [
                        {
                            name: '前端',
                            child: [
                                { name: 'JavaScript', prop: 'js' },
                                { name: 'CSS', prop: 'css' }
                            ]
                        },
                        {
                            name: '后端',
                            child: [
                                { name: 'java', prop: 'nio' }
                            ]
                        }
                    ]
                }
            ]
        }
    },
    methods: {
        handleSelectionChange(selection) {
            this.selectionData = selection;
        },
        exportData() {
            if (!Array.isArray(this.selectionData) || this.selectionData.length < 1) {
                this.$message({ type: 'error', message: '请选择需要导出的数据!' });
                return;
            }
            
            let sheetName = '数据导出';
            let excelHeader = this.buildHeader(this.revealList);
            let headerRows = excelHeader.length;
            let dataList = this.extractData(this.selectionData, this.revealList);
            excelHeader.push(...dataList, []);
            let merges = this.doMerges(excelHeader);
            let ws = this.aoa_to_sheet(excelHeader, headerRows);
            ws['!merges'] = merges;
            
            let workbook = {
                SheetNames: [sheetName],
                Sheets: {}
            };
            workbook.Sheets[sheetName] = ws;
            
            let wopts = {
                bookType: 'xlsx',
                bookSST: false,
                type: 'binary',
                cellStyles: true
            };
            
            let wbout = XLSX.write(workbook, wopts);
            let blob = new Blob([this.s2ab(wbout)], { type: "application/octet-stream" });
            this.openDownloadXLSXDialog(blob, sheetName + '.xlsx');
        }
    }
});

八、Excel 样式定制

为了让导出的 Excel 文件更加美观,可以对单元格的字体、边框、背景色、对齐方式等样式进行定制。aoa_to_sheet 函数中包含了样式定义的核心逻辑:

function aoa_to_sheet(data, headerRows) {
    const ws = {};
    const range = { s: { c: 10000000, r: 10000000 }, e: { c: 0, r: 0 } };
    
    for (let R = 0; R !== data.length; ++R) {
        for (let C = 0; C !== data[R].length; ++C) {
            // 更新数据范围
            if (range.s.r > R) range.s.r = R;
            if (range.s.c > C) range.s.c = C;
            if (range.e.r < R) range.e.r = R;
            if (range.e.c < C) range.e.c = C;
            
            // 定义单元格样式
            const cell = {
                v: data[R][C] || '',
                s: {
                    font: { name: "宋体", sz: 11, color: { auto: 1 } },
                    alignment: {
                        wrapText: 1,      // 自动换行
                        horizontal: "center",  // 水平居中
                        vertical: "center"     // 垂直居中
                    }
                }
            };
            
            // 表头样式:添加边框和背景色
            if (R < headerRows) {
                cell.s.border = {
                    top: { style: 'thin', color: { rgb: "000000" } },
                    left: { style: 'thin', color: { rgb: "000000" } },
                    bottom: { style: 'thin', color: { rgb: "000000" } },
                    right: { style: 'thin', color: { rgb: "000000" } }
                };
                cell.s.fill = {
                    patternType: 'solid',
                    fgColor: { rgb: 'DDD9C4' },
                    bgColor: { rgb: '8064A2' }
                };
            }
            
            // 设置单元格类型
            const cell_ref = XLSX.utils.encode_cell({ c: C, r: R });
            if (typeof cell.v === 'number') {
                cell.t = 'n';
            } else if (typeof cell.v === 'boolean') {
                cell.t = 'b';
            } else {
                cell.t = 's';
            }
            ws[cell_ref] = cell;
        }
    }
    
    if (range.s.c < 10000000) {
        ws['!ref'] = XLSX.utils.encode_range(range);
    }
    return ws;
}

通过修改 cell.s 对象中的属性,可以自定义字体(font)、边框(border)、填充色(fill)、对齐方式(alignment)等样式。表头行可以通过判断行索引 R < headerRows 来应用特殊的样式。

九、常见问题与解决方案

在前端导出 Excel 的实践中,可能会遇到一些常见问题。首先是数据量过大导致的性能问题,当导出数万条数据时,JavaScript 的处理速度可能变慢。解决方案是分页导出或者使用 Web Worker 在后台线程处理数据。其次是中文文件名的乱码问题,在某些浏览器中,直接使用中文文件名可能导致下载时出现乱码,可以对文件名进行 URL 编码处理。第三是单元格内容过长导致显示不全,需要开启自动换行功能并适当调整列宽。

对于包含文件路径的字段,需要特殊处理。例如,后端返回的附件路径可能是多个路径用分隔符连接的字符串,需要解析后拼接成完整的下载链接:

function buildFilePath(fileNames, header, rawData) {
    let value = "";
    if (fileNames.indexOf(header.prop) > -1) {
        var originFilePath = rawData[header.prop];
        var files = originFilePath.split("?");
        let finalFilePaths = "";
        for (let j = 0; j < files.length; j++) {
            finalFilePaths += baseUrl + "/" + files[j] + "\n";
        }
        value = finalFilePaths;
    }
    return value;
}

十、总结

前端导出 Excel 是一项非常实用的技术,能够显著提升用户体验并减轻服务器压力。通过 xlsx.js 库,我们可以轻松实现各种复杂的导出需求,包括多级表头、单元格合并、样式定制等功能。

本文详细介绍了从基础到高级的前端 Excel 导出方案,包括纯 JavaScript 实现、Vue 3 集成、企业级 AJAX 数据处理等场景。核心要点包括:使用嵌套对象配置定义多级表头、递归算法构建表头和数据、处理嵌套数据结构实现单元格合并、通过样式定制提升导出文件的美观度。

掌握这些技术后,前端开发者完全可以独立完成各种复杂的 Excel 导出需求,无需依赖后端实现。


作者:spike

分类: Nodejs

创作时间:2026-03-06

更新时间:2026-03-06