前端转型全栈(五)——NestJS 文件上传功能开发复盘

1. 背景与需求

在全栈开发中,前端需要实现图片上传功能,并能够在上传成功后进行预览。前端通过 FormData 将文件发送至后端的 /file/upload 接口,后端需要接收文件、保存到本地,并返回能够访问该文件的标识(如文件名或 URL),以便前端拼接预览地址。

2. 技术栈与依赖

  • 框架: NestJS

  • 核心中间件 : Multer (用于处理 multipart/form-data)

  • 依赖安装 :

    bash 复制代码
    npm i @types/multer multer @nestjs/serve-static

3. 核心实现步骤

3.1 配置文件存储逻辑

使用 NestJS 内置的 FileInterceptor 结合 multer 的 diskStorage 来处理文件上传。

  • 目标目录 : 项目根目录下的 uploads 文件夹。
  • 文件名生成: 使用时间戳 + 随机数保证文件名唯一,并保留原文件扩展名。

3.2 提供文件预览能力

有两种常见方式提供文件预览:

  1. 静态文件服务 : 使用 @nestjs/serve-static 将 uploads 目录映射为静态资源路由。
  2. 自定义预览接口 : 编写一个 GET 接口,通过 res.sendFile 返回文件流。(本项目最终采用此方式,以配合前端 /deduce/file/preview/${res} 的路由规范)。

4. 踩坑与问题解决记录

🔴 问题一:HTTP 状态码 201 报错

现象 :前端调用上传接口时,报错提示 Status Code 为 201。
原因 :在 NestJS 中,@Post() 装饰器默认的成功状态码是 201 Created。如果前端的请求拦截器严格校验 response.status === 200,就会抛出异常。
解决方案 :在控制器方法上添加 @HttpCode(HttpStatus.OK) 装饰器,强制返回 200 状态码。

🔴 问题二:前端统一拦截器解析报错

现象 :前端报错 const error = new Error(errorMessage); (error as any).status = res.status;。
原因:

  1. 响应格式不匹配 :前端的通用请求封装(如 createAfterResponseHook)通常期望后端返回统一的 JSON 结构(例如 { code: '200', data: ..., msg: '...' })。如果后端直接返回纯字符串(如文件名),会导致前端 JSON 解析失败或逻辑判断异常。
  2. 潜在的 500 错误 :如果请求中没有包含文件(例如字段名不匹配或未选择文件),@UploadedFile() 注入的 file 将为 undefined。此时直接访问 file.filename 会导致后端抛出 500 内部服务器错误,进而触发前端的错误拦截。
    解决方案:
  3. 增加空值校验,当 !file 时抛出 BadRequestException。
  4. 调整返回值格式,使其符合前端统一的响应规范。

5. 最终代码参考

file.controller.ts

typescript 复制代码
import {
  Controller,
  Post,
  Get,
  Param,
  Res,
  UseInterceptors,
  UploadedFile,
  HttpCode,
  HttpStatus,
  BadRequestException,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import type { Response } from 'express';
import { diskStorage } from 'multer';
import { extname, join } from 'path';
import * as fs from 'fs';

const uploadDir = './uploads';
// 确保上传目录存在
if (!fs.existsSync(uploadDir)) {
  fs.mkdirSync(uploadDir);
}

@Controller('file')
export class FileController {
  @Post('upload')
  @HttpCode(HttpStatus.OK) // 解决默认返回 201 的问题
  @UseInterceptors(
    FileInterceptor('file', {
      storage: diskStorage({
        destination: uploadDir,
        filename: (req, file, cb) => {
          // 生成唯一文件名
          const uniqueSuffix = Date.now() + '-' + Math.round(Math.random() * 1e9);
          const ext = extname(file.originalname);
          cb(null, `${uniqueSuffix}${ext}`);
        },
      }),
    }),
  )
  uploadFile(@UploadedFile() file: Express.Multer.File) {
    // 异常处理:防止未传文件导致 500 错误
    if (!file) {
      throw new BadRequestException('请上传文件');
    }
    // 统一响应格式,适配前端拦截器
    return {
      msg: '上传成功',
      code: '200',
      data: file.filename,
    };
  }

  // 文件预览接口
  @Get('preview/:filename')
  previewFile(@Param('filename') filename: string, @Res() res: Response) {
    res.sendFile(join(process.cwd(), uploadDir, filename));
  }
}

6. 总结与最佳实践

  1. 前后端规约对齐 :在开发接口前,务必确认前端的请求拦截器逻辑和期望的响应数据结构(如统一的 code/data/msg 包装)。
  2. 状态码管理:注意框架的默认行为(如 NestJS 的 Post 默认 201),必要时显式声明状态码。
  3. 健壮性处理:永远不要信任客户端的输入。对于文件上传,必须处理未传文件、文件过大、文件类型不符等边界情况,避免服务端崩溃。
  4. 静态资源与路由冲突 :如果使用 ServeStaticModule,要注意配置 serveRoot,避免静态文件路由与 API 路由发生冲突。
相关推荐
福兮说4 小时前
设计稿是 #4A7C6F,页面量出来是 #4B7C6F:HEX、HSL、透明度、canvas 来回转的七个坑
前端·javascript·css·canvas
郑州光合科技余经理4 小时前
海外版外卖加盟:总站与分站配送规则怎么分开管
java·开发语言·前端·后端·uni-app·php·ai编程
小呆呆6665 小时前
副业搞起来,小说,漫画,漫剧的成本优化思路
前端·后端·面试
Dovis(誓平步青云)5 小时前
浇水提醒刚弹出又消失,植物状态别只存一个百分比
开发语言·前端·javascript·pdf·ecmascript·电脑
码艺-Alimjan6 小时前
Vben Admin 新增维吾尔语 Vben-Modal的关键坑之一
前端·javascript·vue.js
可乐鸡翅yeah_6 小时前
hls.js 手动自定义 http 请求 loader,修改请求头实战
开发语言·前端·javascript·网络协议·http·ecmascript·m3u8在线
IT_陈寒6 小时前
Vite静态资源导入这个坑我帮你们踩过了
前端·人工智能·后端
广州华水科技7 小时前
大坝安全监测解决方案:单北斗GNSS形变监测系统应用与维护
前端
yivifu7 小时前
中文古籍电子书注释集成
前端·javascript·python·beautifulsoup·epub
默_笙8 小时前
🛴 从散件到整机:DeepAgents 与 Agent 身上预留的那些"插槽"(前置介绍)
前端·javascript