前言
Koa 2.x
版本是当下最流行的 NodeJS 框架,Koa 2.0
的源码特别精简,不像 Express
封装的功能那么多,所以大部分的功能都是由 Koa
开发团队(同 Express
是一家出品)和社区贡献者针对 Koa
对 NodeJS 的封装特性实现的中间件来提供的,用法非常简单,就是引入中间件,并调用 Koa
的 use
方法使用在对应的位置,这样就可以通过在内部操作 ctx
实现一些功能,我们接下来就讨论常用中间件的实现原理以及我们应该如何开发一个 Koa
中间件供自己和别人使用。
Koa 的洋葱模型介绍
我们本次不对洋葱模型的实现原理进行过多的刨析,主要根据 API 的使用方式及洋葱模型分析中间件是如何工作的。
// 洋葱模型特点// 引入 Koa
const Koa = require("koa");
// 创建服务
const app = new Koa();
app.use(async (ctx, next) => {
console.log(1);
await next();
console.log(2);
});
app.use(async (ctx, next) => {
console.log(3);
await next();
console.log(4);
});
app.use(async (ctx, next) => {
console.log(5);
await next();
console.log(6);
});
// 监听服务
app.listen(3000);
// 1
// 3
// 5
// 6
// 4
// 2
我们知道 Koa
的 use
方法是支持异步的,所以为了保证正常的按照洋葱模型的执行顺序执行代码,需要在调用 next
的时候让代码等待,等待异步结束后再继续向下执行,所以我们在 Koa
中都是建议使用 async/await
的,引入的中间件都是在 use
方法中调用,由此我们可以分析出每一个 Koa
的中间件都是返回一个 async
函数的。
koa-bodyparser 中间件模拟
想要分析 koa-bodyparser
的原理首先需要知道用法和作用,koa-bodyparser
中间件是将我们的 post
请求和表单提交的查询字符串转换成对象,并挂在 ctx.request.body
上,方便我们在其他中间件或接口处取值,使用前需提前安装。
npm install koa koa-bodyparser
koa-bodyparser 具体用法如下:
// koa-bodyparser 的用法
const Koa = require("koa");
const bodyParser = require("koa-bodyparser");
const app = new Koa();
// 使用中间件
app.use(bodyParser());
app.use(async (ctx, next) => {
if (ctx.path === "/" && ctx.method === "POST") {
// 使用中间件后 ctx.request.body 属性自动加上了 post 请求的数据
console.log(ctx.request.body);
}
});
app.listen(3000);
根据用法我们可以看出 koa-bodyparser
中间件引入的其实是一个函数,我们把它放在了 use
中执行,根据 Koa
的特点,我们推断出 koa-bodyparser
的函数执行后应该给我们返回了一个 async
函数,下面是我们模拟实现的代码。
// 文件:my-koa-bodyparser.js
const querystring = require("querystring");
module.exports = functionbodyParser() {
returnasync (ctx, next) => {
await new Promise((resolve, reject) => {
// 存储数据的数组
let dataArr = [];
// 接收数据
ctx.req.on("data", data => dataArr.push(data));
// 整合数据并使用 Promise 成功
ctx.req.on("end", () => {
// 获取请求数据的类型 json 或表单
let contentType = ctx.get("Content-Type");
// 获取数据 Buffer 格式
let data = Buffer.concat(dataArr).toString();
if (contentType === "application/x-www-form-urlencoded") {
// 如果是表单提交,则将查询字符串转换成对象赋值给 ctx.request.body
ctx.request.body = querystring.parse(data);
} elseif (contentType === "applaction/json") {
// 如果是 json,则将字符串格式的对象转换成对象赋值给 ctx.request.body
ctx.request.body = JSON.parse(data);
}
// 执行成功的回调
resolve();
});
});
// 继续向下执行
await next();
};
};
在上面代码中由几点是需要我们注意的,即 next
的调用以及为什么通过流接收数据、处理数据和将数据挂在 ctx.request.body
要在 Promise 中进行。
首先是 next
的调用,我们知道 Koa
的 next
执行,其实就是在执行下一个中间件的函数,即下一个 use
中的 async
函数,为了保证后面的异步代码执行完毕后再继续执行当前的代码,所以我们需要使用 await
进行等待,其次就是数据从接收到挂在 ctx.request.body
都在 Promise 中执行,是因为在接收数据的操作是异步的,整个处理数据的过程需要等待异步完成后,再把数据挂在 ctx.request.body
上,可以保证我们在下一个 use
的 async
函数中可以在 ctx.request.body
上拿到数据,所以我们使用 await
等待一个 Promise 成功后再执行 next
。