Nest.js 使用 multer 实现文件上传

项目创建与配置

新建 nest 项目:

bash 复制代码
nest new nest-multer-upload -p npm

安装下 multer 的 ts 类型的包:

bash 复制代码
npm install @types/multer -D

让 nest 服务支持跨域:

单文件上传

添加一个 handler:

typescript 复制代码
@Post('aaa')
@UseInterceptors(
  FileInterceptor('aaa', {
    dest: 'uploads',
  }),
)
uploadFile(@UploadedFile() file: Express.Multer.File, @Body() body) {
  console.log('body', body);
  console.log('file', file);
}

这里使用 FileInterceptor 提取请求中的 aaa 字段,并通过 UploadedFile 装饰器将其作为参数传递。

当我们运行 nest start --watch 的时候,uploads 文件夹就会创建。

前端代码:

html 复制代码
<!DOCTYPE html>
<html lang="en">
	<head>
		<script src="https://unpkg.com/axios@0.24.0/dist/axios.min.js"></script>
	</head>
	<body>
		<input id="fileInput" type="file" multiple />
		<script>
			const fileInput = document.querySelector('#fileInput')

			async function formData() {
				const data = new FormData()
				data.set('name', 'Yun')
				data.set('age', 20)
				data.set('aaa', fileInput.files[0])

				const res = await axios.post('http://localhost:3000/aaa', data)
				console.log(res)
			}

			fileInput.onchange = formData
		</script>
	</body>
</html>

服务端就打印了 file 对象和 body 字段,并且文件也保存到了 uploads 目录:

多文件上传

typescript 复制代码
@Post('bbb')
@UseInterceptors(
  FilesInterceptor('bbb', 3, {
    dest: 'uploads',
  }),
)
uploadFiles(
  @UploadedFiles() files: Array<Express.Multer.File>,
  @Body() body,
) {
  console.log('body', body);
  console.log('files', files);
}

把 FileInterceptor 换成 FilesInterceptor,把 UploadedFile 换成 UploadedFiles,都是多加一个 s。

前端代码:

typescript 复制代码
async function formData2() {
  const data = new FormData()
  data.set('name', 'Yun')
  data.set('age', 20)
  ;[...fileInput.files].forEach(item => {
    data.append('bbb', item)
  })

  const res = await axios.post('http://localhost:3000/bbb', data, {
    headers: { 'content-type': 'multipart/form-data' },
  })
  console.log(res)
}

这样就可以上传多文件了:

如果有多个文件的字段,和 multer 里类似,使用这种方式来指定:

typescript 复制代码
@Post('ccc')
@UseInterceptors(FileFieldsInterceptor([
    { name: 'aaa', maxCount: 2 },
    { name: 'bbb', maxCount: 3 },
], {
    dest: 'uploads'
}))
uploadFileFields(@UploadedFiles() files: { aaa?: Express.Multer.File[], bbb?: Express.Multer.File[] }, @Body() body) {
    console.log('body', body);
    console.log('files', files);
}

前端代码:

typescript 复制代码
async function formData3() {
  const data = new FormData()
  data.set('name', 'Yun')
  data.set('age', 20)
  data.append('aaa', fileInput.files[0])
  data.append('aaa', fileInput.files[1])
  data.append('bbb', fileInput.files[2])
  data.append('bbb', fileInput.files[3])

  const res = await axios.post('http://localhost:3000/ccc', data)
  console.log(res)
}

后端收到了上传的 aaa、bbb 的文件:

如果不知道前端上传字段,哪些是用于文件上传的字段,可以使用 AnyFilesInterceptor:

typescript 复制代码
@Post('ddd')
@UseInterceptors(AnyFilesInterceptor({
    dest: 'uploads'
}))
uploadAnyFiles(@UploadedFiles() files: Array<Express.Multer.File>, @Body() body) {
    console.log('body', body);
    console.log('files', files);
}

前端代码:

typescript 复制代码
async function formData4() {
  const data = new FormData()
  data.set('name', 'Yun')
  data.set('age', 20)
  data.set('aaa', fileInput.files[0])
  data.set('bbb', fileInput.files[1])
  data.set('ccc', fileInput.files[2])
  data.set('ddd', fileInput.files[3])

  const res = await axios.post('http://localhost:3000/ddd', data)
  console.log(res)
}

同样识别出了所有 file 字段:

这就是 Nest 上传文件的方式。

自定义存储

typescript 复制代码
import * as multer from 'multer';
import * as fs from 'fs';
import * as path from 'path';

