- 文档
- 教程
- 后端
【免费下载链接】koa2-note
《Koa2进阶学习笔记》已完结🎄🎄🎄
导读
本文对应《Koa2进阶学习笔记》中"上传文件简单实现"一节,核心讲解如何在 Koa2 应用中不借助 koa-bodyparser、只依赖busboy模块完成multipart/form-data表单文件上传:包括依赖安装、上传工具函数封装、路由入口接入与运行验证。读完本文,你将掌握busboy的事件驱动解析流程(file/field/finish/error)、流式写入文件的方法,以及一套可直接复用的上传封装代码。
本仓库中与该文档对应的完整可运行示例位于 demo/upload,工具函数实现在 util/upload.js,入口文件为 index.js,配套的 busboy 模块笔记见 note/upload/busboy.md。
运行结果截图如下(来源:原文档"运行结果"章节):
依赖模块与安装
安装 busboy
按文档要求,在示例目录下执行:
npm install --save busboybusboy用于解析 POST 请求中 node 原生req携带的文件流(即multipart/form-data编码的请求体),是本节实现的核心依赖。- 从 demo/upload/package.json 可以看到示例的实际依赖为
"busboy": "^0.2.14"与"koa": "^2.3.0",并且声明了运行环境"node": ">=7.6.0"——因为示例代码大量使用了async/await,需要 Node 7.6.0 及以上版本支持。
为什么需要 busboy
原生 Node.js 的req对象不会自动解析请求体,更不会区分普通表单字段与文件流。busboy是一个流式解析器:它接收req的请求头配置后,把传入的请求体按multipart/form-data的分隔边界(boundary)逐段切分,并通过事件回调把"文件流"和"普通字段"分别暴露出来。这与 koa-bodyparser 的做法不同:后者会把请求体整体解析为ctx.request.body,而本节演示的是直接对接 node 原生req的底层方案。
补充:
busboy的详细用法笔记见 note/upload/busboy.md,其中包含更基础的事件监听示例,本文的uploadFile即是对该机制的封装升级。
封装上传文件到写入服务的方法
核心思路:把"解析文件流 → 保存到指定目录 → 返回结果"这一过程封装成uploadFile(ctx, options)函数,返回一个 Promise,供路由层的async/await直接使用。封装文件位于 demo/upload/util/upload.js。
三个辅助函数
1. mkdirsSync —— 同步递归创建目录
function mkdirsSync( dirname ) { if (fs.existsSync( dirname )) { return true } else { if (mkdirsSync( path.dirname(dirname)) ) { fs.mkdirSync( dirname ) return true } } }- 先检查目录是否存在,存在直接返回
true; - 不存在则递归创建父目录(
path.dirname(dirname)),父目录就绪后再fs.mkdirSync(dirname)创建当前目录; - 属于"同步递归建目录"的经典写法,用于确保
path.join(options.path, fileType)组合出的目标目录一定存在。
2. getSuffixName —— 获取文件后缀名
function getSuffixName( fileName ) { let nameList = fileName.split('.') return nameList[nameList.length - 1] }- 通过
split('.')切分文件名,取最后一段作为后缀; - 例如
avatar.jpg→jpg,用于在生成随机文件名时保留原始文件类型(图片、文档等扩展名)。
3. uploadFile —— 上传主函数
function uploadFile( ctx, options) { let req = ctx.req // koa 上下文中的 node 原生请求对象 let res = ctx.res // node 原生响应对象(本示例未使用) let busboy = new Busboy({headers: req.headers}) // 获取类型 let fileType = options.fileType || 'common' let filePath = path.join( options.path, fileType) let mkdirResult = mkdirsSync( filePath ) return new Promise((resolve, reject) => { console.log('文件上传中...') let result = { success: false, formData: {}, // 用于收集普通表单字段 } ... }) }关键点:
ctx.req是 Koa 上下文暴露的 node 原生req,busboy必须用原生请求头的headers构造实例,才能正确识别multipart边界;options.fileType用于按类型分子目录(示例中传入'album',即"相册"),默认值为'common';filePath = path.join(options.path, fileType)即"上传根目录 + 文件类型子目录";- 整个解析过程包在
new Promise(...)中,把异步的事件回调结果收敛为 Promise 的resolve/reject,方便路由层await。
四个 busboy 事件
file 事件 —— 处理文件流
busboy.on('file', function(fieldname, file, filename, encoding, mimetype) { let fileName = Math.random().toString(16).substr(2) + '.' + getSuffixName(filename) let _uploadFilePath = path.join( filePath, fileName ) let saveTo = path.join(_uploadFilePath) // 文件保存到指定路径 file.pipe(fs.createWriteStream(saveTo)) // 文件写入事件结束 file.on('end', function() { result.success = true result.message = '文件上传成功' console.log('文件上传成功!') resolve(result) }) })fieldname是表单中文件域的 name(本示例为file),file是文件的可读流对象,filename是原始文件名,encoding/mimetype分别是编码与 MIME 类型;- 文件名使用
Math.random().toString(16).substr(2)生成 16 进制随机串作为主名,再拼接getSuffixName(filename)得到的后缀,既避免重名覆盖,又保留扩展名; - 通过
file.pipe(fs.createWriteStream(saveTo))把文件流直接管道写入目标路径,全程流式处理、不占内存; file流触发end事件表示写入完成,此时将result.success置为true并写入提示信息;- 注:仓库 demo/upload/util/upload.js 中的实际实现将
resolve(result)统一放在下面的finish事件中,end事件只负责标记成功状态,两种写法都能工作,文档版本把resolve提前到end便于说明"文件写完即可认为成功"。
field 事件 —— 解析普通表单字段
busboy.on('field', function(fieldname, val, fieldnameTruncated, valTruncated, encoding, mimetype) { console.log('表单字段数据 [' + fieldname + ']: value: ' + inspect(val)); result.formData[fieldname] = inspect(val); });- 表单里与文件并列的普通文本字段(如示例中的
picName)会触发field事件; - 使用
util.inspect(val)将字段值转为可读字符串,收集进result.formData,供后续业务使用; - 额外的
fieldnameTruncated/valTruncated参数用于标识字段名/值是否被截断。
finish 事件 —— 解析结束
busboy.on('finish', function( ) { console.log('文件上结束') resolve(result) })- 当整个请求体解析完成(所有文件与字段处理完毕)时触发,此时调用
resolve(result)把最终结果返回给调用方。
error 事件 —— 解析出错
busboy.on('error', function(err) { console.log('文件上出错') reject(result) })- 解析过程出现错误时触发,调用
reject(result)让 Promise 进入失败状态,由路由层捕获处理。
最后通过req.pipe(busboy)把原生请求体接入 busboy,触发整个解析流程;模块导出{ uploadFile }供入口文件解构引用。
入口文件:路由接入与表单页面
入口文件 demo/upload/index.js 只注册了一个中间件,用ctx.url+ctx.method做分支路由:
GET / —— 返回上传表单页
if ( ctx.url === '/' && ctx.method === 'GET' ) { let html = ` <h1>koa2 upload demo</h1> <form method="POST" action="/upload.json" enctype="multipart/form-data"> <p>file upload</p> <span>picName:</span><input name="picName" type="text" /><br/> <input name="file" type="file" /><br/><br/> <button type="submit">submit</button> </form> ` ctx.body = html }- 直接在
ctx.body输出一段内联 HTML; - 表单必须声明
enctype="multipart/form-data"与method="POST",否则 busboy 无法解析文件流; - 表单包含一个文本字段
picName与一个文件域file,正好对应 uploadFile 中的field与file两个事件。
POST /upload.json —— 处理上传
} else if ( ctx.url === '/upload.json' && ctx.method === 'POST' ) { let result = { success: false } let serverFilePath = path.join( __dirname, 'upload-files' ) result = await uploadFile( ctx, { fileType: 'album', // common or album path: serverFilePath }) ctx.body = result }- 上传根目录为
path.join(__dirname, 'upload-files'),即相对入口文件所在目录的upload-files目录,最终文件实际存放在upload-files/album/随机名.后缀; - 通过
await uploadFile(ctx, { fileType: 'album', path: serverFilePath })等待上传完成,结果以 JSON 形式返回给前端; - 这也是整个 demo 唯一的业务分支:
fileType支持'album'/'common'等任意自定义类型值。
其他请求 —— 404
} else { ctx.body = '<h1>404!!! o(╯□╰)o</h1>' }对非/、非/upload.json的请求统一返回 404 提示。
启动服务
app.listen(3000, () => { console.log('[demo] upload-simple is starting at port 3000') })服务监听 3000 端口,启动后在浏览器访问http://localhost:3000/即可看到上传表单。
运行与结果验证
按原文档"运行结果"章节的截图,完整流程如下:
- 启动服务:控制台输出
[demo] upload-simple is starting at port 3000; - 浏览器访问
http://localhost:3000/,看到含picName文本框与file文件选择框的表单页; - 选择本地图片等文件并提交,服务端控制台依次打印
文件上传中...、表单字段数据(picName)、文件上传成功!; - 浏览器收到 JSON 响应(
success: true、message: '文件上传成功'、formData中携带picName字段),同时在upload-files/album/目录下生成随机命名的上传文件。
相关运行截图见文章开头的两张图(以及原文档中的另外两张: upload-simple-result-02.png 与 upload-simple-result-04.png)。
关键机制与注意事项小结
- 流式处理:
busboy全程以流的方式消费请求体,file.pipe(fs.createWriteStream(...))边读边写,即使大文件也不会把整个文件载入内存,这是选择它做文件上传的核心原因; - 事件驱动:
file、field、finish、error四个事件分别对应"文件流"、"普通字段"、"解析完成"、"出错",封装时务必覆盖finish与error两个收尾事件,避免 Promise 悬空; - Promise 化:通过
new Promise把 busboy 的异步事件收敛为可await的接口,是让上传逻辑融入 Koa2 中间件async/await体系的关键一步; - 目录与命名:
mkdirsSync递归建目录保证目标路径存在;Math.random().toString(16).substr(2) + '.' + 后缀的随机命名避免并发上传互相覆盖,但也意味着原文件名被丢弃,如需保留原始文件名需自行扩展; - 环境要求:示例依赖
koa@^2.3.0与 Node>=7.6.0(见 demo/upload/package.json),运行前请确认 Node 版本; - 对照进阶:本文属于上传功能的"简单实现"(同步思路 + 手动事件封装),仓库中另有基于异步流程、带进度展示的进阶版本 demo/upload-async 及其笔记 note/upload/pic-async.md,可进一步对比学习。
相关文档索引
- 本文主体笔记:note/upload/simple.md
- busboy 模块详解:note/upload/busboy.md
- 示例源码:上传封装 demo/upload/util/upload.js、入口 demo/upload/index.js、依赖声明 demo/upload/package.json
- 进阶版本:异步上传示例 demo/upload-async 与笔记 note/upload/pic-async.md
- 文档
- 教程
- 后端
【免费下载链接】koa2-note
《Koa2进阶学习笔记》已完结🎄🎄🎄
相关推荐
Koa2 文件上传基石:busboy 模块解析 multipart 表单流实战(koa2-note 学习笔记)
Koa2 文件上传基石:busboy 模块解析 multipart 表单流实战(koa2 note 学习笔记) 本文是 koa2 note 开源项目《Koa2
文档教程后端Koa2 异步图片上传实战:基于 busboy 流式解析与 XHR 进度监听实现(koa2-note)
Koa2 异步图片上传实战:基于 busboy 流式解析与 XHR 进度监听实现(koa2 note) 本篇指南以《Koa2进阶学习笔记》开源仓库中的异步上传图
文档教程后端koa2-note 实战:用 koa-bodyparser 中间件解析 Koa2 POST 请求体
koa2 note 实战:用 koa bodyparser 中间件解析 Koa2 POST 请求体 在 Koa2 中,GET 请求参数可以通过 ctx.quer
文档教程后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考