1. 背景与需求
在全栈开发中,前端需要实现图片上传功能,并能够在上传成功后进行预览。前端通过 FormData 将文件发送至后端的 /file/upload 接口,后端需要接收文件、保存到本地,并返回能够访问该文件的标识(如文件名或 URL),以便前端拼接预览地址。
2. 技术栈与依赖
-
框架: NestJS
-
核心中间件 : Multer (用于处理
multipart/form-data) -
依赖安装 :
bashnpm i @types/multer multer @nestjs/serve-static
3. 核心实现步骤
3.1 配置文件存储逻辑
使用 NestJS 内置的 FileInterceptor 结合 multer 的 diskStorage 来处理文件上传。
- 目标目录 : 项目根目录下的
uploads文件夹。 - 文件名生成: 使用时间戳 + 随机数保证文件名唯一,并保留原文件扩展名。
3.2 提供文件预览能力
有两种常见方式提供文件预览:
- 静态文件服务 : 使用
@nestjs/serve-static将uploads目录映射为静态资源路由。 - 自定义预览接口 : 编写一个
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;。
原因:
- 响应格式不匹配 :前端的通用请求封装(如
createAfterResponseHook)通常期望后端返回统一的 JSON 结构(例如{ code: '200', data: ..., msg: '...' })。如果后端直接返回纯字符串(如文件名),会导致前端 JSON 解析失败或逻辑判断异常。 - 潜在的 500 错误 :如果请求中没有包含文件(例如字段名不匹配或未选择文件),
@UploadedFile()注入的file将为undefined。此时直接访问file.filename会导致后端抛出 500 内部服务器错误,进而触发前端的错误拦截。
解决方案: - 增加空值校验,当
!file时抛出BadRequestException。 - 调整返回值格式,使其符合前端统一的响应规范。
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. 总结与最佳实践
- 前后端规约对齐 :在开发接口前,务必确认前端的请求拦截器逻辑和期望的响应数据结构(如统一的
code/data/msg包装)。 - 状态码管理:注意框架的默认行为(如 NestJS 的 Post 默认 201),必要时显式声明状态码。
- 健壮性处理:永远不要信任客户端的输入。对于文件上传,必须处理未传文件、文件过大、文件类型不符等边界情况,避免服务端崩溃。
- 静态资源与路由冲突 :如果使用
ServeStaticModule,要注意配置serveRoot,避免静态文件路由与 API 路由发生冲突。