const storage = multer.diskStorage({
  // 自定义目录
  destination: function (req, file, cb) {
    try {
      fs.mkdirSync(path.join(process.cwd(), 'my-uploads'));
    } catch (e) {}

    cb(null, path.join(process.cwd(), 'my-uploads'));
  },
  // 自定义文件
  filename: function (req, file, cb) {
    const uniqueSuffix =
      Date.now() +
      '-' +
      Math.round(Math.random() * 1e9) +
      '-' +
      file.originalname;
    cb(null, file.fieldname + '-' + uniqueSuffix);
  },
});

export { storage };

然后在 controller 使用这个 storage:


其实 Nest 上传文件的方式就是对 multer 做了一层简单的封装。

文件校验

此外我们还可能对上传文件的大小,类型做限制。这部分可以放在 pipe 做。

我们生成一个 pipe:

bash 复制代码
nest g pipe file-size-validation-pipe --no-spec --flat

添加检查文件大小的逻辑,大于 10k 就抛出异常,返回 400 的响应:

typescript 复制代码
import {
  PipeTransform,
  Injectable,
  ArgumentMetadata,
  HttpException,
  HttpStatus,
} from '@nestjs/common';

@Injectable()
export class FileSizeValidationPipe implements PipeTransform {
  transform(value: Express.Multer.File, metadata: ArgumentMetadata) {
    if (value.size > 10 * 1024) {
      throw new HttpException('文件大于 10k', HttpStatus.BAD_REQUEST);
    }
    return value;
  }
}

加到 UploadedFile 的参数里:

当上传一个图片大于 10k 的时候:

但像文件大小、类型的校验这种常见的逻辑,Nest 内置了:

typescript 复制代码
@Post('fff')
@UseInterceptors(FileInterceptor('aaa', {
    dest: 'uploads'
}))
uploadFile3(@UploadedFile(new ParseFilePipe({
    validators: [
      new MaxFileSizeValidator({ maxSize: 1000 }),
      new FileTypeValidator({ fileType: 'image/jpeg' }),
    ],
})) file: Express.Multer.File, @Body() body) {
    console.log('body', body);
    console.log('file', file);
}

MaxFileSizeValidator 是校验文件大小、FileTypeValidator 是校验文件类型。

返回的也是 400 响应,并且 message 说明了具体的错误信息。

而且这个错误信息 message 可以通过 exceptionFactory 工厂函数自定义。

我们也可以自己实现这样的 validator,只要继承 FileValidator 就可以:

javascript 复制代码
import { FileValidator } from '@nestjs/common';

export class MyFileValidator extends FileValidator {
  constructor(options) {
    super(options);
  }

  isValid(file: Express.Multer.File): boolean | Promise<boolean> {
    if (file.size > 10000) {
      return false;
    }
    return true;
  }
  buildErrorMessage(file: Express.Multer.File): string {
    return `文件 ${file.originalname} 大小超出 10k`;
  }
}

然后在 controller 用一下:

浏览器上传文件:

可以看到我们自定义的 FileValidator 生效了。

最后注意限制文件大小,大小超过之后文件最终还是会上传到服务器,因为文件写入才能拿到相关信息,我们可以根据路径来删除不合规的文件。

相关推荐
@大迁世界5 分钟前
TypeScript 的本质并非类型,而是信任
开发语言·前端·javascript·typescript·ecmascript
GIS之路14 分钟前
GDAL 实现矢量裁剪
前端·python·信息可视化
勇哥java实战分享15 分钟前
短信平台 Pro 版本 ,比开源版本更强大
后端
是一个Bug17 分钟前
后端开发者视角的前端开发面试题清单(50道)
前端
Amumu1213819 分钟前
React面向组件编程
开发语言·前端·javascript
学历真的很重要20 分钟前
LangChain V1.0 Context Engineering(上下文工程)详细指南
人工智能·后端·学习·语言模型·面试·职场和发展·langchain
计算机毕设VX:Fegn089523 分钟前
计算机毕业设计|基于springboot + vue二手家电管理系统(源码+数据库+文档)
vue.js·spring boot·后端·课程设计
上进小菜猪39 分钟前
基于 YOLOv8 的智能杂草检测识别实战 [目标检测完整源码]
后端
持续升级打怪中41 分钟前
Vue3 中虚拟滚动与分页加载的实现原理与实践
前端·性能优化
GIS之路44 分钟前
GDAL 实现矢量合并
前端