无处不在的next函数
Node.js中的connect库、生成器自动执行库co、redux、Koa中都有各自的一个流程控制函数:next,这里我们通过koa的中间件源码来聊一下next函数是如何进行流程控制的。
Koa的核心源码实现极其简洁,很适合用于学习,他的主目录lib下只有四个模块:
koa使用方式
koa的使用也很简单:
// 这里导入的koa其实就是application.js导出的一个class
const Koa = require('koa');
// 实例化Koa application对象
const app = new Koa();
// 使用中间件
app.use(async ctx => {
ctx.body = 'Hello World';
});
// 监听
app.listen(3000);
koa实现原理
首先,在new Koa时,会执行构造函数,初始化一个数组存储中间件:
constructor(options) {
super();
...
this.middleware = [];
...
}
同时,use方法也很简单,就是将传入的中间件函数push到middleware中,并且返回自身以便实现链式调用:
use(fn) {
// 判断中间件是否为function
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
...
this.middleware.push(fn);
return this;
}
最后调用listen方法监听对应端口的请求:
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
这里其实就是调用了node.js提供的net内置库创建一个httpServer:
// http.js
function createServer(opts, requestListener) {
return new Server(opts, requestListener);
}
Server函数中对opts做了类型判断,如果传入的第一个参数时function类型,则会被视作为回调:
function Server(options, requestListener) {
...
if (typeof options === 'function') {
requestListener = options;
...
}
...
}
而createServer中传入的是this.callback(),它返回一个函数。所以当服务监听到请求时,就会调用callback方法执行返回的这个函数,接下来就重点看看该方法的实现:
callback() {
const fn = compose(this.middleware);
...
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
return handleRequest;
}
其中做了两点重要的工作,一个是调用compose方法对中间件数组进行处理返回一个方法fn,另一个就是对类成员函数handleRequest函数的包装,传入了上下文ctx,ctx的作用很明显,内部封装了请求中用到的各种上下文变量,便于开发者获取想要的参数,而fn函数则是在handleRequest进行了调用:
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
res.statusCode = 404;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
onFinished(res, onerror);
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
虽然不知道compose做了什么,但是从handleRequest的实现来看,我们可以大致看出:这个方法执行了中间件函数对请求做处理,并且处理完成之后返回了一个Promise,且调用handleResponse方法,并最终调用respond对请求做了处理,而respond方法中对请求做了最后的返回:
function respond(ctx) {
...
res.end(body);
}
看到这里,就可以发现,koa中中间件的关键实现逻辑其实就在compose函数中,理解了compose函数,就理解了koa中间件洋葱模型的实现逻辑。
理解compose
compose方法是一个单独的库,其中只有一个文件,且只导出了一个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!')
}
return function (context, next) {
// 定义分发函数
function dispatch (i) {
// 避免重复调用
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
// 获取第i项中间件函数
let fn = middleware[i]
if (i === middleware.length) fn = next
// fn为空则直接返回一个空Promise
if (!fn) return Promise.resolve()
try {
// 最核心的一行代码:返回一个Promise.resolve,resolve中传入fn函数(也就是当前循环取到的中间件函 数)的调用,同时调用fn时的第二个参数为dispatch函数,会调用下一个中间件函数,依次递归
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
// 通过try catch返回Promise
return Promise.reject(err)
}
}
let index = -1
// 启动dispatch,i传入0,也就是会调用middleware的中第一个函数
return dispatch(0)
}
}
相关的释义均详细地在代码中进行了注释。
其中最关键的代码就是
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
根据注释
还有一点就是,在使用中间件时,一般需要在处理完自己的逻辑后调用一下next函数,我们常说koa实现了洋葱模型:
// 洋葱模型示例,
app.use(async (ctx, next) => {
console.log('before next middleware invoke')
await next()
console.log('after next middleware invoke')
})
这个next是什么呢?打个断点很容易看出,其实就是bound dispatch,也就是dispatch.bind(null, i + 1)
由此可见,通过next,koa完成了中间件依次执行,并且通过支持async/await,实现了一个类似洋葱的模型。
compose的意义
在很多业务场景中,我们都可以使用compose来优化代码
比如,假设有一个业务流程,有多个步骤需要用户进行操作,而不同类型用户可能需要完成的步骤不一样,那可以维护一个不同类型用户需要执行的函数对象:
{
"normal": [fn1, fn2, fn3, fn4],
"vip": [fn1, fn2, fn4]
}
这样的写法可以避免大量的if/else逻辑,提供了很好的可读性和扩展性。