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

相关文章
|
2月前
|
自然语言处理 JavaScript 前端开发
深入理解JavaScript中的闭包:原理与实战
【10月更文挑战第12天】深入理解JavaScript中的闭包:原理与实战
|
2月前
|
JavaScript 前端开发 内存技术
js文件的入口代码及需要入口代码的原因
js文件的入口代码及需要入口代码的原因
45 0
|
15天前
Next.js 实战 (二):搭建 Layouts 基础排版布局
本文介绍了作者在Next.js v15.x版本发布后,对一个旧项目的重构过程。文章详细说明了项目开发规范配置、UI组件库选择(最终选择了Ant-Design)、以及使用Ant Design的Layout组件实现中后台布局的方法。文末展示了布局的初步效果,并提供了GitHub仓库链接供读者参考学习。
Next.js 实战 (二):搭建 Layouts 基础排版布局
|
9天前
|
存储 网络架构
Next.js 实战 (四):i18n 国际化的最优方案实践
这篇文章介绍了Next.js国际化方案,作者对比了网上常见的方案并提出了自己的需求:不破坏应用程序的目录结构和路由。文章推荐使用next-intl库来实现国际化,并提供了详细的安装步骤和代码示例。作者实现了国际化切换时不改变路由,并把当前语言的key存储到浏览器cookie中,使得刷新浏览器后语言不会失效。最后,文章总结了这种国际化方案的优势,并提供Github仓库链接供读者参考。
|
10天前
Next.js 实战 (三):优雅的实现暗黑主题模式
这篇文章介绍了在Next.js中实现暗黑模式的具体步骤。首先,需要安装next-themes库。然后,在/components/ThemeProvider/index.tsx文件中新增ThemeProvider组件,并在/app/layout.tsx文件中注入该组件。如果想要加入过渡动画,可以修改代码实现主题切换时的动画效果。最后,需要在需要的位置引入ThemeModeButton组件,实现暗黑模式的切换。
|
25天前
|
设计模式 前端开发 JavaScript
JavaScript设计模式及其在实战中的应用,涵盖单例、工厂、观察者、装饰器和策略模式
本文深入探讨了JavaScript设计模式及其在实战中的应用,涵盖单例、工厂、观察者、装饰器和策略模式,结合电商网站案例,展示了设计模式如何提升代码的可维护性、扩展性和可读性,强调了其在前端开发中的重要性。
29 2
|
2月前
|
JavaScript 前端开发 开发者
探索JavaScript原型链:深入理解与实战应用
【10月更文挑战第21天】探索JavaScript原型链:深入理解与实战应用
33 1
|
2月前
|
SQL 前端开发 JavaScript
Nest.js 实战 (十五):前后端分离项目部署的最佳实践
这篇文章介绍了如何使用现代前端框架Vue3和后端Node.js框架Nest.js实现的前后端分离架构的应用,并将其部署到生产环境。文章涵盖了准备阶段,包括云服务器的设置、1Panel面板的安装、数据库的安装、域名的实名认证和备案、SSL证书的申请。在部署Node服务环节,包括了Node.js环境的创建、数据库的配置、用户名和密码的设置、网站信息的填写、静态网站的部署、反向代理的配置以及可能遇到的常见问题。最后,作者总结了部署经验,并希望对读者有所帮助。
203 11
|
3月前
|
前端开发 JavaScript API
前端JS读取文件内容并展示到页面上
前端JavaScript使用FileReader API读取文件内容,支持文本类型文件。在文件读取成功后,可以通过onload事件处理函数获取文件内容,然后展示到页面上。
113 2
前端JS读取文件内容并展示到页面上
|
2月前
|
存储 JavaScript 前端开发
前端开发:Vue.js入门与实战
【10月更文挑战第9天】前端开发:Vue.js入门与实战