系列文章目录
从零学习Node.js框架Koa 【一】 Koa 初探从环境搭建到第一个应用程序
从零学习Node.js框架Koa 【二】Koa 核心机制解析:中间件与 Context 的深度理解
从零学习Node.js框架Koa 【三】Koa路由与静态资源管理:处理请求与响应
从零学习Node.js框架Koa 【四】Koa 与数据库(MySQL)连接,实现CRUD操作
从零学习Node.js框架Koa 【五】Koa鉴权全解析:JWT+Redis构建安全认证系统
从零学习Node.js框架Koa 【六】Koa文件上传下载实现:@koa/multer 与 koa-send 深度解析
文章目录
- 系列文章目录
- 前言
- 一、Koa上传功能
-
- [1. @koa/multer是什么](#1. @koa/multer是什么)
- [2. 依赖安装](#2. 依赖安装)
- [3. 基础使用示例](#3. 基础使用示例)
-
- (1)单文件上传
- [(2)多文件上传 - 同一字段名](#(2)多文件上传 - 同一字段名)
- [(3)多文件上传 - 不同字段名](#(3)多文件上传 - 不同字段名)
- (4)文件大小和总个数限制
- (5)文件类型限制
- [4.multer(options) 详解](#4.multer(options) 详解)
- [5. 实战开发示例](#5. 实战开发示例)
- 二、Koa下载功能
-
- 1、依赖安装
- [2、 基础使用示例:](#2、 基础使用示例:)
- 3、koa-send详解
- 三、总结
前言
web应用开发中我们经常需要处理文件上传和下载功能,在 Koa 框架中,虽然没有内置的文件处理模块,但通过第三方优秀的中间件,我们可以轻松实现这些功能。本篇文章将继续介绍如何在 Koa 中使用 @koa/multer 处理文件上传和使用koa-send实现文件下载的最佳实践。
一、Koa上传功能
实现Koa上传功能有多种方案,可以选择koa-body中间件方案也可以使用@koa/multer,这里选择使用@koa/multer,实现更优雅的封装。
1. @koa/multer是什么
@koa/multer 是专为 Koa 框架设计的文件上传中间件,它基于 Express 的 multer 进行适配,让 Koa 能够轻松处理 multipart/form-data 格式的文件上传,包括单文件、多文件等不同场景
2. 依赖安装
javascript
npm install @koa/multer multer
3. 基础使用示例
javascript
const Router = require("@koa/router");
const router = new Router();
const path = require("path");
const multer = require("@koa/multer");
// 配置存储引擎
const storage = multer.diskStorage({
destination: (req, file, cb) => {
// 设置上传文件存储路径(/public/uploads),目录如不存在需手动创建
cb(null, path.join(__dirname, "../public/uploads"));// 第一个参数为错误对象,null 表示无错误
},
filename: (req, file, cb) => {
// 生成唯一文件名
const fileExt = path.extname(file.originalname);
//格式:时间戳+文件扩展名
const fileName = `${Date.now()}_${fileExt}`;
cb(null, fileName);
},
});
// 创建 multer 实例
const multerUpload = multer({
storage,
});
说明:multer 是个函数,入参是个可选配置对象,包括dest/storage、limits、fileFilter、preservePath 等参数,后面会详细介绍。
(1)单文件上传
javascript
//单文件上传,字段名为file
router.post("/api/upload/single", multerUpload.single("file"), async (ctx) => {
// 单文件上传成功后,文件信息在ctx.file中
const file = ctx.file;
ctx.body = {
code: 200,
msg: "上传成功",
data: file,
};
});
说明:单文件上传一次只能传一个文件,通过实例single方法实现,入参为上传表单的字段名(通常设为"file")
(2)多文件上传 - 同一字段名
javascript
//多文件上传,最多3个文件,字段名为files
router.post("/api/upload/multiple", multerUpload.array("files",3), async (ctx) => {
// 多文件上传成功后,文件信息在ctx.files中
const files = ctx.files;
ctx.body = {
code: 200,
msg: "上传成功",
data: files,
};
});
说明:多文件上传一次能传多个文件,通过实例array方法实现,第一个入参为上传表单的字段名(通常设为"files"),第二个入参表示允许的上传最大文件数
(3)多文件上传 - 不同字段名
javascript
//多文件上传 - 不同字段名
router.post("/api/upload/multiple", multerUpload.fields([
{ name: 'avatar', maxCount: 1 },// 头像,最多1个
{ name: 'photos', maxCount: 3 }// 照片,最多3个
]), async (ctx) => {
// 多文件上传成功后,文件信息在ctx.files中
const files = ctx.files;
ctx.body = {
code: 200,
msg: "上传成功",
data: files,
};
});
说明:多文件上传一次能传多个文件,多个文件可通过不同字段区分,通过实例fields方法实现,入参为对象数组,对象name表示上传表单字段,maxCount表示该字段允许最大上传数量
(4)文件大小和总个数限制
javascript
// 创建 multer 实例
const multerUpload = multer({
storage,
limits: {
fileSize: 20 * 1024 * 1024, // 限制文件大小20MB
files: 6,//一次请求最多6个文件
},
});
说明:可通过limits参数限制上传文件大小和文件个数,其中特别注意files参数,表示一次http请求最多上传的文件数量,跟前面讲的multerUpload.array("files",maxCount)或multerUpload.fields([
{ name: 'avatar', maxCount: 1 },// 头像,最多1个
{ name: 'photos', maxCount: 3 }// 照片,最多3个
]),中maxCount的区别在于,maxCount针对的限制单个字段的上传个数,而limits.files是限制所有字段上传文件总个数。可以看出使用array的时候不管设置maxCount还是limits.files效果一样,因为只有一个字段。而multerUpload.fields就要求数组内maxCount之和要小于limits.files才不会报错。
(5)文件类型限制
javascript
// 文件过滤器
const fileFilter = (req, file, cb) => {
// 允许的图像类型
const allowedMimes = ["image/jpeg", "image/png", "image/gif", "image/webp"];
if (allowedMimes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error(`不支持的文件类型: ${file.mimetype}`), false);
}
};
// 创建 multer 实例
const multerUpload = multer({
storage,
limits: {
fileSize:20* 1024 * 1024, // 20MB 限制
files: 6,//最多6个文件
},
fileFilter,//设置过滤器
});
说明:上传文件类型限制可通过fileFilter(过滤器)自定义逻辑控制,共3个入参,第一个入参req 原生node请求体,第二个入参file文件对象,第三个入参cb回调函数。其中注意回调函数cb有2个入参,第一个入参为错误对象(Error )用来主动抛出错误,如果没错误可设置为null,第二个入参boolean类型,告知 multer 是否允许当前文件上传(true 允许,false 拒绝),优先级第一个参数高,当第一个参数 err 为 null 时第二个参数才生效。
4.multer(options) 详解
通过上面示例,我们已经熟悉了multer基础用法,这个模块我们将对multer进行总结和补充。
语法:
javascript
multer(options)
入参:options可选配置项,非必须,如果不传,文件会被临时存储在内存中(不写入磁盘)
javascript
//文件会被临时存储在内存中
multer()
返回值:multer实例
options常用配置项
- dest :文件存储配置,用于指定文件的存储位置和方式(仅简易场景使用,无法自定义文件名),文件会被写入磁盘。
类型:string
javascript
// 上传文件存储到项目根目录的 uploads 文件夹
const upload = multer({ dest: './uploads' });
- storage:自定义存储,实现更灵活的存储逻辑(如自定义文件名、存储到云存储等),内置2种存储引擎(diskStorage/memoryStorage),既可以选择内存存储(memoryStorage)也可以选择磁盘存储(diskStorage),实际开发中我们会使用自定义存储方式,存储到服务器的文件名正常会重命名防止覆盖问题。
类型:StorageEngine
javascript
//磁盘存储
const storage = multer.diskStorage({
destination: (req, file, cb) => {
// 存储到 uploads 目录
cb(null, './uploads');
},
filename: (req, file, cb) => {
// 自定义文件名:时间戳-原文件名
const uniqueName = Date.now() + '-' + file.originalname;
cb(null, uniqueName);
}
});
const upload = multer({ storage: storage });
javascript
//内存存储
const storage = multer.memoryStorage();
const upload = multer({
storage: storage,
limits: { fileSize: 1024 * 1024 * 2 } // 限制2MB,避免内存溢出
});
-
limits :上传限制配置 ,类型:Object
{
fileSize:number,//单文件最大大小
files:number,//单次请求最大文件数量
...}
-
fileFilter:文件过滤函数(控制哪些文件允许上传、哪些文件拒绝(如限制文件类型为图片 / 视频)),类型Function
javascript
const fileFilter = (req, file, cb) => {
// 允许的文件 MIME 类型
const allowedMimeTypes = ['image/jpeg', 'image/png', 'image/gif'];
// 判断文件类型是否合法
if (allowedMimeTypes.includes(file.mimetype)) {
cb(null, true); // 允许上传
} else {
cb(new Error('仅允许上传 jpg/png/gif 格式的图片!'), false); // 拒绝并抛错
}
};
const upload = multer({
dest: './uploads',
fileFilter: fileFilter
});
multer实例方法
创建 multer实例后,需调用其方法生成 Koa 中间件给路由使用,常用的方法如下:
- single(fieldname):上传单个文件,fieldname(string):表单中文件上传的字段名(如 < input type="file" name="avatar" /> 中的 avatar)。上传完文件信息会被挂载到 ctx.file 上。该方法返回值是个中间件。
javascript
// 接收字段名为 avatar 的单个文件
router.post("/api/upload/avatar",multerUpload.single('avatar'), async (ctx) => {
ctx.body = {
code: 200,
msg: "上传成功",
data: ctx.file,//文件信息,是个对象
};
});
- array(fieldname, [maxCount]):上传多个同名字段的文件,fieldname(string):表单文件字段名,maxCount(number,可选):最多接收的文件数量,超出则抛错。上传完文件信息会被挂载到 ctx.files 上。
javascript
// 接收字段名为 photos 的最多6个文件
router.post("/api/upload/photos",multerUpload.array('photos',6), async (ctx) => {
ctx.body = {
code: 200,
msg: "上传成功",
data: ctx.files,//文件信息,是个数组
};
});
- fields(fields):上传多个不同字段的文件,fields(Array),数组中的每个元素为对象,包含 name(字段名)和 maxCount(最大数量)。上传完文件信息会被挂载到 ctx.files 上。
javascript
// 接收 avatar(最多1个)和 photos(最多5个)字段的文件
router.post("/api/upload/file",multerUpload.fields([
{ name: 'avatar', maxCount: 1 },
{ name: 'photos', maxCount: 5 }
], async (ctx) => {
ctx.body = {
code: 200,
msg: "上传成功",
data: ctx.files,//文件信息,是个数组
};
});
文件信息对象
| 属性名 | 类型 | 说明 |
|---|---|---|
fieldname |
字符串 | 表单中的文件字段名 |
originalname |
字符串 | 文件的原始名称(如 avatar.jpg) |
encoding |
字符串 | 文件的编码格式(如 utf-8) |
mimetype |
字符串 | 文件的 MIME 类型(如 image/jpeg) |
size |
数值 | 文件大小(字节) |
destination |
字符串 | 磁盘存储的目录(仅 diskStorage 有效) |
filename |
字符串 | 磁盘存储的文件名(仅 diskStorage 有效) |
path |
字符串 | 文件的完整磁盘路径(仅 diskStorage 有效) |
buffer |
Buffer | 文件的二进制数据(仅 memoryStorage 有效) |
5. 实战开发示例
在实际项目开发中,我们要求对上传的文件进行分类存储,主要分为图片和其他文件两类,分别存放在不同的目录中。具体来说,图片文件将存储在 public/uploads/image 目录下,其他类型文件则存放在 public/uploads/file 目录下。同时,文件还需按照上传时间自动生成对应的日期目录。例如,假设一张图片的上传时间为 2025 年 11 月 20 日,那么它的最终存储路径为 public/uploads/image/2025/11/20/xxxxxx.png(如下图所示),这种做法好处是可以批量管理按时间删除图片,最后我们还需要捕获错误返回正确的状态码和提示文字。

功能需求总结:
封装一个高度可配置的文件上传中间件,支持:
- 自动创建目录结构(按日期)
- 文件分类存储(图片/普通文件)
- 自定义文件过滤
- 文件大小和数量限制
- 友好的错误处理
实现代码
上传中间件 :
middleware/uploadMiddleware.js
javascript
const multer = require("@koa/multer");
const path = require("path");
const fs = require("fs");
const dayjs = require("dayjs");
//存储目录
const uploadDir = {
image: path.join(__dirname, "../public/uploads/image"), //图片上传目录路径
file: path.join(__dirname, "../public/uploads/file"), //文件上传目录路径
};
// 递归创建存储目录
const mkdir = (dirPath) => {
if (!fs.existsSync(dirPath)) {
fs.mkdirSync(dirPath, { recursive: true });
}
};
/**
* 上传中间件主函数
*入参可选配置(对象):
* @param {*} uploadPath :上传文件路径(可选)
* @param {*} limits :上传文件大小、个数限制可选配置,默认20MB、一次最多传9个文件 (可选)
* limits:{
fileSize: 20 * 1024 * 1024, // 单个文件最大20MB
files: 9, //一次最多9个文件
}
* @param {*} allowedMimes :允许文件类型,默认全部允许 (可选),数组例如:["image/jpeg", "image/png", "image/gif", "image/webp"]
* @param {*} fileFilter :自定义过滤函数 (可选)
* @returns
*/
const uploadMiddleware = ({
uploadPath = null,
limits = {},
allowedMimes = null,
fileFilter = null,
} = {}) => {
// 配置存储引擎
const storage = multer.diskStorage({
destination: (req, file, cb) => {
// 通过 MIME 类型检测是图片类型还是其他文件
const targetDir =
uploadPath || file.mimetype.startsWith("image/")
? uploadDir.image
: uploadDir.file;
//上传目录
let realUploadDir = path.join(
targetDir,
`${dayjs().format("YYYY/MM/DD")}`
);
//创建日期目录
mkdir(realUploadDir);
cb(null, realUploadDir);
},
filename: (req, file, cb) => {
//文件名前缀
let prefix=file.mimetype.startsWith("image/") ? "image" : "file";
// 生成唯一文件名
const fileExt = path.extname(file.originalname);
//格式:image或file+ 时间戳 + 随机数 + 文件扩展名
const fileName = `${prefix}_${Date.now()}_${Math.random()
.toString(36)
.substring(2)}${fileExt}`;
cb(null, fileName);
},
});
// 自定义文件过滤器
const _fileFilter = (req, file, cb) => {
// 如果传入了自定义过滤器,使用自定义的
if (fileFilter) {
return fileFilter(req, file, cb);
}
// 如果指定了允许的 MIME 类型
if (Array.isArray(allowedMimes) && allowedMimes.length > 0) {
if (allowedMimes.includes(file.mimetype)) {
cb(null, true);
} else {
// 生成友好的错误信息
const allowedTypes = allowedMimes.map(item => {
const parts = item.split('/');
return parts.length > 1 ? parts[1] : parts[0];
});
cb(
new Error(`仅支持 ${allowedTypes.join('、')} 格式的文件`),
false
);
}
} else {
// 没有限制,允许所有文件
cb(null, true);
}
};
// 创建 multer 实例
const multerUpload = multer({
storage,
limits: {
fileSize: 20 * 1024 * 1024, // 20MB 限制
files: 9, //最多9个文件
...limits,
},
fileFilter: _fileFilter,
});
// 错误处理函数
const handleMulterError = (err, ctx) => {
const multerFileErrors = [
{ key: "LIMIT_FILE_SIZE", message: "文件大小超限" },
{ key: "LIMIT_FILE_COUNT", message: "文件数量超限" },
{ key: "LIMIT_UNEXPECTED_FILE", message: "意外的文件字段" },
{ key: "LIMIT_FIELD_KEY", message: "字段名过长" },
{ key: "LIMIT_FIELD_VALUE", message: "字段值过长" },
{ key: "LIMIT_FIELD_COUNT", message: "字段数量超限" },
{ key: "LIMIT_PART_COUNT", message: "表单字段数量超限" },
];
const target =
err.code && multerFileErrors.find((item) => item.key === err.code);
ctx.status = 400;
ctx.body = {
code: 400,
message: target?.message ?? err.message ?? "上传失败",
};
};
// 适配函数 - 使用 try-catch
const handleUpload = (type, fieldName, fields = []) => {
let uploadMiddleware;
switch (type) {
case "single":
uploadMiddleware = multerUpload.single(fieldName);
break;
case "array":
uploadMiddleware = multerUpload.array(fieldName, fields[0]?.maxCount);
break;
case "fields":
uploadMiddleware = multerUpload.fields(fields);
break;
default:
throw new Error("Unknown upload type");
}
return async (ctx, next) => {
try {
await uploadMiddleware(ctx, next);
} catch (err) {
handleMulterError(err, ctx);
}
};
};
return {
single: (fieldName = "file") => handleUpload("single", fieldName),//单文件上传
array: (fieldName = "files") => handleUpload("array", fieldName),//多文件上传 - 相同字段
fields: (fields) => handleUpload("fields", null, fields),//多文件上传 -不同字段
};
};
module.exports = uploadMiddleware;
说明:1、上述代码入口函数入参定义了可配置的对象,支持自定义上传路径,文件大小、类型限制配置、自定义过滤器等便于扩展的功能。2、自动根据上传的文件类型(图片或文件)存储不同的目录,如果目录不存在会自动创建。3、友好处理了各种错误情况的提示,把原来库自带的英文转换为对应的中文提示。4、最后暴露的3个不同方法对应不同的的文件上传方案,保持和@koa/multer原来的使用一致性。
使用示例:
路由文件(routes/upload.js):
javascript
const Router = require("@koa/router");
const router = new Router();
const path = require("path");
const uploadMiddleware = require('../middleware/uploadMiddleware');//上传中间件
const upload = uploadMiddleware(
//可选配置项
// {
// limits:{
// fileSize: 20 * 1024 * 1024, // 单个文件最大20MB
// files: 5, //一次最多5个文件
// },
// allowedMimes:["image/jpeg", "image/png"],//允许上传文件格式
// //自定义过滤函数
// fileFilter:(req, file, cb)=>{
// }
// }
);
//上传路由(单文件)
router.post("/api/upload/single", upload.single(), async (ctx) => {
const file = ctx.file;
// 上传成功,返回文件信息
ctx.body = {
code: 200,
msg: "上传成功",
data: {
filename: file.filename, //文件名
path: `/static/${file.path.split("\\public\\")[1].replaceAll("\\", "/")}`, //设置文件访问路径,public替换为static,例如/static/uploads/2025-11-20/1763610844193_sttgu3zux0g.png
size: Math.round(file.size / 1024), //文件大小,单位kb
},
};
});
//上传路由(多文件)
router.post("/api/upload/multiple", upload.array(), async (ctx) => {
const files = ctx.files;
// 上传成功,返回文件信息
ctx.body = {
code: 200,
msg: "上传成功",
data: files.map((item) => ({
filename: item.filename,
path: `/static/${item.path.split('\\public\\')[1].replaceAll('\\','/')}` , //设置文件访问路径
size: Math.round(item.size / 1024), //文件大小,单位kb
})),
};
});
运行测试:
(1)单文件


(2)多文件


(3)错误提示



二、Koa下载功能
Koa下载功能实现可以使用koa-send这个利器, koa-send是一个专为 Koa 设计的文件传输工具,让你用最少的代码实现最强大的下载功能。
1、依赖安装
javascript
npm install koa-send
2、 基础使用示例:
javascript
const Router = require("@koa/router");
const router = new Router();
const path = require("path");
const send = require("koa-send");
//下载
router.get("/api/download/:filename", async (ctx) => {
const fileName = ctx.params.filename; // 待下载的文件名
const rootDir = path.join(__dirname, "../public/uploads/file"); // 文件根目录(限制下载范围)
try {
// 调用 koa-send 下载文件
await send(ctx, fileName, {
root: rootDir, // 必须指定根目录
setHeaders: (res) => {
// 设置 Content-Disposition 为 attachment,强制下载,不设默认预览
res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(fileName)}"`);
}
});
} catch (err) {
// 处理错误
ctx.status = err.status || 404;
ctx.body = { code: ctx.status, message: "文件不存在" };
}
});
3、koa-send详解
语法
javascript
async function send(ctx, path, options) {}
参数说明:
- ctx: Koa 上下文对象(必须)
- path: 要发送的文件路径(相对路径或绝对路径,必填)
- options: 配置对象(可选),包含以下核心属性:
| 配置项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
root |
String | '' |
文件根目录(必填,若 path 为相对路径) |
index |
String/Boolean | 'index.html' |
目录默认文件,设为 false 禁用 |
maxage |
Number | 0 |
缓存控制头 max-age,单位毫秒 |
hidden |
Boolean | false |
是否允许发送隐藏文件(以.开头) |
gzip |
Boolean | true |
若客户端支持 gzip 且存在.gz文件,则优先发送压缩文件 |
br |
Boolean | true |
若客户端支持 brotli 且存在.br文件,则优先发送压缩文件 |
format |
Boolean | true |
禁用路径后缀解析(如.html) |
extensions |
Array | [] |
尝试匹配的文件扩展名(如['.html', '.htm']) |
setHeaders |
Function | undefined |
自定义响应头函数(参数:res, path, stats) |
immutable |
Boolean | false |
添加 Cache-Control: immutable(适用于长期缓存文件) |
说明:1、如果path设置的是相对路径(或文件名),options-root必须设置。2、下载默认开启协商缓存,如果想设置强缓存可以通过options-maxage设置缓存时间。3、文件下载默认为预览模式,可以通过自定义请求头设置"Content-Disposition"为"attachment; filename=文件名"强制下载
运行测试:


三、总结
通过本文介绍,我们全面掌握了在Koa框架中使用@koa/multer实现安全可控的文件上传功能,包括单文件、多文件上传、类型过滤和自动目录管理,以及利用koa-send实现高效安全的文件下载方案,这些核心技能为构建功能完备的Web应用奠定了坚实基础。同时,本文示例还提供的上传中间件封装方案具有很高的实用价值,可以直接应用于实际项目中。按日期分类存储、自动目录创建、友好的错误提示等特性,都是生产环境中不可或缺的功能。