分析
1、首先这是 koa2 最简单的入门例子,我将通过这个入门例子来演示 koa2 的洋葱模型
const Koa = require('koa');
const app = new Koa();
app.use((ctx,next)=>{console.log("第一个中间件执行");
next() ;});
// 第二个中间件
app.use((ctx,next)=>{console.log("第二个中间件");
})
let r = app.listen(8080);
console.log("server run on 8080");
在这里面,app 首先是调用了两次 use,然后就开始监听端口,
listen(...args) {debug('listen');
// 当客户端发送请求将会调用 callback
const server = http.createServer(this.callback());
return server.listen(...args);
}
因此 use 是核心:
use(fn) {
// ... 一些判断代码
this.middleware.push(fn);
return this;
}
从上面可以看出这里将外部 use 的函数通过内部的一个 middleware 变量记录下来,然后就没了。
OK,现在当客户端发送请求的时候, 内部会创建上下文对象,然后处理请求:
callback() {const fn = compose(this.middleware);
if (!this.listenerCount('error')) this.on('error', this.onerror);
const handleRequest = (req, res) => {
// 创建上下文
const ctx = this.createContext(req, res);
// 处理请求
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
处理请求
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
// 核心,调用中间件, 从这里可以看出我们 use(fn)中的 fn 是一个 promise
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
OK,到这里我们知道当浏览器发送一个请求的时候,koa 的 application 对象会根据 middleware 调用 compose 来生成一个另一个函数 fn, 然后向 fn 中传入上下文 ctx 执行这个函数。我们都知道,最上面的代码执行顺序是先打印 第一个中间件执行
, 再打印 第二个中间件执行
, 那么这个 compose 这个函数就需要来保证这个机制。具体怎么实现看如下:
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)
}
}
}
}
上面代码虽然不多,但是是 koa2 实现洋葱模型最重要的部分。整个过程如下:
- 客户端发送请求, 调用 fn(ctx), 此时 next 为空 =>dispatcher(0), 获取第一次 use 的函数(middleware[0])执行这个函数, 参数为:ctx、dispatch(1), 这个 dispatcher(1), 也就是第一次 use 中的 next; 执行 next();
- 在第一次 use 中的方法体中执行 next()等价于执行 dispatcher(1), 此时获取第二次 use 的函数(middleware[1]), 接着再执行这个函数, 参数为:ctx、dispatch(2), 以此类推执行后面的中间件.
总结
koa2 源码虽然少,但是原理巧妙,值得学习,也正是因为它的小,对于 w 我们看源码学习也能更轻松.