Skip to content

当然!我们来探讨一项处理大文件时至关重要的技术:文件流式传输 (Streaming Files)。这就像是为你的应用接上了一根高效、节能的“高速水管”,专门用来输送海量的数据。

为你的应用接上“高速水管”:NestJS 文件流式传输完全指南

想象一下你的应用是一位慷慨的图书管理员,他需要将一本非常非常厚的《世界百科全书》(一个巨大的文件,比如 4GB 的高清视频或大型日志文件)递给一位读者(客户端)。

没有流式传输的“大力士”模式:

  • 图书管理员需要先用尽全力,将整本几百公斤重的百科全书从书架上抱起来,完整地托在手里(将整个文件一次性读入服务器内存 RAM)。
  • 这不仅会耗尽管理员的所有力气(服务器内存被占满,可能导致崩溃),而且在他完全抱起书之前,读者只能眼巴巴地干等。
  • 然后,他再用同样的方式,将这本书一次性地递给读者。

这种方式对于小册子(小文件)来说没问题,但对于大部头书籍,显然是低效且危险的。

引入流式传输的“高速水管”模式:

  • 图书管理员不再去抱书,而是在百科全书和读者之间接上了一根透明的、高速的“内容传输水管” (Stream)
  • 他打开开关,书的内容(数据)就源源不断地、一页一页地(一小块一小块地,即 chunk)通过水管流向读者。
  • 优点显而易见
    1. 内存效率极高:图书管理员(服务器)在任何时刻都只需要处理正在通过水管的那一小部分内容,完全不需要把整本书都抱在手里。
    2. 更快的首字节时间 (TTFB):读者几乎在瞬间就能看到书的第一页内容,而不是等待整本书都被“抱起来”之后。这极大地提升了用户体验。
    3. 处理无限大小的文件成为可能:理论上,只要水管不断,你可以传输任意大小的文件。

NestJS 提供了一个非常方便的辅助类 StreamableFile,让实现文件流式传输变得异常简单。

1. 基础入门:从本地磁盘接上“水管”

这是最常见的场景:用户请求下载一个存储在服务器本地磁盘上的文件。

前置知识:Node.js 的 fs.createReadStream

这是 Node.js 内置的文件系统 (fs) 模块中的一个核心函数。它的作用就是创建一个可读流 (Readable Stream),也就是我们比喻中的“水管”的源头。它会打开一个文件,并准备好按需、一小块一小块地读取其内容。

src/files/files.controller.ts```typescript import { Controller, Get, Res } from '@nestjs/common'; import { createReadStream } from 'fs'; import { join } from 'path'; import { StreamableFile } from '@nestjs/common'; import type { Response } from 'express';

@Controller('files') export class FilesController { @Get('download/local') downloadLocalFile(): StreamableFile { // 1. 使用 fs.createReadStream() 创建一个文件读取流 const fileStream = createReadStream(join(process.cwd(), 'package.json'));

// 2. 将这个流包装在 StreamableFile 中并返回
return new StreamableFile(fileStream);

} }

**`StreamableFile` 的魔力**:
当你从 Controller 返回一个 `StreamableFile` 的实例时,NestJS 会在后台为你做很多事情。它会识别出这是一个流,然后自动地将这个流“管道连接” (`pipe`) 到 HTTP 响应流上。这意味着文件内容会直接、高效地流向客户端。

但是,上面的代码有一个小问题:浏览器收到文件后,不知道该怎么处理它。它不知道文件名,也不知道文件类型,可能会直接在页面上显示文本内容。我们需要提供更多的“元信息”。

### 2. 增加“包裹标签”:设置响应头 (Headers)

为了让浏览器正确地将文件作为附件下载,我们需要设置两个重要的 HTTP 响应头:
*   `Content-Type`: 告诉浏览器这是一个什么类型的文件(如 `application/json`, `image/jpeg`)。
*   `Content-Disposition`: 告诉浏览器如何处理这个文件。`attachment; filename="package.json"` 会触发浏览器的下载对话框。

我们可以通过注入 `@Res()` 来设置这些头。

**`src/files/files.controller.ts` (添加 Headers)**
```typescript
@Get('download/local-with-headers')
downloadWithHeaders(@Res({ passthrough: true }) res: Response): StreamableFile {
  const fileStream = createReadStream(join(process.cwd(), 'package.json'));

  // 1. 设置响应头
  res.set({
    'Content-Type': 'application/json',
    'Content-Disposition': 'attachment; filename="package.json"',
  });

  // 2. 依然返回 StreamableFile,NestJS 会处理后续的流传输
  return new StreamableFile(fileStream);
}
  • @Res({ passthrough: true }): 再次强调,这个选项至关重要!它告诉 NestJS:“我只想用 res 对象来设置一下‘包裹标签’(Headers),但‘包裹’的实际寄送工作(流式传输)还是请你来完成。”

