一.Koa2中间件的使用方式
1.新建一个项目,命名为koa2-test
2.在命令行中,进入koa2-test,执行npm init -y
npm init -y
3.将此项目中的package.json中的"main":"index.js"替换为"main":"app.js"

npm install koa --save
4.创建app.js文件,将如下代码复制到app.js中,(一共有三个中间件),以下代码为官网示例代码
const Koa = require('koa');
const app = new Koa();
// logger 记录日志
app.use(async (ctx, next) => {
await next();
const rt = ctx.response.get('X-Response-Time');
console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});
// x-response-time 处理请求时间
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
});
// response
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
5.在命令行中启动(启动命令如下)
node app.js
6.浏览器访问 localhost:3000
命令行中打印如下:

代码执行流程解析:
1.先注册3个中间件,再监听3000端口。
2.在第一个logger中间件执行到await next();时,下面的代码先不执行,继续执行下一个中间件x-response-time,直到遇到ctx.body,再开始逆向执行await next();下的内容,最终打印出响应所需的时间。
此流程即为洋葱圈模型:(此图为网上搜索得到)

将代码进行如下修改,加入注释
const Koa = require('koa');
const app = new Koa();
// logger 记录日志
app.use(async (ctx, next) => {
console.log("第一层洋葱---开始")
await next();
const rt = ctx.response.get('X-Response-Time');
console.log(`${ctx.method} ${ctx.url} - ${rt}`);
console.log("第一层洋葱---结束")
});
// x-response-time 处理请求时间
app.use(async (ctx, next) => {
console.log("第二层洋葱---开始")
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
console.log("第二层洋葱---结束")
});
// response
app.use(async ctx => {
console.log("第三层洋葱---开始")
ctx.body = 'Hello World';
console.log("第三层洋葱---结束")
});
app.listen(3000);
请求在命令行打印如下:这样就清楚的解释了洋葱圈模型(由外向内执行,再由内向外执行)

此行为用洋葱来形容非常的形象,先由外向内执行一个一个的next,等执行到最中心,再由内向外一层层执行。
二.分析如何实现,并用代码模拟实现
从上面的例子中我们可以进行分析,中间件是如何进行实现的? 猜测至少应该有两个步骤:
1.app.use用来注册中间件,并进行收集
2.实现next机制:通过上一个next触发下一个next
// 引入http
const http = require('http')
// 组合中间件
function compose(middlewareList) {
return function (ctx) {
// 中间件调用的逻辑
function dispatch(i) {
const fn = middlewareList[i]
try {
return Promise.resolve(
// 执行中间件,并封装为Promise,格式兼容
fn(ctx, dispatch.bind(null, i + 1)) // Promise
)
} catch (err) {
return Promise.reject(err)
}
}
return dispatch(0)
}
}
// 定义构造函数
class Koa2 {
constructor () {
// 中间件数组
this.middlewareList = []
}
use(fn) {
this.middlewareList.push(fn)
return this
}
// 将req和res组合为ctx
createContext(req, res) {
const ctx = {
req,
res
}
ctx.query = req.query
return ctx
}
handleRequest(ctx, fn) {
return fn(ctx)
}
callback() {
const fn = compose(this.middlewareList)
return (req, res) => {
const ctx = this.createContext(req, res)
return this.handleRequest(ctx, fn)
}
}
// 创建服务并监听 ...args传入多个参数
listen(...args) {
const server = http.createServer(this.callback())
server.listen(..args)
}
}
module.exports = Koa2
结构为:在class Koa2中有 use createContext callback listen等方法!通过use方法来收集中间件。
compose为组合中间件的方法,从而实现next(),其中Promise.resolve()是为了防止,在使用app.use()时没有使用async包裹,就返回的不是promise函数,Promise.resolve()包裹后就一直返回promise。
将fn(ctx, dispatch.bind(null, i + 1))包裹在Promise.resolve()中,fn为async函数
-
Promise.resolve(value)方法返回一个以给定值解析后的
Promise对象。如果该值(指代value)为promise,返回这个promise;如果value值为promise,返回这个promise。此函数将类promise对象的多层嵌套展平。 -
在
MDN的解释中,如果Promise.resolve(value)中的value值为promise,则返回这个promise。在KOA2中,中间件为async await包裹的异步函数,而async await是promise的语法糖。因此即使用Promise.resolve(value)把中间件进行了包裹,也会不想影响结果,而且避免了中间件没有使用async await时的报错。
在这篇文章中也有提及。
三.代码分析
compose为组合中间件的方法,其实也就不难看出,整个中间件的核心功能就在compose,此方法将中间件push到middlewareList中。
因此重点在于在compose中进行递归,在监听到request请求的时候,将上下文对象ctx传入其中,最终使所有中间件按照洋葱圈模型执行。
当middlewares数组合成到最后一个中间件的时候,则直接返回,此时递归则结束。
Promise.resolve()
递归的返回值为什么要经过Promise.resolve()的包裹呢?因为涉及到async、await等相关的异步操作。若在使用app.use()时未用async包裹则会发生错误。
我们最终的目的是返回一个可接收上下文参数ctx的函数,因此需要对dispatch进行进一步的包装,就形成了我们最终的compose,dispatch执行的过程是一个递归的过程。
function compose(middlewareList) {
reutrn function(ctx) {
function dispatch(i) {
const fn = middlewareList[i]
try {
return Promise.resolve(
fn(ctx, dispatch.bind(null, i+1))
)
} catch (err) {
return Promise.reject(err)
}
}
reutrn dispatch(0)
}
}
KOA源码结构如下图:

lib文件夹下放着四个KOA2核心文件,application.js、context.js、request.js、response.js
koa-compose源码如下
'use strict'
/**
* Expose compositor.
*/
module.exports = compose
/**
* Compose `middleware` returning
* a fully valid middleware comprised
* of all those which are passed.
*
* @param {Array} middleware
* @return {Function}
* @api public
*/
function compose (middleware) {
if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
for (const fn of middleware) {
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
/**
* @param {Object} context
* @return {Promise}
* @api public
*/
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}