原文博客地址,欢迎学习交流: 点击预览
读了下Koa的源码,写的相当的精简,遇到处理中间件执行的模块koa-Compose,决定学习一下这个模块的源码。
阅读本文可以学到:
- Koa中间件的加载
- next参数的来源
- 中间件控制权执行顺序
先上一段使用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()方法,用来将中间件添加到队列中
- 中间件就是传给app.use()作为的参数的函数
- 使用app.use()将函数添加至队列之中后,当有请求时,会依次触发队列中的函数,也就是依次执行一个个中间件函数,执行顺序按照调用app.use()添加的顺序。
- 在每个中间件函数中,会执行next()函数,意思是把控制权交到下一个中间件(实际上是调用next函数后,会调用下一个中间件函数,后面解析源码会有说明),如果不调用next()函数,不能调用下一个中间件函数,那么队列执行也就终止了,在上面的代码中表现就是不能响应客户端的请求了。
app.use(async (ctx,next) => {
console.log('第二个中间件')
// next(); 注释之后,下一个中间件函数就不会执行
})
内部过程分析
- 内部利用app.use()添加到一个数组队列中:
// 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
- 使用koa-compose模块的compose方法,把这个中间件数组合并成一个大的中间件函数
const fn = compose(this.middleware);
具体参考这里Koa的源码https://github.com/koajs/koa/blob/master/lib/application.js#L126
- 在有请求后后会执行这个中间件函数fn,进而会把所有的中间件函数依次执行
这样片面的描述可能会不知所云,可以跳过不看,只是让诸位知道Koa执行中间件的过程
本篇主要是分析 koa-compose的源码,之后分析整个Koa的源码后会做详细说明
所以最主要的还是使用koa-compose模块来控制中间件的执行,那么来一探究竟这个模块如何进行工作的
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参数
// 这个匿名函数返回一个promise
return function (context, next) {
// last called middleware #
//初始下标为-1
let index = -1
return 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
// 方面之后做调用then
if (!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)
}
}
}
}
补充说明:
- 根据以上的源码分析得到,在一个中间件函数中不能调用两次next(),否则会抛出错误
function one(ctx,next){
console.log('第一个');
next();
next();
}
抛出错误:
next() called multiple times
- next()调用后返回的是一个Promise对象,可以调用then函数
function two(ctx,next){
console.log('第二个');
next().then(function(){
console.log('第二个调用then后')
});
}
- 中间件函数可以是async/await函数,在函数内部可以写任意的异步处理,处理得到结果后再进行下一个中间件函数。
创建一个文件问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