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

相关文章
|
29天前
|
自然语言处理 JavaScript 前端开发
深入理解JavaScript中的闭包:原理与实战
【10月更文挑战第12天】深入理解JavaScript中的闭包:原理与实战
|
1月前
|
JavaScript 前端开发 内存技术
js文件的入口代码及需要入口代码的原因
js文件的入口代码及需要入口代码的原因
34 0
|
19天前
|
JavaScript 前端开发 开发者
探索JavaScript原型链:深入理解与实战应用
【10月更文挑战第21天】探索JavaScript原型链:深入理解与实战应用
26 1
|
1月前
|
SQL 前端开发 JavaScript
Nest.js 实战 (十五):前后端分离项目部署的最佳实践
这篇文章介绍了如何使用现代前端框架Vue3和后端Node.js框架Nest.js实现的前后端分离架构的应用,并将其部署到生产环境。文章涵盖了准备阶段,包括云服务器的设置、1Panel面板的安装、数据库的安装、域名的实名认证和备案、SSL证书的申请。在部署Node服务环节,包括了Node.js环境的创建、数据库的配置、用户名和密码的设置、网站信息的填写、静态网站的部署、反向代理的配置以及可能遇到的常见问题。最后,作者总结了部署经验,并希望对读者有所帮助。
120 11
|
1月前
|
存储 JavaScript 前端开发
前端开发:Vue.js入门与实战
【10月更文挑战第9天】前端开发:Vue.js入门与实战
|
2月前
|
前端开发 JavaScript API
前端JS读取文件内容并展示到页面上
前端JavaScript使用FileReader API读取文件内容,支持文本类型文件。在文件读取成功后,可以通过onload事件处理函数获取文件内容,然后展示到页面上。
77 2
前端JS读取文件内容并展示到页面上
|
12天前
|
前端开发 JavaScript
JavaScript新纪元:ES6+特性深度解析与实战应用
【10月更文挑战第29天】本文深入解析ES6+的核心特性,包括箭头函数、模板字符串、解构赋值、Promise、模块化和类等,结合实战应用,展示如何利用这些新特性编写更加高效和优雅的代码。
26 0
|
1月前
|
数据采集 JSON 前端开发
JavaScript逆向爬虫实战分析
JavaScript逆向爬虫实战分析
|
1月前
|
前端开发 JavaScript API
JavaScript逆向爬取实战——使用Python实现列表页内容爬取(二)
JavaScript逆向爬取实战——使用Python实现列表页内容爬取(二)
|
1月前
|
前端开发 JavaScript API
JavaScript逆向爬取实战——使用Python实现列表页内容爬取(一)
JavaScript逆向爬取实战——使用Python实现列表页内容爬取(一)