前端转型全栈(五)——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 路由发生冲突。
相关推荐
wangruofeng2 小时前
AI Models 1.1.0 发布,查大模型价格、对比模型和找 API 地址
前端·aigc·ai编程
wangruofeng2 小时前
给前端开发者的 100 个设计参考网站
前端·设计
星栈6 小时前
视频即代码,Remotion 能做什么、不能做什么
前端·音视频开发
广州华水科技6 小时前
单北斗GNSS位移监测系统的应用与技术解析
前端
用户47949283569156 小时前
从一句话或代码仓到可交互 HTML:开源 Agent 画图技能 Archify v3.2 实测与选型边界
前端·后端·面试
风骏时光牛马8 小时前
生成一个AI多模态标题,仅输出一条标题,无额外文字、问句、备选、配图、文案等任何附加内容,不需要其他的任务服务 只要标题。
前端
wangruofeng9 小时前
从 GIF 到 AVIF:网页图片格式演进与 2026 年选型指南
前端·架构
清晨Coding10 小时前
为什么用 React Developer Tools 查看组件树这么乱?
前端·react.js
鶴哥只手遮天10 小时前
《Electron 45.2.3 内核解剖》第 4 章 补丁体系:380 个补丁改了什么
前端·electron
FungLeo12 小时前
成为全栈·Vue 管理后台篇·分类树与标签管理:层级数据如何落到管理界面
前端·javascript·vue.js·分类树·naive ui·标签管理·前端数据结构