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

使用NestJS+fast-csv监听流事件时返回空数据的问题

问题

在使用NestJS结合fast-csv库计算导出文件流的大小时遇到异常:原本代码可正常导出CSV文件,但启用stream的data事件监听器计算流大小后,虽能获取到流大小数值,但接口返回的CSV文件内容为空。

服务代码

import { format } from 'fast-csv';

async export() {
    const stream = format({ headers: true });

    stream.write(['id', 'first_name', 'last_name', 'is_active']);

    let size = 0;

    // 启用这段代码后出现问题
    // stream.on('data', (chunk) => {
    //   size += chunk.length;
    // });

    // stream.on('end', () => {
    //   console.log(size);
    // });

    await this.getData(stream)().finally(() => {
      stream.end();
    });

    return stream ? { stream, size } : null;
  }

  private getData = (stream: Transform) => async () => {
    for (let i = 0; i < 1; i++) {
      const users = await User.find();

      if (users.length === 0) {
        break;
      }

      for (let i = 0; i < users.length; i++) {
        const row = users[i];

        stream.write([row.id, row.firstName, row.lastName, row.isActive]);
      }
    }
  };

控制器代码

@Post('export')
  async exporrt() {
    const data = await this.appService.export();

    console.log('size', data.size);

    return new StreamableFile(data.stream, {
      disposition: `attachment; filename=test.csv`,
      type: 'text/csv; charset=utf-8',
    });
  }
解决方案

问题根源

Node.js的流在绑定data事件后会自动切换到流动模式,如果只监听data事件却不消费(将数据传递给下游),流中的chunk会直接被丢弃,最终返回给客户端的流自然为空。


方法一:通过PassThrough中转流实现统计

创建一个PassThrough流作为中转,让fast-csv的输出先流经这个中转流,在中转流上监听data事件计算大小,同时将中转流返回给客户端,既保证数据不丢失,又能完成大小统计。

修改后的服务代码:

import { format } from 'fast-csv';
import { PassThrough } from 'stream';

async export() {
    const csvStream = format({ headers: true });
    const passThrough = new PassThrough(); // 中转流
    let size = 0;

    // 在中转流上监听data事件累加大小
    passThrough.on('data', (chunk) => {
      size += chunk.length;
    });

    passThrough.on('end', () => {
      console.log('文件总字节数:', size);
    });

    // 将csv流的数据导入中转流
    csvStream.pipe(passThrough);

    csvStream.write(['id', 'first_name', 'last_name', 'is_active']);

    await this.getData(csvStream)().finally(() => {
      csvStream.end();
    });

    // 返回中转流和统计的大小
    return { stream: passThrough, size };
  }

  // getData方法保持原有逻辑不变
  private getData = (stream: Transform) => async () => {
    for (let i = 0; i < 1; i++) {
      const users = await User.find();

      if (users.length === 0) {
        break;
      }

      for (let i = 0; i < users.length; i++) {
        const row = users[i];

        stream.write([row.id, row.firstName, row.lastName, row.isActive]);
      }
    }
  };

方法二:自定义Transform流封装统计逻辑

通过自定义Transform流,在处理每个chunk时自动累加大小,同时将chunk传递给下游,这种方式更符合Node.js流的处理规范。

修改后的服务代码:

import { format } from 'fast-csv';
import { Transform } from 'stream';

async export() {
    const csvStream = format({ headers: true });
    let size = 0;

    // 自定义Transform流,统计大小同时传递数据
    const sizeCounter = new Transform({
      transform(chunk, encoding, callback) {
        size += chunk.length;
        callback(null, chunk); // 将chunk传递给下游流
      }
    });

    sizeCounter.on('end', () => {
      console.log('文件总字节数:', size);
    });

    csvStream.pipe(sizeCounter);

    csvStream.write(['id', 'first_name', 'last_name', 'is_active']);

    await this.getData(csvStream)().finally(() => {
      csvStream.end();
    });

    return { stream: sizeCounter, size };
  }

注意事项

  • 禁止直接在返回给客户端的流上监听data事件却不消费数据,这是导致数据丢失的核心原因。
  • 统计得到的size是字节数,若需要转换为KB/MB等可读单位,可自行添加单位转换逻辑。

内容的提问来源于stack exchange,提问作者Đoàn Đức Bảo

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 05:25:06