koa compose源码阅读

栏目: Node.js · 发布时间: 5年前

内容简介:众所周知,在函数式编程中,compose是将多个函数合并成一个函数(形如:我们创建koa应用如下:以上代码,可以使用

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

koa compose源码阅读

洋葱模型执行顺序

我们创建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 中间件实现对比解析


以上所述就是小编给大家介绍的《koa compose源码阅读》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

SCWCD Exam Study Kit Second Edition

SCWCD Exam Study Kit Second Edition

Hanumant Deshmukh、Jignesh Malavia、Matthew Scarpino / Manning Publications / 2005-05-20 / USD 49.95

Aimed at helping Java developers, Servlet/JSP developers, and J2EE developers pass the Sun Certified Web Component Developer Exam (SCWCD 310-081), this study guide covers all aspects of the Servlet an......一起来看看 《SCWCD Exam Study Kit Second Edition》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具