You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Node.js/Express导出数据库数据为CSV并触发浏览器下载的最佳实践及问题排查

Node.js/Express 数据库数据导出CSV并触发浏览器下载的最佳实践

核心问题分析

你的代码无法触发下载的关键原因:

  1. 提前调用res.end()截断了响应流,导致CSV数据未完整传输
  2. 错误使用process.exit()(会直接关闭整个Express服务器,仅适用于脚本场景)
  3. 手动拼接CSV时未处理字段转义(如逗号、引号、换行),同时响应头设置冗余且不规范

最佳实践方案

1. 优先使用成熟CSV库处理格式

推荐使用fast-csv这类库,自动处理表头生成、字段转义(逗号、引号、换行),避免手动拼接的格式错误。

2. 规范设置响应头

用Express内置方法替代手动设置Header,兼容性更强:

  • res.type('text/csv'):统一设置Content-Type
  • res.attachment(filename):自动设置Content-Disposition: attachment,触发浏览器下载对话框,同时自动处理文件名编码
  • 添加Content-Encoding: utf-8避免中文乱码

3. 流式处理数据

利用数据库的迭代器逐行读取数据,避免一次性加载大量数据到内存,适合大数据量导出场景。

4. 禁止提前结束响应

通过流的pipe机制自动处理响应结束,不要手动调用res.end()。

修正后的代码(基于fast-csv)

import csv from 'fast-csv';
import { Response } from 'express';
import { StatusCodes } from 'http-status-codes';
// 替换为你的类型导入
import { IGetListRequest, SqliteError, AppServerError } from './your-types';

const csvExport = (
    sql: string,
    params: IGetListRequest,
    user: string,
    filename: string,
    res: Response
) => {
    try {
        // 禁用缓存
        res.setHeader('Expires', 'Tue, 03 Jul 2001 06:00:00 GMT');
        res.setHeader(
            'Cache-Control',
            'max-age=0, no-cache, must-revalidate, proxy-revalidate'
        );
        res.setHeader('Last-Modified', `${new Date().toUTCString()} GMT`);

        // 设置CSV响应头
        res.type('text/csv');
        res.setHeader('Content-Encoding', 'utf-8');
        const safeFilename = filename.replace(/[.: ]/g, '-').toLowerCase() + '.csv';
        res.attachment(safeFilename);

        // 创建CSV流,自动生成表头并处理字段转义
        const csvStream = csv.format({ headers: true });
        // 管道到响应,流结束时自动关闭响应
        csvStream.pipe(res);

        // 流式读取数据库数据
        const stmt = db.conn.prepare(sql);
        const dataIterator = stmt.iterate({
            user,
            from: params.from.toISOString(),
            to: params.to ? params.to.toISOString() : params.from.toISOString(),
        });

        // 逐行写入CSV流
        for (const record of dataIterator) {
            csvStream.write(record);
        }

        // 结束CSV流,触发响应完成
        csvStream.end();

        // 监听CSV流错误
        csvStream.on('error', (err) => {
            if (!res.headersSent) {
                res.status(StatusCodes.INTERNAL_SERVER_ERROR).send('CSV生成失败');
            }
        });

    } catch (err) {
        if (!res.headersSent) {
            if (err instanceof SqliteError) {
                res.status(StatusCodes.INTERNAL_SERVER_ERROR).send(err.message);
            } else {
                res.status(StatusCodes.INTERNAL_SERVER_ERROR).send('服务器内部错误');
            }
        }
    }
};

手动处理CSV的修正版(无第三方库)

如果不想依赖第三方库,必须处理字段转义逻辑:

import { PassThrough } from 'stream';
import { Response } from 'express';
import { StatusCodes } from 'http-status-codes';
import { IGetListRequest, SqliteError } from './your-types';

// CSV字段转义函数:处理逗号、引号、换行
const escapeCsvField = (field: any): string => {
    const str = String(field);
    if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) {
        return `"${str.replace(/"/g, '""')}"`;
    }
    return str;
};

const csvExport = (
    sql: string,
    params: IGetListRequest,
    user: string,
    filename: string,
    res: Response
) => {
    try {
        // 禁用缓存
        res.setHeader('Expires', 'Tue, 03 Jul 2001 06:00:00 GMT');
        res.setHeader(
            'Cache-Control',
            'max-age=0, no-cache, must-revalidate, proxy-revalidate'
        );
        res.setHeader('Last-Modified', `${new Date().toUTCString()} GMT`);

        // 设置响应头
        res.type('text/csv');
        res.setHeader('Content-Encoding', 'utf-8');
        const safeFilename = filename.replace(/[.: ]/g, '-').toLowerCase() + '.csv';
        res.attachment(safeFilename);

        const passThrough = new PassThrough();
        passThrough.pipe(res);

        const stmt = db.conn.prepare(sql);
        const dataIterator = stmt.iterate({
            user,
            from: params.from.toISOString(),
            to: params.to ? params.to.toISOString() : params.from.toISOString(),
        });

        let headersWritten = false;

        for (const record of dataIterator) {
            if (!headersWritten) {
                // 写入表头
                const headers = Object.keys(record).map(escapeCsvField);
                passThrough.write(headers.join(',') + '\r\n');
                headersWritten = true;
            }
            // 写入数据行
            const values = Object.values(record).map(escapeCsvField);
            passThrough.write(values.join(',') + '\r\n');
        }

        passThrough.end();

        passThrough.on('error', (err) => {
            if (!res.headersSent) {
                res.status(StatusCodes.INTERNAL_SERVER_ERROR).send('CSV生成失败');
            }
        });

    } catch (err) {
        if (!res.headersSent) {
            if (err instanceof SqliteError) {
                res.status(StatusCodes.INTERNAL_SERVER_ERROR).send(err.message);
            } else {
                res.status(StatusCodes.INTERNAL_SERVER_ERROR).send('服务器内部错误');
            }
        }
    }
};

关键注意事项

  • 绝对不要在Express服务中调用process.exit(),会直接关闭整个服务器
  • 必须确保响应头在发送任何数据前设置完成
  • 大数据量场景下,流式处理是唯一可行的方案,避免内存溢出

内容的提问来源于stack exchange,提问作者Templar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.14 07:45:34