Nest.js 实战 (五):如何实现文件本地上传

简介: 这篇文章介绍了使用Nest框架和multer中间件包实现文件上传功能的过程。首先,作者在开发用户管理模块时遇到了需要上传用户头像的需求,因此开发了文件上传功能。文章详细介绍了如何安装依赖,如何处理单个文件上传,如何在模块层注册并配置文件上传路径,并以代码形式展示了如何实现这些功能。最后,作者对使用第三方平台存储文件进行了说明,并建议业务量大的公司很少用上传到服务器本地的方式,该方式更适合个人站点、博客使用。

前言

最近在开发用户管理模块,需要上传用户头像,正好顺便把文件上传这块的功能开发了。

为了处理文件上传,Nest 提供了一个内置的基于 multer 中间件包的 Express 模块。Multer 处理以 multipart/form-data 格式发送的数据,该格式主要用于通过 HTTP POST 请求上传文件。

安装依赖

pnpm add @nestjs/platform-express multer uuid

我们需要安装三个包,前面两个是文件上传必须的,后面的 uuid 是生成文件名的,如果不需要可以不安装。

单个文件

当我们要上传单个文件时, 我们只需将 FileInterceptor() 与处理程序绑定在一起, 然后使用 @UploadedFile() 装饰器从 request 中取出 file

@Post('upload')
@UseInterceptors(FileInterceptor('file'))
uploadFile(@UploadedFile() file: Express.Multer.File) {
   
  console.log(file);
}

FileInterceptor() 装饰器是 @nestjs/platform-express 包提供的, @UploadedFile() 装饰器是 @nestjs/common 包提供的。

FileInterceptor() 接收两个参数:

  1. fieldName:指向包含文件的 HTML 表单的字段
  2. options:类型为 MulterOptions 。这个和被传入 multer 构造函数 (此处有更多详细信息) 的对象是同一个对象。

文件数组

文件数组使用 FilesInterceptor() 装饰器,这个装饰器有三个参数:

  1. fieldName:同上
  2. maxCount:可选的数字,定义要接受的最大文件数
  3. options:同上
@Post('upload')
@UseInterceptors(FilesInterceptor('files'))
uploadFile(@UploadedFiles() files: Array<Express.Multer.File>) {
   
  console.log(files);
}

多个文件

要上传多个文件(全部使用不同的键),请使用 FileFieldsInterceptor() 装饰器。这个装饰器有两个参数:

  1. uploadedFields:对象数组,其中每个对象指定一个必需的 name 属性和一个指定字段名的字符串值
  2. options:同上
@Post('upload')
@UseInterceptors(FileFieldsInterceptor([
  {
    name: 'avatar', maxCount: 1 },
  {
    name: 'background', maxCount: 1 },
]))
uploadFile(@UploadedFiles() files: {
    avatar?: Express.Multer.File[], background?: Express.Multer.File[] }) {
   
  console.log(files);
}

