好的,我们来攻克一个在 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
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,
};
}
}
工作流程:
- 客户端(如 Postman 或前端表单)发送一个
POST
请求,Content-Type
为multipart/form-data
,其中包含一个名为avatar
的文件字段。 FileInterceptor('avatar')
拦截这个请求。它就像一位专门负责接收“头像”文件的门卫。- 它会处理
multipart
数据流,将名为avatar
的文件提取出来。默认情况下,Multer 会将文件暂存在内存中。 - 然后,
@UploadedFile()
装饰器将这个被提取出的文件对象注入到你的file
参数中。 - 你的
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
(添加验证)
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
(配置磁盘存储)
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
npm install @aws-sdk/client-s3
第二步:创建一个云存储服务
src/s3/s3.service.ts
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 中调用
// 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 轻量 |
从基础的磁盘存储到专业的云存储方案,掌握这些文件上传技术,你就能为你的应用打造一个既安全又高效的“收件窗口”。