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

相关文章
|
3月前
|
自然语言处理 JavaScript 前端开发
深入理解JavaScript中的闭包:原理与实战
【10月更文挑战第12天】深入理解JavaScript中的闭包:原理与实战
|
11天前
|
设计模式 数据安全/隐私保护
Next.js 实战 (七):浅谈 Layout 布局的嵌套设计模式
这篇文章介绍了在Next.js框架下,如何处理中后台管理系统中特殊页面(如登录页)不包裹根布局(RootLayout)的问题。作者指出Next.js的设计理念是通过布局的嵌套来创建复杂的页面结构,这虽然保持了代码的整洁和可维护性,但对于特殊页面来说,却造成了不必要的布局包裹。文章提出了一个解决方案,即通过判断页面的skipGlobalLayout属性来决定是否包含RootLayout,从而实现特殊页面不包裹根布局的目标。
65 33
|
2天前
|
存储 安全 API
Next.js 实战 (九):使用 next-auth 完成第三方身份登录验证
这篇文章介绍了next-auth,一个为Next.js设计的身份验证库,支持多种认证方式,如电子邮件和密码、OAuth2.0提供商(如Google、GitHub、Facebook等)以及自定义提供商。文章包含了如何配置Github Provider以及会话管理,并提到了适配器Adapters在next-auth中的作用。最后,文章强调了next-auth的强大功能值得进一步探索。
23 10
|
3天前
|
中间件 API
Next.js 实战 (八):使用 Lodash 打包构建产生的“坑”?
这篇文章介绍了作者在使用Nextjs15进行项目开发时遇到的部署问题。在部署过程中,作者遇到了打包构建时的一系列报错,报错内容涉及动态代码评估在Edge运行时不被允许等问题。经过一天的尝试和调整,作者最终删除了lodash-es库,并将radash的部分源码复制到本地,解决了打包报错的问题。文章最后提供了项目的线上预览地址,并欢迎读者留言讨论更好的解决方案。
17 10
|
3月前
|
JavaScript 前端开发 内存技术
js文件的入口代码及需要入口代码的原因
js文件的入口代码及需要入口代码的原因
54 0
|
19天前
|
前端开发 API 开发者
Next.js 实战 (五):添加路由 Transition 过渡效果和 Loading 动画
这篇文章介绍了Framer Motion,一个为React设计的动画库,提供了声明式API处理动画和页面转换,适合创建响应式用户界面。文章包括首屏加载动画、路由加载Loading、路由进场和退场动画等主题,并提供了使用Framer Motion和next.js实现这些动画的示例代码。最后,文章总结了这些效果,并邀请读者探讨更好的实现方案。
|
17天前
|
JavaScript 前端开发 API
Next.js 实战 (六):如何实现文件本地上传
这篇文章介绍了在Next.js中如何实现文件上传到本地的方法。文章首先提到Next.js官方文档中没有提供文件上传的实例代码,因此开发者需要自行实现,通常有两种思路:使用Node.js原生上传或使用第三方插件如multer。接着,文章选择了使用Node.js原生上传的方式来讲解实现过程,包括如何通过哈希值命名文件、上传到指定目录以及如何分类文件夹。然后,文章展示了具体的实现步骤,包括编写代码来处理文件上传,并给出了代码示例。最后,文章通过一个效果演示说明了如何通过postman模拟上传文件,并展示了上传后的文件夹结构。
|
1月前
Next.js 实战 (二):搭建 Layouts 基础排版布局
本文介绍了作者在Next.js v15.x版本发布后,对一个旧项目的重构过程。文章详细说明了项目开发规范配置、UI组件库选择(最终选择了Ant-Design)、以及使用Ant Design的Layout组件实现中后台布局的方法。文末展示了布局的初步效果,并提供了GitHub仓库链接供读者参考学习。
Next.js 实战 (二):搭建 Layouts 基础排版布局
|
1月前
|
存储 网络架构
Next.js 实战 (四):i18n 国际化的最优方案实践
这篇文章介绍了Next.js国际化方案,作者对比了网上常见的方案并提出了自己的需求:不破坏应用程序的目录结构和路由。文章推荐使用next-intl库来实现国际化,并提供了详细的安装步骤和代码示例。作者实现了国际化切换时不改变路由,并把当前语言的key存储到浏览器cookie中,使得刷新浏览器后语言不会失效。最后,文章总结了这种国际化方案的优势,并提供Github仓库链接供读者参考。
|
1月前
Next.js 实战 (三):优雅的实现暗黑主题模式
这篇文章介绍了在Next.js中实现暗黑模式的具体步骤。首先,需要安装next-themes库。然后,在/components/ThemeProvider/index.tsx文件中新增ThemeProvider组件,并在/app/layout.tsx文件中注入该组件。如果想要加入过渡动画,可以修改代码实现主题切换时的动画效果。最后,需要在需要的位置引入ThemeModeButton组件,实现暗黑模式的切换。