[关闭]
@wy 2018-02-28T18:44:20.000000Z 字数 5861 阅读 448

在此处输入标题

nodejs


读了下Koa的源码,写的相当的精简,遇到处理中间件执行的模块koa-Compose,决定学习一下这个模块的源码。

阅读本文可以学到:

先上一段使用Koa启动服务的代码:
放在文件app.js

  1. const koa = require('koa'); // require引入koa模块
  2. const app = new koa(); // 创建对象
  3. app.use(async (ctx,next) => {
  4. console.log('第一个中间件')
  5. next();
  6. })
  7. app.use(async (ctx,next) => {
  8. console.log('第二个中间件')
  9. next();
  10. })
  11. app.use((ctx,next) => {
  12. console.log('第三个中间件')
  13. next();
  14. })
  15. app.use(ctx => {
  16. console.log('准备响应');
  17. ctx.body = 'hello'
  18. })
  19. app.listen(3000)

以上代码,可以使用node app.js启动,启动后可以在浏览器中访问http://localhost:3000/
访问后,会在启动的命令窗口中打印出如下值:

第一个中间件
第二个中间件
第三个中间件
准备响应

代码说明:

  1. app.use(async (ctx,next) => {
  2. console.log('第二个中间件')
  3. // next(); 注释之后,下一个中间件函数就不会执行
  4. })

内部过程分析

  1. // app.use()函数内部添加
  2. this.middleware.push(fn);
  3. // 最终this.middleware为:
  4. this.middleware = [fn,fn,fn...]

具体参考这里Koa的源码use函数:https://github.com/koajs/koa/blob/master/lib/application.js#L104

  1. const fn = compose(this.middleware);

具体参考这里Koa的源码https://github.com/koajs/koa/blob/master/lib/application.js#L126

这样片面的描述可能会不知所云,可以跳过不看,只是让诸位知道Koa执行中间件的过程
本篇主要是分析koa-compose的源码,之后分析整个Koa的源码后会做详细说明

所以最主要的还是使用koa-compose模块来控制中间件的执行,那么来一探究竟这个模块如何进行工作的

koa-compose

koa-compose模块可以将多个中间件函数合并成一个大的中间件函数,然后调用这个中间件函数就可以依次执行添加的中间件函数,执行一系列的任务。

源码地址:https://github.com/koajs/compose/blob/master/index.js

先从一段代码开始,创建一个compose.js的文件,写入如下代码:

  1. const compose = require('koa-compose');
  2. function one(ctx,next){
  3. console.log('第一个');
  4. next(); // 控制权交到下一个中间件(实际上是可以执行下一个函数),
  5. }
  6. function two(ctx,next){
  7. console.log('第二个');
  8. next();
  9. }
  10. function three(ctx,next){
  11. console.log('第三个');
  12. next();
  13. }
  14. // 传入中间件函数组成的数组队列,合并成一个中间件函数
  15. const middlewares = compose([one, two, three]);
  16. // 执行中间件函数,函数执行后返回的是Promise对象
  17. middlewares().then(function (){
  18. console.log('队列执行完毕');
  19. })

可以使用node compose.js运行此文件,命令行窗口打印出:

第一个
第二个
第三个
队列执行完毕

中间件这儿的重点,是compose函数。compose函数的源代码虽然很简洁,但要理解明白着实要下一番功夫。
以下为源码分析:

  1. 'use strict'
  2. /**
  3. * Expose compositor.
  4. */
  5. // 暴露compose函数
  6. module.exports = compose
  7. /**
  8. * Compose `middleware` returning
  9. * a fully valid middleware comprised
  10. * of all those which are passed.
  11. *
  12. * @param {Array} middleware
  13. * @return {Function}
  14. * @api public
  15. */
  16. // compose函数需要传入一个数组队列 [fn,fn,fn,fn]
  17. function compose (middleware) {
  18. // 如果传入的不是数组,则抛出错误
  19. if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  20. // 数组队列中有一项不为函数,则抛出错误
  21. for (const fn of middleware) {
  22. if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  23. }
  24. /**
  25. * @param {Object} context
  26. * @return {Promise}
  27. * @api public
  28. */
  29. // compose函数调用后,返回的是以下这个匿名函数
  30. // 匿名函数接收两个参数,第一个随便传入,根据使用场景决定
  31. // 第一次调用时候第二个参数next实际上是一个undefined,因为初次调用并不需要传入next参数
  32. // 这个匿名函数返回一个promise
  33. return function (context, next) {
  34. // last called middleware #
  35. //初始下标为-1
  36. let index = -1
  37. return dispatch(0)
  38. function dispatch (i) {
  39. // 如果传入i为负数且<=-1 返回一个Promise.reject携带着错误信息
  40. // 所以执行两次next会报出这个错误。将状态rejected,就是确保在一个中间件中next只调用一次
  41. if (i <= index) return Promise.reject(new Error('next() called multiple times'))
  42. // 执行一遍next之后,这个index值将改变
  43. index = i
  44. // 根据下标取出一个中间件函数
  45. let fn = middleware[i]
  46. // next在这个内部中是一个局部变量,值为undefined
  47. // 当i已经是数组的length了,说明中间件函数都执行结束,执行结束后把fn设置为undefined
  48. // 问题:本来middleware[i]如果i为length的话取到的值已经是undefined了,为什么要重新给fn设置为undefined呢?
  49. if (i === middleware.length) fn = next
  50. //如果中间件遍历到最后了。那么。此时return Promise.resolve()返回一个成功状态的promise
  51. // 方面之后做调用then
  52. if (!fn) return Promise.resolve()
  53. // try catch保证错误在Promise的情况下能够正常被捕获。
  54. // 调用后依然返回一个成功的状态的Promise对象
  55. // 用Promise包裹中间件,方便await调用
  56. // 调用中间件函数,传入context(根据场景不同可以传入不同的值,在KOa传入的是ctx)
  57. // 第二个参数是一个next函数,可在中间件函数中调用这个函数
  58. // 调用next函数后,递归调用dispatch函数,目的是执行下一个中间件函数
  59. // next函数在中间件函数调用后返回的是一个promise对象
  60. // 读到这里不得不佩服作者的高明之处。
  61. try {
  62. return Promise.resolve(fn(context, function next () {
  63. return dispatch(i + 1)
  64. }))
  65. } catch (err) {
  66. return Promise.reject(err)
  67. }
  68. }
  69. }
  70. }

