前端转型全栈(五)——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 结合 multerdiskStorage 来处理文件上传。

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

3.2 提供文件预览能力

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

  1. 静态文件服务 : 使用 @nestjs/serve-staticuploads 目录映射为静态资源路由。
  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 路由发生冲突。
相关推荐
木斯佳2 小时前
前端八股文面经大全:来未来前端实习一面(2026-04-17)·面经深度解析
前端·校招·实习
刘佬GEO2 小时前
GEO 效果看什么指标:从提及、引用到推荐的判断框架
前端·网络·人工智能·搜索引擎·ai
Liu.7742 小时前
Vue 3开发中遇到的报错(1)
前端·javascript·vue.js
还有你Y8 小时前
Shell 脚本语法
前端·语法·sh
踩着两条虫10 小时前
如何评价VTJ.PRO?
前端·架构·ai编程
Mh11 小时前
鼠标跟随倾斜动效
前端·css·vue.js
小码哥_常11 小时前
Kotlin类型魔法:Any、Unit、Nothing 深度探秘
前端
Web极客码13 小时前
深入了解WordPress网站访客意图
服务器·前端·wordpress
幺风13 小时前
Claude Code 源码分析 — Tool/MCP/Skill 可扩展工具系统
前端·javascript·ai编程