最近一年零零散散看了很多开源项目的源码, 多少也有点心得, 这里想经过这篇文章总结一下, 这里以Koa为例, 前段时间其实看过Koa的源码, 可是发现理解的有点误差, 因此从新过一遍.git
不得不说阅读tj的代码真的收获很大, 没啥奇技淫巧, 代码优雅, 设计极好. 注释什么的就更不用说了. 总之仍是推荐把他的项目都过一遍(逃)github
Koa做为一个web框架, 咱们要去阅读它的源码确定是得知道它的用法, Koa的文档也很简单, 它一开始就提供了一个例子:web
const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
复制代码
这是启动最基本的的web服务, 这个跑起来没啥问题.express
一样, 文档也提供了做为Koa的核心卖点的中间件的基本用法:api
const Koa = require('koa');
const app = new Koa();
// 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`);
});
// logger
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
console.log(`${ctx.method} ${ctx.url} - ${ms}`);
});
// response
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
复制代码
上面代码可能跟咱们以前写的js代码常识不太符合了, 由于async/await会暂停做案现场, 相似同步. 也就是碰到await next
, 代码会跳出当前中间件, 执行下一个, 最终还回原路返回, 依次执行await next
下面的代码, 固然这只是一个表述而已, 实际就是一个递归返回Promise, 后面会提到.数组
好了. 咱们知道Koa怎么用了, 那对于这个框架咱们想知道什么呢. 先看一下源码的目录结构好了:promise
注意这个compose.js
是我为了方便修改源码拉过来的, 其实它是额外的一个包.app
application.js
做为入口文件确定是个构造函数 context.js
就是ctx
咯 request.js
response.js
框架
那咱们读源码总须要一个目标吧, 这篇文章里咱们假定目标就是弄懂Koa的中间件原理好了koa
好, 目标也有了, 下面正式进入源码阅读状态. 咱们以最简单的示例代码做为入口来切入Koa的执行过程:
const app = new Koa();
复制代码
上面咱们能够看到Koa是做为构造函数引用的, 那么咱们来看看入口文件Application.js
导出了个啥:
module.exports = class Application extends Emitter {
// ...
}
复制代码
毫无疑问是能够对应上的, 导出了一个类.
app.use(async ctx => {
ctx.body = 'Hello World';
});
复制代码
看上面的东西彷佛进入正题了, 咱们知道use就是引用了一个中间件, 那来看看use是个啥玩意:
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
deprecate('Support for generators will be removed in v3. ' +
'See the documentation for examples of how to convert old middleware ' +
'https://github.com/koajs/koa/blob/master/docs/migration.md');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}
复制代码
太长太臭, 精简一下
use(fn) {
this.middleware.push(fn);
return this;
}
复制代码
emm 这下就很清楚了, 就是维护了一个中间件数组middleware
, 到这里不要忘了咱们的目标: Koa的中间件原理, 既然找到这个中间件数组了, 咱们就来看看它是怎么被调用的吧. 全局搜一下, 咱们发现其实就一个方法里用到了middleware
:
callback() {
const fn = compose(this.middleware);
if (!this.listeners('error').length) this.on('error', this.onerror);
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
复制代码
上面的代码能够看到, 彷佛有一个compose
对middleware进行处理了, 咱们好像离真相愈来愈近了
function compose (middleware) {
/** * @param {Object} context * @return {Promise} * @api public */
return function (context, next) {
// last called middleware #
let index = -1
return dispatch(0)
function dispatch (i) {
index = i
let fn = middleware[i]
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1)
}))
} catch (err) {
return Promise.reject(err)
}
}
}
}
复制代码
compose.js
的代码很短, 可是仍是嫌长怎么办, 以前有文章提到的, 删除边界条件和异常处理:
function compose (middleware) {
/** * @param {Object} context * @return {Promise} * @api public */
return function (context, next) {
let index = -1
return dispatch(0)
function dispatch (i) {
index = i
let fn = middleware[i]
if (!fn) return Promise.resolve()
return Promise.resolve(fn(context, function next () {
return dispatch(i + 1)
}))
}
}
}
复制代码
这么一看就清晰多了, 不就是一个递归遍历middleware
嘛. 彷佛跟express有点像.
大胆假设嘛, 前面提到了, await 会暂停执行, 那await next
彷佛暂停的就是这里, 而后不断递归调用中间件, 而后递归中断了, 代码又从一个个的promise里退出来, 彷佛这样就很洋葱了.
emm 究竟是不是这样呢, 我也不知道. 比较还想再水一篇文章呢.