从零学习Node.js框架Koa 【六】Koa文件上传下载实现:@koa/multer 与 koa-send 深度解析

系列文章目录

从零学习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 深度解析


文章目录


前言

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应用奠定了坚实基础。同时,本文示例还提供的上传中间件封装方案具有很高的实用价值,可以直接应用于实际项目中。按日期分类存储、自动目录创建、友好的错误提示等特性,都是生产环境中不可或缺的功能。

相关推荐
q***47181 小时前
Windows 上彻底卸载 Node.js
windows·node.js
洞窝技术5 小时前
一键屏蔽某国IP访问实战
前端·nginx·node.js
weixin79893765432...5 小时前
使用 node.js 的心得
node.js
fruge5 小时前
前端自动化脚本:用 Node.js 写批量处理工具(图片压缩、文件重命名)
前端·node.js·自动化
O***p6045 小时前
JavaScript在Node.js中的集群负载均衡
javascript·node.js·负载均衡
名字不相符7 小时前
攻防世界WEB难度一(个人记录)
学习·php·web·萌新
孟祥_成都8 小时前
深入 Nestjs 底层概念(1):依赖注入和面向切面编程 AOP
前端·node.js·nestjs
Q_Q5110082859 小时前
python+django/flask的结合人脸识别和实名认证的校园论坛系统
spring boot·python·django·flask·node.js·php
Q_Q5110082859 小时前
python+django/flask的选课系统与课程评价整合系统
spring boot·python·django·flask·node.js·php