3. 企业级方案:从云存储接上“洲际水管” (以 AWS S3 为例)

在生产环境中,文件通常存储在云端(如 AWS S3)。从云端流式传输文件到客户端是更常见的需求。我们的 NestJS 应用在这里扮演一个安全的中间人角色:它负责验证用户权限,然后直接将云存储的“水管”对接到用户的“水龙头”。

这个流程中,文件数据完全不经过你的服务器内存!

第一步:准备好你的 S3Service (参考文件上传章节)

你需要一个能与 S3 交互的服务。

第二步:创建一个能返回 S3 对象流的服务方法

AWS SDK v3 的一个绝佳特性是,当你获取一个对象时,其响应体 Body 本身就是一个可读流

src/s3/s3.service.ts

typescript
import { Injectable } from '@nestjs/common';
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
// ...

@Injectable()
export class S3Service {
  // ... constructor ...

  async getFileStream(key: string) {
    const bucket = this.configService.get<string>('AWS_S3_BUCKET_NAME');

    const command = new GetObjectCommand({
      Bucket: bucket,
      Key: key,
    });

    const response = await this.s3Client.send(command);

    // response.Body 就是一个可读流 (ReadableStream)!
    // 我们需要将其返回,以便 Controller 可以使用
    return response.Body;
  }
}

第三步:在 Controller 中调用服务并返回 StreamableFile

typescript
// files.controller.ts
import { Readable } from 'stream';

@Controller('files')
export class FilesController {
  constructor(private readonly s3Service: S3Service) {}

  @Get('download/from-s3')
  async downloadFromS3(
    @Res({ passthrough: true }) res: Response
  ): Promise<StreamableFile> {
    const fileKey = 'some-large-video.mp4'; // 假设这是你要下载的文件在 S3 上的 Key
    const s3Stream = await this.s3Service.getFileStream(fileKey);

    // 你可能需要从 S3 获取元数据来设置正确的头信息
    res.set({
      'Content-Type': 'video/mp4',
      'Content-Disposition': `attachment; filename="${fileKey}"`,
    });

    // 将从 S3 获取的流包装起来并返回
    // 需要断言为 Readable,因为 SDK 的类型可能不直接匹配
    return new StreamableFile(s3Stream as Readable);
  }
}

这个方案完美地展示了流式传输的威力。数据从 S3,通过你的 NestJS 应用(只做了权限校验和头信息设置),直接流向了最终用户,你的服务器内存占用几乎为零,实现了高效、可扩展的大文件分发。

总结

流式传输是处理大文件的关键技术,它能极大地优化应用的性能和资源使用。

当你想要...你应该...核心概念
高效地将一个大文件发送给客户端返回一个 StreamableFile 实例StreamableFile 是 NestJS 的流式响应辅助类。
触发浏览器的下载行为设置 Content-Disposition 响应头通过 @Res({ passthrough: true }) 来注入并设置响应头。
从本地磁盘流式传输fs.createReadStream() 的结果包装在 StreamableFileNode.js 内置的文件系统流。
从云存储流式传输 (企业级)获取云存储 SDK 返回的响应体流,并将其包装在 StreamableFile零内存占用,将 NestJS 作为高效的、安全的代理。

不要再用“大力士”模式去搬运你的大文件了。学会使用 StreamableFile,为你的应用接上这根轻巧而强大的“高速水管”吧!