前端转型全栈(五)——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 路由发生冲突。
相关推荐
kyriewen12 小时前
别再 console.log 了:5 个 Chrome DevTools 调试技巧,用过就回不去了
前端·javascript·面试
IT_陈寒14 小时前
Python搞不定字符串编码?这破玩意坑我两小时!
前端·人工智能·后端
DigitalOcean15 小时前
Laravel 开发者已在 DigitalOcean 上开通超过 10 万台服务器
前端·laravel
星始流年15 小时前
从 Tool 到 Skill——基于 LangChain 的服务端Skill实现
前端·langchain·agent
李惟15 小时前
开源本地通信库,纯客户端 RPC,像聊天一样通信
前端
YAwu1115 小时前
深入解析 React 炫彩鼠标跟随标题组件:从坐标定位到动画性能
前端·react.js
GuWenyue15 小时前
排序效率低?5分钟吃透快速排序,性能飙升至O(nlogn)
前端·javascript·面试
OpenTiny社区16 小时前
🎨 看完 GenUI SDK 源码我悟了!
前端·vue.js·github
叁两16 小时前
前端转型AI Agent该如何学习?(前置篇)
前端·人工智能·node.js
何时梦醒16 小时前
深入理解递归与快速排序 —— 从基础入门到手写实现
前端·javascript