Skip to content

好的,我们来攻克一个在 Web 应用中极为常见的需求:文件上传 (File Upload)。这就像是为你的应用开设一个“收件窗口”,让用户可以安全、高效地将文件(如头像、文档、视频)递交给你。

为你的应用开设“收件窗口”:NestJS 文件上传完全指南

想象你的应用是一个大型的档案管理中心。

你需要一个安全、规范的“收件窗口”,来接收来自各方的文件材料。这个窗口需要具备以下能力:

  • 识别不同类型的文件:能处理单个文件、多个文件,甚至一个表单中既有文件又有文本数据的情况。
  • 安全检查:能限制上传文件的类型(比如只接收图片)和大小,防止恶意文件攻击。
  • 妥善保管:能将接收到的文件存储到指定的位置(服务器磁盘、云存储等)。

在 HTTP 协议中,文件上传通常是通过 multipart/form-data 这种特殊的 Content-Type 来完成的。NestJS 利用其底层的 Web 框架(如 Express)的中间件生态,特别是 Multer,来优雅地处理这种复杂的数据格式。

1. 基础入门:使用内置的 FileInterceptor

NestJS 提供了一系列内置的、基于 Multer 的拦截器 (Interceptors),让处理文件上传变得像处理普通 JSON Body 一样简单。

第一步:搭建“单文件收件窗口” (FileInterceptor)

这是最基础的场景:用户上传一个头像。

src/files/files.controller.ts

