文件上传:使用 multer 和 @nestjs/platform-express 轻松搞定


文件上传:使用 multer 和 @nestjs/platform-express 轻松搞定

大家好,今天我们来聊聊 Web 开发中绕不开的一个功能——文件上传。无论是用户头像、图片分享,还是 Excel 导入,都需要处理文件上传。在 NestJS 中,文件上传的实现非常方便,因为底层直接集成了 Express 生态里最流行的 multer 中间件。你只需要引入几个内置的拦截器,就能轻松接收和处理上传的文件。今天我们就来完整地走一遍文件上传的流程。


一、文件上传的基本知识

在 HTTP 协议中,文件上传通常使用 multipart/form-data 格式的请求。这种格式会将请求体分成多个部分,每个部分有自己的头部和内容,文件数据放在其中一个或多个部分中。这种格式的好处是可以同时传输文件和普通的表单字段(比如文本输入)。

NestJS 通过 @nestjs/platform-express 包提供了对 multer 的封装,让你可以用拦截器的方式来处理文件上传。


二、安装依赖

如果你用的是默认的 Express 适配器,multer 已经作为 @nestjs/platform-express 的依赖被安装了,所以你不需要额外安装。但如果需要类型定义,可以安装 @types/multer

npm install --save-dev @types/multer

三、单文件上传

最常见的场景是上传单个文件。Nest 提供了 FileInterceptor 拦截器,它会从请求中取出指定字段的文件,然后挂到 req.file 上。你可以通过 @UploadedFile() 装饰器获取这个文件对象。

步骤:

  1. 在控制器方法上使用 @UseInterceptors(FileInterceptor('file')),其中 'file' 是表单中文件字段的名称。
  2. 方法参数中使用 @UploadedFile() file: Express.Multer.File 来接收文件。
  3. 其他非文件字段仍然通过 @Body() 获取。
import { Controller, Post, UseInterceptors, UploadedFile, Body } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';

@Controller('upload')
export class UploadController {
  @Post('single')
  @UseInterceptors(FileInterceptor('file'))
  uploadSingle(@UploadedFile() file: Express.Multer.File, @Body() body) {
    console.log(file);
    return { message: '文件上传成功', filename: file.originalname };
  }
}

默认情况下,multer 会把文件保存在内存中(作为 buffer),如果你想保存到磁盘,可以配置 deststorage 选项。

配置保存路径

FileInterceptor 可以接受第二个参数,是一个 multer 的选项对象:

@UseInterceptors(FileInterceptor('file', { dest: './uploads' }))

这样文件就会自动保存到 uploads 目录下,文件名是随机生成的。如果你想自定义文件名或存储逻辑,可以使用 diskStorage

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

@UseInterceptors(FileInterceptor('file', {
  storage: diskStorage({
    destination: './uploads',
    filename: (req, file, callback) => {
      const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
      const ext = extname(file.originalname);
      callback(null, file.fieldname + '-' + uniqueSuffix + ext);
    },
  }),
}))

四、多文件上传(同一个字段多个文件)

如果前端上传多个文件,且使用同一个字段名(比如 <input type="file" multiple>name="files"),你需要用 FilesInterceptor(注意是复数)。

@Post('multiple')
@UseInterceptors(FilesInterceptor('files', 10)) // 最多10个文件
uploadMultiple(@UploadedFiles() files: Array<Express.Multer.File>, @Body() body) {
  console.log(files);
  return { message: '多文件上传成功', count: files.length };
}

FilesInterceptor 的第一个参数是字段名,第二个参数是最大文件数量限制(可选)。


五、多字段文件上传

有时候一个表单里可能有多个不同的文件字段,比如头像字段 avatar 和背景图字段 background。这时候可以用 FileFieldsInterceptor,它允许你指定多个字段及其最大文件数。

@Post('fields')
@UseInterceptors(FileFieldsInterceptor([
  { name: 'avatar', maxCount: 1 },
  { name: 'background', maxCount: 1 },
]))
uploadFields(
  @UploadedFiles() files: { avatar?: Express.Multer.File[], background?: Express.Multer.File[] },
  @Body() body,
) {
  console.log(files.avatar?.[0]);
  console.log(files.background?.[0]);
  return { message: '多字段上传成功' };
}

@UploadedFiles() 返回的是一个对象,键是字段名,值是对应文件数组(即使 maxCount:1 也是数组,需要取第 0 项)。