新建模块 module

  1. 使用生成器创建模块,也可以自己手动创建
    nest g resource file-upload
    
  2. file-upload.service.ts,服务层为空即可

    import {
          Injectable } from '@nestjs/common';
    
    @Injectable()
    export class FileUploadService {
          }
    
  3. file-upload.controller.ts,当我们要上传单个文件时, 我们只需将 FileInterceptor() 与处理程序绑定在一起, 然后使用 @UploadedFile() 装饰器从 request 中取出 file

    import {
          Controller, Post, Req, UploadedFile, UseInterceptors } from '@nestjs/common';
    import {
          FileInterceptor } from '@nestjs/platform-express';
    import {
          ApiBody, ApiConsumes } from '@nestjs/swagger';
    import {
          Request } from 'express';
    
    import {
          responseMessage } from '@/utils';
    
    import {
          FileUploadDto } from './dto';
    
    @Controller('upload')
    export class FileUploadController {
         
    /**
    * @description: 上传单个文件
    */
    @UseInterceptors(FileInterceptor('file'))
    @Post('single-file')
    @ApiConsumes('multipart/form-data')
    @ApiBody({
         
     description: '单个文件上传',
     type: FileUploadDto,
    })
    uploadFile(@UploadedFile() file: Express.Multer.File, @Req() req: Request): Api.Common.Response<Express.Multer.File> {
         
     // 获取客户端域名端口
     const hostname = req.headers['x-forwarded-host'] || req.hostname;
     const port = req.headers['x-forwarded-port'] || req.socket.localPort;
     const protocol = req.headers['x-forwarded-proto'] || req.protocol;
     file.path = `${
           protocol}://${
           hostname}:${
           port}/static${
           file.path.replace(/\\/g, '/').replace(/upload/g, '')}`;
     return responseMessage(file);
    }
    }
    
  4. file-upload.module.ts,我们在 module 层注册并根据实际情况配置文件上传路径

    import {
          Module } from '@nestjs/common';
    import {
          MulterModule } from '@nestjs/platform-express';
    import dayjs from 'dayjs';
    import {
          diskStorage } from 'multer';
    import {
          v4 as uuidv4 } from 'uuid';
    
    import {
          checkDirAndCreate } from '@/utils';
    
    import {
          FileUploadController } from './file-upload.controller';
    import {
          FileUploadService } from './file-upload.service';
    
    @Module({
         
    imports: [
     MulterModule.registerAsync({
         
       useFactory: async () => ({
         
         limits: {
         
          fileSize: 1024 * 1024 * 5, // 限制文件大小为 5MB
         },
         storage: diskStorage({
         
           // 配置文件上传后的文件夹路径
           destination: (_, file, cb) => {
         
             // 定义文件上传格式
             const allowedImageTypes = ['gif', 'png', 'jpg', 'jpeg', 'bmp', 'webp', 'svg', 'tiff']; // 图片
             const allowedOfficeTypes = ['xls', 'xlsx', 'doc', 'docx', 'ppt', 'pptx', 'pdf', 'txt', 'md', 'csv']; // office
             const allowedVideoTypes = ['mp4', 'avi', 'wmv']; // 视频
             const allowedAudioTypes = ['mp3', 'wav', 'ogg']; // 音频
             // 根据上传的文件类型将图片视频音频和其他类型文件分别存到对应英文文件夹
             const fileExtension = file.originalname.split('.').pop().toLowerCase();
             let temp = 'other';
             if (allowedImageTypes.includes(fileExtension)) {
         
               temp = 'image';
             } else if (allowedOfficeTypes.includes(fileExtension)) {
         
               temp = 'office';
             } else if (allowedVideoTypes.includes(fileExtension)) {
         
               temp = 'video';
             } else if (allowedAudioTypes.includes(fileExtension)) {
         
               temp = 'audio';
             }
             // 文件以年月命名文件夹
             const filePath = `upload/${
           temp}/${
           dayjs().format('YYYY-MM')}`;
             checkDirAndCreate(filePath); // 判断文件夹是否存在,不存在则自动生成
             return cb(null, `./${
           filePath}`);
           },
           filename: (_, file, cb) => {
         
             // 使用随机 uuid 生成文件名
             const filename = `${
           uuidv4()}.${
           file.mimetype.split('/')[1]}`;
             return cb(null, filename);
           },
         }),
       }),
     }),
    ],
    controllers: [FileUploadController],
    providers: [FileUploadService],
    })
    export class FileUploadModule {
          }
    

效果演示

我们使用 postman 模拟上传:
x3w09u6ayx0n0n12rfddvgy0s4mpbl06.gif

上传后的文件夹结构:
8xx5b6vs5vprotqq06d42ac6bww0s9lw.png

配置文件访问

我们上传完成后的地址,比如:http://localhost:3000/static/image/2024-07/68bfe42a-06f2-462f-91fa-626f52f04845.jpeg 是不能直接访问的,我们还需要在 main.ts 里面配置:

import {
    NestFactory } from '@nestjs/core';
import {
    NestExpressApplication } from '@nestjs/platform-express';
import * as express from 'express';
import {
    join } from 'path';

import {
    AppModule } from './app.module';
async function bootstrap() {
   
  const app = await NestFactory.create<NestExpressApplication>(AppModule);

  // 配置文件访问  文件夹为静态目录,以达到可直接访问下面文件的目的
  const rootDir = join(__dirname, '..');
  app.use('/static', express.static(join(rootDir, '/upload')));

  await app.listen(3000);
}
bootstrap();

配置完成就能正常访问文件了。

总结

我只能了单个文件上传,文件数组和多个文件上传也是一样的道理,大家可自行实现。

现在很多公司文件存储业务都已经使用第三方平台,很少用上传到服务器本地的,业务量大的话会对服务器造成压力,一般这种适合个人站点、博客使用,这里我们当做学习就行。

