孤阴不生,独阳不长,负阴而抱阳,充气以为和。
前言
本文主要介绍的是koa框架是如何优雅的处理中间件。
基于koa v2.13.1、koa-compose v4.2.0
使用示例
先看一段我们很熟悉的node项目中入口文件代码:
const Koa = require('koa');
const app = new Koa();
// logger
app.use(async (ctx, next) => {
await next();
const rt = ctx.response.get('X-Response-Time');
console.log(`${ctx.method} ${ctx.url} - ${rt}`);
});
// x-response-time
app.use(async (ctx, next) => {
const start = Date.now();
await next();
const ms = Date.now() - start;
ctx.set('X-Response-Time', `${ms}ms`);
});
// response
app.use(async ctx => {
ctx.body = 'Hello World';
});
app.listen(3000);
复制代码
上面的代码很简单,主要就是三个步骤:
- 创建一个koa对象的实例app
- 再调用app的use方法。以app.use(XX).use(XX)插入中间件函数。
- 最后调用启动方法,并监听3000端口
中间件方法的保存,与触发
1. 保存中间件
首先我们来看看是如何以app.use(fn).use(fn).use(fn)
保存中间件的
//lib/application.js
module.exports = class Application extends Emitter {
use(fn) {
//校验fn格式
this.middleware.push(fn);
//返回this,可以一直app.use()重复调用
return this;
}
};
复制代码
app.use(fn)
做的事情很简单:就是在http
服务启动前,将我们项目预设的中间件保存在middleware
数组中,等待http
服务发起回调触发这些中间件。
那下面我们看看一个http请求过来,保存在middleware
中的中间件是如何触发的。
2. 触发中间件
创建一个http
的服务server
,并以callback()
作为回调。
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
复制代码
在callbakc()
方法中,以compose
来组织中间件,放返回回调方法
callback() {
//以某种方式组织这些中间件
const fn = compose(this.middleware);
if (!this.listenerCount('error')) this.on('error', this.onerror);
//获取到http请求中的req,res,保存在ctx上下文中
const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};
//返回经过compose处理后的中间件方法
return handleRequest;
}
复制代码
在接受的http
请求时,作为回调函数执行经过compose处理过的中间件方法,修改上下文ctx
。
handleRequest(ctx, fnMiddleware) {
const res = ctx.res;
const onerror = err => ctx.onerror(err);
const handleResponse = () => respond(ctx);
//将上下文作为入参传入中间件。
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
}
复制代码
compose函数的实现
下面就来介绍我们的核心方法。
就是因为这个compose方法,我们才得以在中间件中以await next()
的形式将控制权转交给下一个中间件,并在最后一个中间件将控制权依次向上传递。
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!')
}
return function (context, next) {
// last called middleware #
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, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
复制代码
compose的源码来源koa-compose
,代码也比较简单。我们来简单分析一下:
- 做一个入参(中间件)的类型校验
- 并马上返回一个译名函数。做一个柯里化,分步传入参数。对应源码:
//compose化中间件
const fn = compose(this.middleware);
...
//返回请求的回调函数-即compose之后的中间件函数
return fnMiddleware(ctx).then(handleResponse).catch(onerror);
复制代码
- 在佚名函数中马上返回了一个函数调用。做了一个尾递归的优化,防止栈溢出。一句话解释:在A函数内部返回B函数的执行结果,B函数的内存空间就能马上释放
- 而最关键的就是
dispatch
函数,主要逻辑:- 先做了一个边界情况
if (!fn) return Promise.resolve()
,没有下一个中间件了,就Promise.resolve()
,回溯到上一个中间件的await next()
之后的逻辑; - 然后就是返回第一个中间件的调用结果,这样就是一个尾递归优化。
- 关键的是
fn
的第二个参数,是下一个中间件函数,能够让上一个中间件优雅的调用。
- 先做了一个边界情况
小结:
- js控制权从第一个中间件开始,依次往下传递。向下传递的关键在于执行
await next()
,手动触发执行。 - 而
await next()
方法也为后面的控制权向上回溯做了铺垫。每一个await next()
都嵌套了下一个中间件的方法,直到最后一个没有await next()
的中间件执行完毕 - 最后一个中间件执行完毕,开始向上回溯,执行上一个中间件的
await next()
后的方法
总结
compose
函数利用了尾递归进行了内存栈的优化- 将下一个中间件的作为当前执行中间件的入参,来手动控制进入下一个中间件的时机。
- 利用
async/await
来处理异步的中间件来向上回溯
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END