六、任意字段文件上传(不指定字段名)

如果你不知道前端会传什么字段,或者想处理所有上传的文件,可以用 AnyFilesInterceptor。它会捕获请求中的所有文件,不管字段名是什么。

@Post('any')
@UseInterceptors(AnyFilesInterceptor({ dest: 'uploads/' }))
uploadAny(@UploadedFiles() files: Array<Express.Multer.File>, @Body() body) {
  console.log(files); // 所有文件都在这个数组里
  return { message: '任意文件上传成功' };
}

文件里给的例子就是用的 AnyFilesInterceptor


七、文件对象的属性

通过 @UploadedFile()@UploadedFiles() 拿到的文件对象是标准的 multer 文件对象,包含以下常用属性:

  • fieldname:表单字段名
  • originalname:原始文件名
  • encoding:文件编码
  • mimetype:文件 MIME 类型
  • size:文件大小(字节)
  • destination:保存路径(如果配置了 dest
  • filename:保存后的文件名
  • path:完整路径
  • buffer:文件内容的 Buffer(如果没有保存到磁盘)

八、文件验证和限制

你可以通过 multer 的 limits 选项来限制文件大小等:

@UseInterceptors(FileInterceptor('file', {
  limits: { fileSize: 2 * 1024 * 1024 }, // 限制 2MB
}))

如果需要根据文件类型进行过滤,可以使用 fileFilter 函数:

@UseInterceptors(FileInterceptor('file', {
  fileFilter: (req, file, callback) => {
    if (!file.originalname.match(/\.(jpg|jpeg|png|gif)$/)) {
      return callback(new Error('只允许上传图片文件'), false);
    }
    callback(null, true);
  },
}))

注意:fileFilter 中如果回调错误,会抛给 Nest 的异常过滤器。你可以自定义异常过滤器来捕获这些错误并返回友好的响应。


九、结合 DTO 和 ValidationPipe

有时候你可能需要同时验证非文件字段,比如上传图片时附带一个描述字段。你可以定义一个 DTO,然后在 @Body() 中使用,配合 ValidationPipe

export class UploadDto {
  @IsString()
  @IsOptional()
  description: string;
}

@Post('single')
@UseInterceptors(FileInterceptor('file'))
uploadSingle(
  @UploadedFile() file: Express.Multer.File,
  @Body() body: UploadDto,
) {
  // body 已经过 ValidationPipe 验证
}

记得在全局或控制器上启用 ValidationPipe。


十、错误处理

当上传出错时(比如文件太大、文件类型不对、超过文件数量限制),multer 会抛出错误。这些错误会被 Nest 的异常层捕获,默认返回 500 错误。你可以通过自定义异常过滤器来捕获这些错误,并返回更友好的响应。

import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';
import { MulterError } from 'multer';

@Catch(MulterError)
export class MulterExceptionFilter implements ExceptionFilter {
  catch(exception: MulterError, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    let message = '文件上传错误';
    if (exception.code === 'LIMIT_FILE_SIZE') {
      message = '文件大小超过限制';
    } else if (exception.code === 'LIMIT_FILE_COUNT') {
      message = '文件数量超过限制';
    } else if (exception.code === 'LIMIT_UNEXPECTED_FILE') {
      message = '意外字段';
    }
    response.status(400).json({
      statusCode: 400,
      message,
    });
  }
}

然后在控制器或全局使用 @UseFilters(MulterExceptionFilter)


十一、关于文件和字段的顺序

有一点需要注意:@Body() 不能用来获取文件字段的值,因为 multipart/form-data 中文件字段是单独的部分,不会被解析到 body 中。所有文件字段都应该通过 @UploadedFile()@UploadedFiles() 获取,而其他普通字段通过 @Body() 获取。


总结

NestJS 的文件上传功能基于成熟的 multer 库,通过几个内置拦截器就能轻松处理各种上传场景。你可以:

  • FileInterceptor 处理单文件
  • FilesInterceptor 处理同一个字段的多个文件
  • FileFieldsInterceptor 处理多个不同字段的文件
  • AnyFilesInterceptor 处理所有文件

同时可以通过 multer 的选项配置存储、限制、过滤,并结合 DTO 和 ValidationPipe 进行数据验证。错误处理也可以通过自定义异常过滤器来优化。

声明:麋鹿与鲸鱼|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - 文件上传:使用 multer 和 @nestjs/platform-express 轻松搞定


Carpe Diem and Do what I like