koa-compose源码阅读

众所周知,在函数式编程中,compose是将多个函数合并成一个函数(形如: g() + h() => g(h())),koa-compose则是将 koa/koa-router 各个中间件合并执行,结合 next() 就形成了洋葱式模型。

image

洋葱模型执行顺序

我们创建koa应用如下:

const koa = require('koa');
const app = new koa();
app.use((ctx, next) => {
 console.log('第一个中间件函数')
 await next();
 console.log('第一个中间件函数next之后');
})
app.use(async (ctx, next) => {
 console.log('第二个中间件函数')
 await next();
 console.log('第二个中间件函数next之后');
})
app.use(ctx => {
 console.log('响应');
 ctx.body = 'hello'
})
​
app.listen(3000)

以上代码,可以使用node text-next.js启动,启动后可以在浏览器中访问http://localhost:3000/

访问后,会在启动的命令窗口中打印出如下值:

第一个中间件函数
第二个中间件函数
响应
第二个中间件函数next之后
第一个中间件函数next之后

注意:在使用app.use将给定的中间件添加到应用程序时,中间件(其实就是一个函数)接收两个参数:ctx和next。其中next也是一个函数。

koa-compose源码

再接着深入koa-compose源码之前,我们先来看一下,koa源代码中是怎么调用compose的。详细参考上一篇文章

listen(...args) {
   debug('listen');
   const server = http.createServer(this.callback());
   return server.listen(...args);
}
​
callback() {
   // 这里调用的compose的函数,返回值是fn
   const fn = compose(this.middleware);
​
   if (!this.listenerCount('error')) this.on('error', this.onerror);
​
   const handleRequest = (req, res) => {
   const ctx = this.createContext(req, res); // 创建ctx对象
   return this.handleRequest(ctx, fn);  // 将fn传递给了this.handleRequest
 };
​
 return handleRequest;
}
​
handleRequest(ctx, fnMiddleware) {
   const res = ctx.res;
   res.statusCode = 404;
   onFinished(res, onerror);
   // 在这里,看到以下fnMiddleware().then().catch()写法.
   // 我们大胆猜测compose函数的返回值是一个function。而且该function的返回值是一个promise对象。
   // 待下文源码验证。
   return fnMiddleware(ctx)
   .then(() => respond(ctx))
   .catch(err => ctx.onerror(err));
}

callback函数是在app.listen时执行的,也就是在app.listen时利用 Node 原生的http 模块建立http server,并在创建server的时候,处理中间件逻辑。

好,现在我们已经知道了koa是怎么调用compose的,接下来,看koa-compose源代码。koa-compose 的代码只有不够50行,细读确实是一段很精妙的代码,而实际核心代码则是这一段:

module.exports = compose
​
function compose (middleware) {
 // 传入的 middleware 参数必须是数组
 if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
 // middleware 数组的元素必须是函数
 for (const fn of middleware) {
   if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
 }
​
 // 返回一个函数闭包, 保持对 middleware 的引用。
 // 这里也验证了上文的猜测:compose函数的返回值是一个function.
 // 而且看下文可知,该函数的返回值是promise对象。进一步验证了上文的猜测。
 return function (context, next) {
   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, function next () {
         return dispatch(i + 1)
       }))
     } catch (err) {
       return Promise.reject(err)
     }
   }
  }
}

虽然短,但是之中使用了4层 return,初看会比较绕,我们只看第3,4层 return,这是返回实际的执行代码链。

return Promise.resolve(fn(context, function next () {
   return dispatch(i + 1)
}))

fn = middleware[i]也就是某一个中间件,很显然上述代码遍历中间件数组middleware,依次拿到中间件fn,并执行:

fn(context, function next () {
   return dispatch(i + 1)
})

这里可以看到传递给中间件的两个参数:context和next函数

前文提到过:在使用app.use将给定的中间件添加到应用程序时,中间件(其实就是一个函数)接收两个参数:ctx和next。其中next也是一个函数。

看到这里是不是明白了,在注册中间件的时候为什么要有两个参数了呐!!!

接下来,我们继续研究洋葱模型到底是怎么回事儿。 比如前文例子中的第一个中间件:

app.use((ctx, next) => {
 console.log('第一个中间件函数')
 await next();
 console.log('第一个中间件函数next之后');
})
  • 第一次,此时第一个中间件被调用,dispatch(0),展开:
Promise.resolve(((ctx, next) => {
   console.log('第一个中间件函数')
   await next();
   console.log('第一个中间件函数next之后');
})(context, function next () {
   return dispatch(i + 1)
})));

首先执行console.log('第一个中间件函数'),打出来log没毛病。

接下来注意了老铁!注意了老铁!注意了老铁!重要的事情说三遍。在执行到await next();的时候,return dispatch(i + 1)

瞅一眼上文中的dispatch函数,你就能知道,这是递归到了第二个中间件啊,也就是说压根就没执行第二个log即:console.log('第一个中间件函数next之后');,就跑到了第二个中间件。

  • 第二次,此时第二个中间件被调用,dispatch(1),展开:
Promise.resolve((ctx, next) => Promise.resolve((ctx, next) => s{
 console.log('第一个中间件函数')
 await Promise.resolve(((ctx, next) => {
   console.log('第二个中间件函数')
   await next();
   console.log('第二个中间件函数next之后');
 })(context, function next () {
   return dispatch(i + 1)
 })));
 console.log('第一个中间件函数next之后');
});

接下来的事情,想必你们都猜到了,在第二个中间件执行到await next();时,同样会轮转到第三个中间件,以此类推,直到最后一个中间件。

总结

中间件模型非常好用并且简洁, 甚至在 koa 框架上大放异彩, 但是也有自身的缺陷, 也就是一旦中间件数组过于庞大, 性能会有所下降, 因此我们需要结合自身的情况与业务场景作出最合适的选择.

参考文章:

koa 源码解析
koa-用到的delegates NPM包详解
redux, koa, express 中间件实现对比解析

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,377评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,390评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,967评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,344评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,441评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,492评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,497评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,274评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,732评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,008评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,184评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,837评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,520评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,156评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,407评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,056评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,074评论 2 352

推荐阅读更多精彩内容