@wy
2018-02-28T10:44:20.000000Z
字数 5861
阅读 575
nodejs
读了下Koa的源码,写的相当的精简,遇到处理中间件执行的模块koa-Compose,决定学习一下这个模块的源码。
阅读本文可以学到:
先上一段使用Koa启动服务的代码:
放在文件app.js中
const koa = require('koa'); // require引入koa模块const app = new koa(); // 创建对象app.use(async (ctx,next) => {console.log('第一个中间件')next();})app.use(async (ctx,next) => {console.log('第二个中间件')next();})app.use((ctx,next) => {console.log('第三个中间件')next();})app.use(ctx => {console.log('准备响应');ctx.body = 'hello'})app.listen(3000)
以上代码,可以使用node app.js启动,启动后可以在浏览器中访问http://localhost:3000/
访问后,会在启动的命令窗口中打印出如下值:
第一个中间件
第二个中间件
第三个中间件
准备响应
代码说明:
app.use(async (ctx,next) => {console.log('第二个中间件')// next(); 注释之后,下一个中间件函数就不会执行})
// app.use()函数内部添加this.middleware.push(fn);// 最终this.middleware为:this.middleware = [fn,fn,fn...]
具体参考这里Koa的源码use函数:https://github.com/koajs/koa/blob/master/lib/application.js#L104
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模块可以将多个中间件函数合并成一个大的中间件函数,然后调用这个中间件函数就可以依次执行添加的中间件函数,执行一系列的任务。
源码地址:https://github.com/koajs/compose/blob/master/index.js
先从一段代码开始,创建一个compose.js的文件,写入如下代码:
const compose = require('koa-compose');function one(ctx,next){console.log('第一个');next(); // 控制权交到下一个中间件(实际上是可以执行下一个函数),}function two(ctx,next){console.log('第二个');next();}function three(ctx,next){console.log('第三个');next();}// 传入中间件函数组成的数组队列,合并成一个中间件函数const middlewares = compose([one, two, three]);// 执行中间件函数,函数执行后返回的是Promise对象middlewares().then(function (){console.log('队列执行完毕');})
可以使用node compose.js运行此文件,命令行窗口打印出:
第一个
第二个
第三个
队列执行完毕
中间件这儿的重点,是compose函数。compose函数的源代码虽然很简洁,但要理解明白着实要下一番功夫。
以下为源码分析:
'use strict'/*** Expose compositor.*/// 暴露compose函数module.exports = compose/*** Compose `middleware` returning* a fully valid middleware comprised* of all those which are passed.** @param {Array} middleware* @return {Function}* @api public*/// compose函数需要传入一个数组队列 [fn,fn,fn,fn]function compose (middleware) {// 如果传入的不是数组,则抛出错误if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')// 数组队列中有一项不为函数,则抛出错误for (const fn of middleware) {if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')}/*** @param {Object} context* @return {Promise}* @api public*/// compose函数调用后,返回的是以下这个匿名函数// 匿名函数接收两个参数,第一个随便传入,根据使用场景决定// 第一次调用时候第二个参数next实际上是一个undefined,因为初次调用并不需要传入next参数// 这个匿名函数返回一个promisereturn function (context, next) {// last called middleware #//初始下标为-1let index = -1return dispatch(0)function dispatch (i) {// 如果传入i为负数且<=-1 返回一个Promise.reject携带着错误信息// 所以执行两次next会报出这个错误。将状态rejected,就是确保在一个中间件中next只调用一次if (i <= index) return Promise.reject(new Error('next() called multiple times'))// 执行一遍next之后,这个index值将改变index = i// 根据下标取出一个中间件函数let fn = middleware[i]// next在这个内部中是一个局部变量,值为undefined// 当i已经是数组的length了,说明中间件函数都执行结束,执行结束后把fn设置为undefined// 问题:本来middleware[i]如果i为length的话取到的值已经是undefined了,为什么要重新给fn设置为undefined呢?if (i === middleware.length) fn = next//如果中间件遍历到最后了。那么。此时return Promise.resolve()返回一个成功状态的promise// 方面之后做调用thenif (!fn) return Promise.resolve()// try catch保证错误在Promise的情况下能够正常被捕获。// 调用后依然返回一个成功的状态的Promise对象// 用Promise包裹中间件,方便await调用// 调用中间件函数,传入context(根据场景不同可以传入不同的值,在KOa传入的是ctx)// 第二个参数是一个next函数,可在中间件函数中调用这个函数// 调用next函数后,递归调用dispatch函数,目的是执行下一个中间件函数// next函数在中间件函数调用后返回的是一个promise对象// 读到这里不得不佩服作者的高明之处。try {return Promise.resolve(fn(context, function next () {return dispatch(i + 1)}))} catch (err) {return Promise.reject(err)}}}}
补充说明:
function one(ctx,next){console.log('第一个');next();next();}
抛出错误:
next() called multiple times
function two(ctx,next){console.log('第二个');next().then(function(){console.log('第二个调用then后')});}
创建一个文件问test-async.js,写入以下代码:
const compose = require('koa-compose');// 获取数据const getData = () => new Promise((resolve, reject) => {setTimeout(() => resolve('得到数据'), 2000);});async function one(ctx,next){console.log('第一个,等待两秒后再进行下一个中间件');// 模拟异步读取数据库数据await getData() // 等到获取数据后继续执行下一个中间件next()}function two(ctx,next){console.log('第二个');next()}function three(ctx,next){console.log('第三个');next();}const middlewares = compose([one, two, three]);middlewares().then(function (){console.log('队列执行完毕');})
可以使用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,写入以下代码:
const koa = require('koa');const app = new koa();app.use((ctx, next) => {console.log('第一个中间件函数')next();console.log('第一个中间件函数next之后');})app.use(async (ctx, next) => {console.log('第二个中间件函数')next();console.log('第二个中间件函数next之后');})app.use(ctx => {console.log('响应');ctx.body = 'hello'})app.listen(3000)
以上代码,可以使用node text-next.js启动,启动后可以在浏览器中访问http://localhost:3000/
访问后,会在启动的命令窗口中打印出如下值:
第一个中间件函数
第二个中间件函数
响应
第二个中间件函数next之后
第一个中间件函数next之后
是不是对这个顺序产生了深深地疑问,为什么会这样呢?
当一个中间件调用 next() 则该函数暂停并将控制传递给定义的下一个中间件。当在下游没有更多的中间件执行后,堆栈将展开并且每个中间件恢复执行其上游行为。
过程是这样的:
- 先执行第一个中间件函数,打印出 '第一个中间件函数'
- 调用了next,不再继续向下执行
- 执行第二个中间件函数,打印出 '第二个中间件函数'
- 调用了next,不再继续向下执行
- 执行最后一个中间件函数,打印出 '响应'
- ...
- 最后一个中间函数执行后,上一个中间件函数收回控制权,继续执行,打印出 '第二个中间件函数next之后'
- 第二个中间件函数执行后,上一个中间件函数收回控制权,继续执行,打印出 '第一个中间件函数next之后'
借用一张图来直观的说明:

具体看别人怎么理解next的顺序:https://segmentfault.com/q/1010000011033764
最近在看Koa的源码,以上属于个人理解,如有偏差欢迎指正学习,谢谢。
参考资料:https://koa.bootcss.com/
https://cnodejs.org/topic/58fd8ec7523b9d0956dad945