最近一年零零散散看了不少开源项目的源码, 多少也有点心得, 这里想通过这篇文章总结一下, 这里以Koa为例, 前段时间其实看过Koa的源码, 但是发现理解的有点偏差, 所以重新过一遍.
不得不说阅读tj的代码真的收获很大, 没啥奇技淫巧, 代码优雅, 设计极好. 注释什么的就更不用说了. 总之还是推荐把他的项目都过一遍(逃)
跑通例子
Koa作为一个web框架, 我们要去阅读它的源码肯定是得知道它的用法, Koa的文档也很简单, 它一开始就提供了一个例子:
const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
这是启动最基本的的web服务, 这个跑起来没啥问题.
同样, 文档也提供了作为Koa的核心卖点的中间件的基本用法:
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怎么用了, 那对于这个框架我们想知道什么呢. 先看一下源码的目录结构好了:
注意这个compose.js
是我为了方便修改源码拉过来的, 其实它是额外的一个包.
application.js
作为入口文件肯定是个构造函数context.js
就是ctx
咯request.js
response.js
那我们读源码总需要一个目标吧, 这篇文章里我们假定目标就是弄懂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 到底是不是这样呢, 我也不知道. 比较还想再水一篇文章呢.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。