最近一年零零散散看了很多开源项目的源码, 多少也有点心得, 这里想经过这篇文章总结一下, 这里以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 究竟是不是这样呢, 我也不知道. 比较还想再水一篇文章呢.