typescript
import {
  Controller,
  Post,
  UseInterceptors,
  UploadedFile,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';

@Controller('files')
export class FilesController {
  // 1. 使用 @UseInterceptors() 应用 FileInterceptor
  @Post('upload/single')
  @UseInterceptors(FileInterceptor('avatar')) // 'avatar' 是客户端上传文件时使用的字段名 (field name)
  uploadSingleFile(@UploadedFile() file: Express.Multer.File) {
    // 2. 使用 @UploadedFile() 装饰器来获取文件对象

    console.log('收到一个文件:', file);

    // file 对象包含了文件的所有信息:
    // {
    //   fieldname: 'avatar',
    //   originalname: 'my-profile-pic.jpg',
    //   encoding: '7bit',
    //   mimetype: 'image/jpeg',
    //   destination: './uploads', // 如果配置了存储
    //   filename: '...',
    //   path: '...',
    //   size: 12345
    // }

    if (!file) {
      return { message: '没有文件被上传!' };
    }

    return {
      message: '文件上传成功!',
      filename: file.originalname,
      size: file.size,
    };
  }
}

工作流程:

  1. 客户端(如 Postman 或前端表单)发送一个 POST 请求,Content-Typemultipart/form-data,其中包含一个名为 avatar 的文件字段。
  2. FileInterceptor('avatar') 拦截这个请求。它就像一位专门负责接收“头像”文件的门卫。
  3. 它会处理 multipart 数据流,将名为 avatar 的文件提取出来。默认情况下,Multer 会将文件暂存在内存中
  4. 然后,@UploadedFile() 装饰器将这个被提取出的文件对象注入到你的 file 参数中。
  5. 你的 uploadSingleFile 方法现在可以轻松地访问文件的所有信息了。

第二步:扩展“收件窗口”的能力

  • 多文件上传 (FilesInterceptor)
    typescript
    @Post('upload/multiple')
    @UseInterceptors(FilesInterceptor('photos', 10)) // 'photos' 是字段名,10 是最大文件数量
    uploadMultipleFiles(@UploadedFile() files: Array<Express.Multer.File>) {
      console.log('收到了多个文件:', files);
      return { message: `${files.length} 个文件上传成功!` };
    }
  • 混合数据上传 (FileFieldsInterceptor) 一个表单里既有头像,又有相册图片。
    typescript
    @Post('upload/mixed')
    @UseInterceptors(FileFieldsInterceptor([
      { name: 'avatar', maxCount: 1 },
      { name: 'gallery', maxCount: 8 },
    ]))
    uploadMixedFiles(
      @UploadedFiles() files: { avatar?: Express.Multer.File[], gallery?: Express.Multer.File[] },
    ) {
      console.log('头像:', files.avatar);
      console.log('相册:', files.gallery);
      return { message: '混合文件上传成功!' };
    }

2. 添加“安全检查”:验证文件

直接接收任何文件是非常危险的。我们需要对文件进行验证。NestJS 提供了强大的内置 Pipe 来实现这一点。

src/files/files.controller.ts (添加验证)

typescript
import { ParseFilePipe, FileTypeValidator, MaxFileSizeValidator } from '@nestjs/common';

@Post('upload/validated')
@UseInterceptors(FileInterceptor('document'))
uploadAndValidate(
  @UploadedFile(
    // 1. 在 @UploadedFile() 装饰器中添加一个 ParseFilePipe
    new ParseFilePipe({
      // 2. 定义一系列验证器
      validators: [
        // a. 验证文件大小 (比如最大 1 MB)
        new MaxFileSizeValidator({ maxSize: 1024 * 1024 }),
        // b. 验证文件类型 (只接受 JPEG 或 PNG 图片)
        new FileTypeValidator({ fileType: '.(png|jpeg|jpg)' }),
      ],
    }),
  ) file: Express.Multer.File,
) {
  // 如果代码能执行到这里,说明文件已经通过了所有验证
  return { message: '经过验证的文件上传成功!' };
}

如果上传的文件不符合这些规则,ParseFilePipe 会自动抛出一个 BadRequestException,并返回清晰的错误信息,你的 Controller 方法根本不会被执行。

3. “文件归档”:配置存储引擎

默认情况下,文件被暂存在内存中。对于大文件或者生产环境,这是不可接受的。我们需要将文件保存到服务器的磁盘上。

这需要在 FileInterceptor 的第二个参数中配置 storage 选项。

src/files/files.controller.ts (配置磁盘存储)

typescript
import { diskStorage } from 'multer';
import { extname } from 'path';

@Post('upload/to-disk')
@UseInterceptors(FileInterceptor('file', {
  // 1. 配置存储引擎为磁盘存储
  storage: diskStorage({
    // a. 定义文件存储的目标路径
    destination: './uploads',
    // b. 自定义文件名
    filename: (req, file, cb) => {
      const randomName = Array(32).fill(null).map(() => (Math.round(Math.random() * 16)).toString(16)).join('');
      return cb(null, `${randomName}${extname(file.originalname)}`);
    },
  }),
}))
uploadToDisk(@UploadedFile() file: Express.Multer.File) {
  console.log('文件已保存到磁盘:', file.path);
  return { message: '文件已保存到磁盘!', path: file.path };
}

现在,上传的文件会被自动保存到项目根目录下的 uploads 文件夹中,并使用一个随机生成的文件名以避免冲突。

4. 企业级方案:集成云存储 (以 AWS S3 为例)

在现代的、可伸缩的应用中,将文件直接存储在服务器磁盘上通常不是一个好主意。它会使应用状态化,难以进行水平扩展和负载均衡。将文件上传到云存储服务(如 AWS S3, Google Cloud Storage, 阿里云 OSS)是更专业的做法。

NestJS 原生的文件上传功能并不直接支持云存储,但我们可以通过自定义 storage 引擎或者在 Service 层手动上传来实现。

方案一:在 Service 层手动上传

这是最灵活、最解耦的方式。Controller 只负责接收文件到内存,然后将文件 buffer 传递给 Service,由 Service 负责与云存储 SDK 交互。

第一步:安装 AWS SDK

bash
npm install @aws-sdk/client-s3

第二步:创建一个云存储服务

src/s3/s3.service.ts

typescript
import { Injectable } from '@nestjs/common';
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class S3Service {
  private readonly s3Client: S3Client;

  constructor(private readonly configService: ConfigService) {
    this.s3Client = new S3Client({
      region: this.configService.get<string>('AWS_S3_REGION'),
      credentials: {
        accessKeyId: this.configService.get<string>('AWS_ACCESS_KEY_ID'),
        secretAccessKey: this.configService.get<string>(
          'AWS_SECRET_ACCESS_KEY'
        ),
      },
    });
  }

  async uploadFile(file: Express.Multer.File, key: string) {
    const bucket = this.configService.get<string>('AWS_S3_BUCKET_NAME');

    await this.s3Client.send(
      new PutObjectCommand({
        Bucket: bucket,
        Key: key, // 文件在 S3 上的路径/名称
        Body: file.buffer, // 文件内容
        ContentType: file.mimetype, // 文件类型
      })
    );

    // 返回文件的公共访问 URL
    return `https://${bucket}.s3.amazonaws.com/${key}`;
  }
}

第三步:在 Controller 和 Service 中调用

typescript
// files.controller.ts
@Post('upload/to-s3')
@UseInterceptors(FileInterceptor('file')) // 只接收文件到内存
async uploadToS3(@UploadedFile() file: Express.Multer.File) {
  // 调用 service 层的方法来处理上传
  return this.filesService.uploadFileToS3(file);
}

// files.service.ts
@Injectable()
export class FilesService {
  constructor(private readonly s3Service: S3Service) {}

  async uploadFileToS3(file: Express.Multer.File) {
    const key = `uploads/${Date.now()}-${file.originalname}`;
    const url = await this.s3Service.uploadFile(file, key);
    // 在这里,你可能还想将文件的 URL 和其他信息存入你的数据库
    // ...
    return { url };
  }
}
```这种方式将文件处理逻辑(上传到 S3、保存记录到数据库)都封装在了 `Service` 层,保持了 `Controller` 的整洁,是企业级应用中非常推荐的模式。

### 总结

文件上传是 Web 开发的基础功能,NestJS 通过其拦截器和管道系统提供了强大而灵活的解决方案。

| 当你想要... | 你应该使用... | 核心概念 |
| :--- | :--- | :--- |
| **快速处理单个或多个文件上传** | **`FileInterceptor`, `FilesInterceptor`** | **拦截器 (Interceptor)** |
| **确保上传的文件类型和大小合规** | **`ParseFilePipe`** 及其内置的验证器 | **管道 (Pipe)** |
| **将文件保存到服务器磁盘** | 在拦截器中配置 **`diskStorage`** | **存储引擎 (Storage Engine)** |
| **将文件上传到云存储 (企业级方案)**| **接收文件到内存,在 Service 层调用 SDK 手动上传** | **职责分离**,保持 Controller 轻量 |

从基础的磁盘存储到专业的云存储方案,掌握这些文件上传技术,你就能为你的应用打造一个既安全又高效的“收件窗口”。