GithubVue3 Admin
官网文档file-upload

相关文章
|
11天前
|
JavaScript API
深入探索fs.WriteStream:Node.js文件写入流的全面解析
深入探索fs.WriteStream:Node.js文件写入流的全面解析
|
5天前
|
JavaScript
Nest.js 实战 (十一):配置热重载 HMR 给服务提提速
这篇文章介绍了Nest.js服务在应用程序引导过程中,TypeScript编译对效率的影响,以及如何通过使用webpackHMR来降低应用实例化的时间。文章包含具体教程,指导读者如何在项目中安装依赖包,并在根目录下新增webpack配置文件webpack-hmr.config.js来调整HMR相关的配置。最后,文章总结了如何通过自定义webpack配置来更好地控制HMR行为。
|
25天前
|
缓存 中间件
Nest.js 实战 (九):使用拦截器记录用户 CURD 操作日志
这篇文章介绍了在Nest.js中如何实现记录用户CURD操作的需求。首先解释了什么是拦截器以及拦截器的作用,然后通过创建Prisma模型,添加Log模型,并通过编写LoggerInterceptor拦截器,实现了记录用户操作的功能。最后通过效果演示和总结,强调了使用拦截器实现此功能的有效性。
|
8天前
|
SQL 运维 监控
Nest.js 实战 (十):使用 winston 打印和收集日志记录
这篇文章介绍了在Nest服务中如何使用Winston记录日志。文章首先强调了日志记录在后台服务中的重要性,接着提到Nest默认的内部日志记录器,并指出可以通过@nestjs/common包中的Logger类来全面控制日志系统的行为。文章还提到,为了在生产环境中实现更高级的日志功能,可以使用如Winston之类的Node.js日志包。接下来,文章介绍了如何在Nest服务中使用Winston记录日志,包括安装相关依赖、创建winston配置文件以及实现简单的日志记录示例。最后,文章指出更高级的自定义日志功能需要读者自己去探索。
Nest.js 实战 (十):使用 winston 打印和收集日志记录
|
23天前
|
JavaScript 前端开发
JavaScript基础&实战(1)js的基本语法、标识符、数据类型
这篇文章是JavaScript基础与实战教程的第一部分,涵盖了JavaScript的基本语法、标识符、数据类型以及如何进行强制类型转换,通过代码示例介绍了JS的输出语句、编写位置和数据类型转换方法。
JavaScript基础&实战(1)js的基本语法、标识符、数据类型
|
12天前
|
JavaScript 数据安全/隐私保护 Python
网易云音乐搜索接口JS逆向: Params、encSecKey加密和AES实战
网易云音乐搜索接口JS逆向: Params、encSecKey加密和AES实战
17 4
|
9天前
|
前端开发 JavaScript 开发者
前端JS按钮点击事件、跳出弹窗、遮罩的实战示例
本文提供了一个前端JS按钮点击事件、弹出式窗口和遮罩层的实战示例,包括HTML、CSS和JavaScript的具体实现代码,以及功能解析,演示了如何实现按钮点击后触发弹窗显示和遮罩层,并在2秒后自动关闭或点击遮罩层关闭弹窗的效果。
前端JS按钮点击事件、跳出弹窗、遮罩的实战示例
|
12天前
|
JavaScript 前端开发 安全
JS 混淆解析:JS 压缩混淆原理、OB 混淆特性、OB 混淆JS、混淆突破实战
JS 混淆解析:JS 压缩混淆原理、OB 混淆特性、OB 混淆JS、混淆突破实战
24 2
|
19天前
|
JavaScript 数据安全/隐私保护
如何在Vue组件中调用封装好的外部js文件方法
这篇文章介绍了如何在Vue组件中调用封装好的外部js文件方法,包括在Vue项目中全局引入外部js文件,并在组件中通过this.$myMethod()的方式调用外部js文件中定义的方法。
如何在Vue组件中调用封装好的外部js文件方法
|
23天前
|
JavaScript 前端开发
JavaScript基础&实战 JS中正则表达式的使用
这篇文章介绍了JavaScript中正则表达式的使用,包括正则表达式的创建、匹配模式、字符串匹配、拆分、搜索、匹配和替换等方法,并通过示例代码展示了如何应用这些技术。
JavaScript基础&实战 JS中正则表达式的使用
下一篇
DDNS