从 Multer 配置到 RxJS 拦截器,一文打通文件上传全链路,附新手高频踩坑解决方案
在后端开发中,图片上传是非常基础但又极易踩坑的需求。很多新手在 NestJS 中使用 Multer 做上传时,总会遇到 __dirname 未定义、打包后路径失效、静态资源404、返回格式不统一 等问题。
本文将从零到一实现一套可落地的 NestJS 图片上传方案,同时讲清 ES Module 环境适配、静态资源托管、RxJS 响应拦截器的核心原理,并整理全套避坑清单。
一、前置准备
1. 环境说明
本文基于 ES Module 环境(package.json 中 "type": "module"),这也是目前 NestJS 新项目的主流配置。相比 CommonJS,ES Module 没有内置 __dirname,需要手动构造,这也是新手第一个踩坑点。
2. 安装依赖
Multer 是 Express 生态最成熟的文件上传中间件,NestJS 对其做了封装,开箱即用。
pnpm add multer
pnpm add @types/multer -D
3. 目录结构规划
核心原则:上传目录不要放在 src 或 dist 中,必须放在项目根目录。
demo(项目根)
├─ images # 图片存储目录,打包不会被清空
├─ src
│ ├─ main.ts # 入口文件
│ └─ upload
│ ├─ upload.module.ts
│ ├─ upload.controller.ts
│ └─ upload.service.ts
└─ dist # 编译输出目录,只存放 JS 代码
二、核心实现:Multer 图片上传模块
1. UploadModule 完整配置
模块是 NestJS 的核心,我们在 upload.module.ts 中完成 Multer 的全量配置,包括存储路径、命名规则、大小限制、类型过滤。
import { Module } from '@nestjs/common';
import { UploadService } from './upload.service.js';
import { UploadController } from './upload.controller.js';
import { diskStorage } from 'multer';
import { MulterModule } from '@nestjs/platform-express';
import { extname, join, dirname } from 'path';
import { fileURLToPath } from 'url';
// ========== ES Module 手动构造 __dirname ==========
// CommonJS 自带 __dirname,ES Module 必须通过 import.meta.url 转换得到
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@Module({
imports: [
MulterModule.register({
// 磁盘存储配置
storage: diskStorage({
// 配置文件保存目录
destination: (req, file, cb) => {
// 当前文件在 src/upload 下,向上两级到达项目根,拼接 images
const uploadPath = join(__dirname, '../../images');
cb(null, uploadPath);
},
// 自定义保存文件名,避免重名覆盖
filename: (_, file, callback) => {
// 时间戳 + 原始文件后缀
const filename = `${Date.now()}${extname(file.originalname)}`;
callback(null, filename);
}
}),
// 限制单文件最大 5MB
limits: { fileSize: 5 * 1024 * 1024 },
// 文件类型过滤:只允许图片格式
fileFilter: (req, file, cb) => {
const allowTypes = /.(png|jpg|jpeg|gif)$/;
if (allowTypes.test(file.originalname)) {
cb(null, true);
} else {
cb(new Error('仅支持 png/jpg/jpeg/gif 格式图片'), false);
}
}
})
],
controllers: [UploadController],
providers: [UploadService],
})
export class UploadModule {}
关键说明
- 路径计算:
upload.module.ts位于src/upload目录下,__dirname指向当前文件所在文件夹,因此需要../../images向上两级到达项目根目录。 - Multer 特性:Multer 只负责写入文件,不会自动创建目标文件夹,因此
images目录需要提前手动创建。 - 命名规则:使用时间戳作为文件名,避免同名文件覆盖,保留原始后缀保证格式正确。
2. UploadController 接口编写
控制器负责暴露 HTTP 接口,接收前端上传的文件并返回访问地址。
import { Controller, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
@Controller('upload')
export class UploadController {
@Post('img')
@UseInterceptors(FileInterceptor('file'))
uploadImg(@UploadedFile() file: Express.Multer.File) {
return {
code: 200,
msg: '上传成功',
url: `/images/${file.filename}`
};
}
}
关键说明
FileInterceptor('file'):单文件上传拦截器,参数'file'必须和前端form-data的字段名完全一致,否则接收不到文件。@UploadedFile():将解析后的文件对象注入到参数中,包含文件名、大小、路径、MIME 类型等信息。- 返回
url:直接返回静态资源访问路径,前端拼接域名即可预览图片。
三、静态资源托管:让图片可被浏览器访问
上传后的图片不能直接通过 HTTP 访问,需要在 main.ts 中配置静态资源托管,将 images 目录暴露出去。
import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
import { AppModule } from './app.module.js';
// ES Module 构造 __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule);
// 托管项目根目录的 images 文件夹
app.useStaticAssets(
join(__dirname, '../images'),
{ prefix: '/images' }
);
await app.listen(3000);
console.log('服务启动:http://localhost:3000');
}
bootstrap();
路径为什么是 ../images?
main.ts位于src目录下,__dirname指向src;../images向上一级到达项目根,拼接images目录;- 打包后
main.js位于dist目录下,../images同样指向项目根的images,开发和生产环境路径一致。
配置完成后,浏览器访问 http://localhost:3000/images/xxx.png 即可直接查看图片。
四、进阶:统一响应拦截器(RxJS 版)
实际项目中,后端接口通常需要统一返回格式,比如 { code, data, msg, success }。NestJS 的拦截器 + RxJS 可以优雅地实现全局统一包装。
1. 响应拦截器实现
新建 response.interceptor.ts:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
@Injectable()
export class ResponseInterceptor<T> implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// next.handle() 触发执行目标控制器方法,返回 RxJS Observable 流
return next.handle().pipe(
map(data => {
return {
data,
code: 0,
msg: '请求成功',
success: true
};
})
);
}
}
2. 全局注册拦截器
在 main.ts 中添加全局拦截器,所有接口自动应用统一格式:
import { ResponseInterceptor } from './response.interceptor.js';
// ...
app.useGlobalInterceptors(new ResponseInterceptor());
3. 原理解析
next.handle():拦截器的「放行开关」,执行后才会真正调用控制器的业务方法,返回一个 RxJS 的Observable数据流。pipe()+map():RxJS 的链式操作符,对控制器返回的数据做统一转换,不会侵入业务代码。- RxJS 的优势:相比 Promise,Observable 支持多次发射、可取消、丰富的操作符,非常适合处理请求流、事件流。
五、核心概念解惑
1. 为什么上传文件必须用 form-data?
form-data(multipart/form-data)是 HTTP 请求体的一种格式,专门用来传输二进制文件。普通的 application/json 只能传输文本数据,无法直接携带图片、文件等二进制内容。
Multer 中间件的作用就是解析 form-data 格式的请求体,提取出文件和普通字段。
2. images 为什么不能放在 src /dist 里?
- 放在 src:开发环境正常,但打包后代码运行在
dist目录,__dirname路径变化,会找不到src/images。 - 放在 dist:每次执行
npm run build时,dist 目录会被整体清空重建,上传的图片会全部丢失。
最佳实践:业务运行时产生的文件,统一放在项目根目录的独立文件夹中,与编译产物分离。
六、新手高频踩坑清单
1. ReferenceError: __dirname is not defined
- 原因:项目是 ES Module 环境,没有内置
__dirname全局变量。 - 解决:使用
fileURLToPath(import.meta.url)+dirname()手动构造。
2. ENOENT: no such file or directory
- 原因:目标文件夹不存在,Multer 不会自动创建目录。
- 解决:提前手动创建
images文件夹,或在destination回调中用mkdirSync自动创建。
3. 打包后图片找不到
- 原因:路径只适配了开发环境,打包后
__dirname指向 dist,路径错位。 - 解决:将图片目录放在项目根,通过
../层级计算,保证开发和生产指向同一个目录。
4. 静态资源访问 404
- 原因:
useStaticAssets路径配置错误,或prefix与访问地址不匹配。 - 解决:打印
join(__dirname, '../images')确认路径,保证prefix为/images。
5. 上传接口接收不到文件
- 原因:前端
form-data的字段名与FileInterceptor('file')中的参数不一致。 - 解决:前后端字段名严格保持一致,区分大小写。
七、总结
本文完整实现了 NestJS 环境下的图片上传能力,涵盖 Multer 配置、ES Module 适配、静态资源托管、统一响应拦截四个核心部分,同时梳理了新手最容易踩的路径和环境坑。
核心最佳实践:
- 上传目录放项目根,与编译产物分离;
- ES Module 手动构造
__dirname,路径层级按需计算; - 利用拦截器 + RxJS 统一响应格式,解耦业务与通用逻辑;
- 增加文件大小和类型校验,提升接口安全性。
这套方案可以直接用于中小型项目的图片上传场景,也可以扩展至多文件上传、分片上传、OSS 云存储等更复杂的场景。