01 multipart/form-data 与基础上传
1. 学习目标
- 理解文件上传为什么使用 multipart;
- 完成 Express 5 单文件接口;
- 正确构造浏览器 FormData;
- 读取
req.file和req.body; - 理解 Multer 中间件顺序和 TypeScript 类型支持。
2. 为什么不能使用普通 JSON 上传文件
JSON 适合文本结构:
{
"name": "Tom",
"age": 20
}
文件是二进制内容。虽然可以转成 Base64 放进 JSON,但会:
- 增大传输体积;
- 增加编码和解码内存;
- 不适合流式处理;
- 无法自然表达多个文件和普通字段。
multipart/form-data 把请求分成多个 part:
Content-Type: multipart/form-data; boundary=----abc
part: title = 头像
part: avatar = 文件内容
每个文件 part 都有自己的 headers 和 body。
3. 安装与 TypeScript
npm install multer
npm install --save-dev @types/multer
当前 multer@2.2.0 包没有内置 TypeScript 声明,因此 TypeScript 工程通常还需要 @types/multer。
import multer from "multer";
当前项目开启了 esModuleInterop,可以使用默认导入。TypeScript 最终仍按项目配置编译成 CommonJS。
4. 最小单文件接口
import express, {
type Request,
type Response,
} from "express";
import multer from "multer";
const router = express.Router();
const upload = multer({
dest: "uploads/",
});
router.post(
"/avatar",
upload.single("avatar"),
(req: Request, res: Response): void => {
if (req.file === undefined) {
res.status(400).json({
message: "请选择头像文件",
});
return;
}
res.status(201).json({
data: {
fieldname: req.file.fieldname,
originalname: req.file.originalname,
mimetype: req.file.mimetype,
size: req.file.size,
filename: req.file.filename,
},
});
},
);
upload.single("avatar") 表示只接受字段名为 avatar 的一个文件,结果放在:
req.file
如果客户端使用其他字段名,通常会产生 LIMIT_UNEXPECTED_FILE。
5. 浏览器 FormData
const input = document.querySelector<HTMLInputElement>(
"#avatar",
);
const file = input?.files?.[0];
if (file === undefined) {
throw new Error("请选择文件");
}
const formData = new FormData();
formData.append("avatar", file);
formData.append("description", "用户头像");
const response = await fetch("/api/upload/avatar", {
method: "POST",
body: formData,
});
不要手动写:
headers: {
"Content-Type": "multipart/form-data",
}
浏览器需要自动生成包含 boundary 的 Content-Type:
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary...
手动覆盖后缺少正确 boundary,服务器可能无法解析请求。
6. Axios
浏览器环境中同样直接传 FormData:
await axios.post("/api/upload/avatar", formData);
一般不需要手动设置 Content-Type。Node.js 端使用 Axios 构造 multipart 时,具体 Header 处理取决于 FormData 实现,需要使用该实现生成的 headers。
7. HTML form
<form
action="/api/upload/avatar"
method="post"
enctype="multipart/form-data"
>
<input type="text" name="description" />
<input type="file" name="avatar" />
<button type="submit">上传</button>
</form>
两个关键点:
enctype="multipart/form-data"
name="avatar"
name 必须与服务端 .single("avatar") 对应。
8. req.body 与 req.file
console.log(req.body.description);
console.log(req.file);
Multer 会把 multipart 文本字段放入 req.body,文件放入 req.file 或 req.files。
但在 DiskStorage 的 destination() 和 filename() 回调执行时,req.body 不一定已经完整,因为客户端可以先发送文件 part,再发送文本 part。
因此不要在 storage 回调里依赖尚未确定出现顺序的文本字段决定安全关键路径。
9. 文件信息
每个文件可能包含:
| 属性 | 含义 | 来源/存储类型 |
|---|---|---|
fieldname |
表单字段名 | 客户端协议 |
originalname |
用户电脑上的原始文件名 | 不可信 |
encoding |
multipart 编码信息 | 客户端声明 |
mimetype |
multipart 声明的 MIME | 不可信 |
size |
文件字节数 | Multer 统计 |
destination |
保存目录 | DiskStorage |
filename |
保存名称 | DiskStorage |
path |
保存路径 | DiskStorage |
buffer |
完整文件 Buffer | MemoryStorage |
originalname 和 mimetype 都不能作为安全结论。
10. express.json() 与 Multer
express.json() 不解析 multipart,Multer 也不解析普通 JSON 文件上传。
可以在应用中保留:
app.use(express.json());
上传路由仍然需要:
upload.single("avatar")
它们处理不同 Content-Type,不是互相替代。
11. 不要全局安装 Multer
错误示意:
app.use(multer().any());
这会让原本不应该接收文件的路由也能接收上传。Multer 应只出现在明确处理文件的路由上。
12. 最小限制
即使是学习接口,也应限制大小和数量:
const upload = multer({
dest: "uploads/",
limits: {
fileSize: 2 * 1024 * 1024,
files: 1,
fields: 5,
parts: 6,
},
});
默认文件大小和文件数上限可能是 Infinity,不应依赖默认值保护生产接口。
13. 练习题
- 创建一个单头像上传接口,限制文件最大 1MB。
- 使用 HTML form 同时上传 avatar 和 nickname。
- 使用 fetch FormData 调用接口,不手动设置 Content-Type。
- 故意把前端字段名改成 photo,观察 Multer 错误。
- 对比 JSON 请求、urlencoded 请求和 multipart 请求的
req.body解析方式。 - 输出
req.file的全部属性,区分客户端信息和服务器信息。