补充说明:

  1. function one(ctx,next){
  2. console.log('第一个');
  3. next();
  4. next();
  5. }

抛出错误:

next() called multiple times

  1. function two(ctx,next){
  2. console.log('第二个');
  3. next().then(function(){
  4. console.log('第二个调用then后')
  5. });
  6. }

创建一个文件问test-async.js,写入以下代码:

  1. const compose = require('koa-compose');
  2. // 获取数据
  3. const getData = () => new Promise((resolve, reject) => {
  4. setTimeout(() => resolve('得到数据'), 2000);
  5. });
  6. async function one(ctx,next){
  7. console.log('第一个,等待两秒后再进行下一个中间件');
  8. // 模拟异步读取数据库数据
  9. await getData() // 等到获取数据后继续执行下一个中间件
  10. next()
  11. }
  12. function two(ctx,next){
  13. console.log('第二个');
  14. next()
  15. }
  16. function three(ctx,next){
  17. console.log('第三个');
  18. next();
  19. }
  20. const middlewares = compose([one, two, three]);
  21. middlewares().then(function (){
  22. console.log('队列执行完毕');
  23. })

可以使用node test-async.js运行此文件,命令行窗口打印出:

第一个,等待两秒后再进行下一个中间件
第二个
第三个
第二个调用then后
队列执行完毕

在以上打印输出过程中,执行第一个中间件后,在内部会有一个异步操作,使用了async/await后得到同步操作一样的体验,这步操作可能是读取数据库数据或者读取文件,读取数据后,调用next()执行下一个中间件。这里模拟式等待2秒后再执行下一个中间件。

更多参考了async/await:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/async_function
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/await

执行顺序

调用next后,执行的顺序会让人产生迷惑,创建文件为text-next.js,写入以下代码:

  1. const koa = require('koa');
  2. const app = new koa();
  3. app.use((ctx, next) => {
  4. console.log('第一个中间件函数')
  5. next();
  6. console.log('第一个中间件函数next之后');
  7. })
  8. app.use(async (ctx, next) => {
  9. console.log('第二个中间件函数')
  10. next();
  11. console.log('第二个中间件函数next之后');
  12. })
  13. app.use(ctx => {
  14. console.log('响应');
  15. ctx.body = 'hello'
  16. })
  17. app.listen(3000)

以上代码,可以使用node text-next.js启动,启动后可以在浏览器中访问http://localhost:3000/
访问后,会在启动的命令窗口中打印出如下值:

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

是不是对这个顺序产生了深深地疑问,为什么会这样呢?

当一个中间件调用 next() 则该函数暂停并将控制传递给定义的下一个中间件。当在下游没有更多的中间件执行后,堆栈将展开并且每个中间件恢复执行其上游行为。
过程是这样的:
- 先执行第一个中间件函数,打印出 '第一个中间件函数'
- 调用了next,不再继续向下执行
- 执行第二个中间件函数,打印出 '第二个中间件函数'
- 调用了next,不再继续向下执行
- 执行最后一个中间件函数,打印出 '响应'
- ...
- 最后一个中间函数执行后,上一个中间件函数收回控制权,继续执行,打印出 '第二个中间件函数next之后'
- 第二个中间件函数执行后,上一个中间件函数收回控制权,继续执行,打印出 '第一个中间件函数next之后'

借用一张图来直观的说明:
Alt text

具体看别人怎么理解next的顺序:https://segmentfault.com/q/1010000011033764

最近在看Koa的源码,以上属于个人理解,如有偏差欢迎指正学习,谢谢。

参考资料:https://koa.bootcss.com/
https://cnodejs.org/topic/58fd8ec7523b9d0956dad945

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注