Koa 是一个小而美的node server框架,其优秀的中间件机制,为整个框架提供了强大的横向扩展能力;
Koa 核心源码只有四个文件,github源码地址,外加一些依赖包,但如果仅仅只是去理解 Koa 最核心的部分源码,理解中间件运行机制,其实并不需要全部看完核心源码的四个文件和这些依赖包,下面就来看看如何通过50行代码体现 Koa 最核心 http server 和中间件机制;
// application.js
const http = require('http');
const context = {};
function compose(middlewares) {
return function(context, next) {
let index = -1;
function dispatch(i) {
if (i <= index) return Promise.reject(new Error('next() called multiple times'));
index = i;
let fn = middlewares[i];
if (i === middlewares.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)
}
}
return dispatch(0)
}
}
module.exports = class Application {
constructor(options) {
this.middleware = [];
this.context = Object.create(context);
}
listen(...args) {
const server = http.createServer(this.callback());
return server.listen(...args);
}
callback () {
const fn = compose(this.middleware);
return (req, res) => {
this.context.req = req;
this.context.res = res;
return fn(this.context).then(() => {
respond(this.context);
}).catch((error) => {
console.error(error);
});
};
}
use(fn) {
this.middleware.push(fn);
return this;
}
}
function respond(ctx) {
ctx.res.end(ctx.body);
}
利用上面50行源码启动一个监听3000端口的 http server 服务;
const Koa = require('./application');
const app = new Koa();
app.listen(3000);
裁减掉额外的对 context、 request 和 response 封装的语法糖,简化 respond 方法对返回数据的处理,再加上 koa-compose(koa-compose源码)对中间件的处理,以上50行代码,就可以了解到 Koa 最核心的两个点:
一、http server功能
Koa 也是依赖 node 原生的 http 模块来实现 http server 的能力,关于 node 的 http 模块,这里不再深入介绍,先挖个坑,后面再单独写一篇文章,理解下 http 模块及相关的 net 模块;
可以看到,原生 http 模块可以通过几行代码就启动一个监听在 8000 端口的http服务,createServer 的第一个参数是一个回掉函数,这个回掉函数有两个参数,一个是请求对象,一个是响应对象,可以根据请求对象的内容来决定响应数据的内容;
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('node server on port 8000');
});
server.listen(8000);
在 Koa 中,createServer 回掉函数中的 req 和 res 会被保存到 ctx 对象上,伴随整个处理请求的生命周期,Koa 源码中的 request.js 和 response.js 就是对这两个对象添加了大量便捷获取数据和设置数据的方法,如获取请求的方法、请求的路径、设置返回数据体、设置返回状态码等操作,在本文的分析中,省略了这两部分;
二、中间件机制 middleware
比起 http server,中间件机制是 Koa 更具特色的特点,也是 Koa 框架的核心,在 Koa 生成的 app 对象上添加一个中间件并不复杂,如下代码,便可以添加一个中间件:
app.use((ctx, next) => {
console.log('this is a koa middleware');
next();
});
koa appliation.js 中提供了一个 use 方法,该方法只有一个参数,而且这个参数必须是一个函数,use会将这个函数 push 到 middleware 的数组中,这个数组就是保存整个 Koa 实例中间件的数组;
那么在中间件数组中保存这些中间件后,中间件是如何执行的呢?koa 源码中依赖的是一个 koa-compose 的中间件,将数组中所有中间件串联起来,每一个中间件函数执行的时候,会有两个参数:ctx:koa 的上下文对象,里面包含 http 请求原生req对象,http 请求原生res对象,以及koa封装的快捷方法;next:其实是下一个中间件的引用,当前中间件执行到 next 的时候,会进入下一个中间件函数中,以此类推,直到最后一个中间件执行完成,再依次向上执行上一个中间件 next 后的函数代码,执行完后再向上一个中间件,直到最后执行完第一个中间件 next 后的代码后,返回这次请求的执行结果;
下图是 koa 作者提供的一张中间件执行顺序示意图:
抽象出执行流程如下图:
中间件的规则
上面介绍了 koa 是如何依次执行所有中间件的,通过中间件机制,框架使用者也可以根据实际业务需求,开发满足业务需求的中间件;在向 Koa 中添加一个中间件的时候,需要满足那些要求,已经中间件执行有哪些规则呢?通过下面的问题,来回答这两个问题。
Q1.中间件需要满足什么样的规则?
A1:koa中对中间件只有一个条件:必须是一个函数,同步函数和异步函数都可以作为中间件,但如果是 generator 函数的话,会有一个警告:koa 3.x 不会再支持 generators 类型中间件;所以最好是如下形式的函数:
// 含有异步逻辑的中间件
app.use(async (context, next) => {
console.log('middleware 1: something before next');
await db.query();
await next();
console.log('middleware 1: something after next');
});
// 只有同步逻辑的中间件
app.use((context, next) => {
console.log('middleware 2: something before next');
next();
console.log('middleware 2: something after next');
});
// 最后一个中间件,不需要再调用 next,但调用后不会报错,compose 函数监测到没有下一个中间件后,会执行一个空的 promise 后返回
app.use((context, next) => {
context.body = 'response body';
console.log("middleware 3: it's all~");
});
Q2.中间件的执行顺序是什么?
A2: 中间件的执行顺序取决于app.use()的调用时机,即中间件被 push到 middleware 中的顺序;
- 最早被
app.use()/push到中间件数组的中间件,next之前的代码最早执行,next之后的代码最晚执行; - 最后被
app.use()/push到中间件数组的中间件,next之前的代码最晚执行,但早于前面的中间件next后的代码执行,next之后的代码也早于前面的中间件next后的代码执行,但晚于当前中间件next之前的代码;
Q3.中间件函数的第二个参数 next 一定要被调用吗?可以多次调用吗?
A3: 理论上 next 参数是对下一个中间件的引用,可以不被调用,但如果不主动调用 next 函数,后面的中间件将无法执行,当前请求执行完当前中间件后,将向上执行之前中间件 next 的后的代码,然后返回;
next 函数在一个中间件中只能被调用一次,若超过一次,app 会抛错:new Error('next() called multiple times');
Q4. 中间件有两个参数:context 和 next,可以向中间件函数添加别的参数吗?
A4:中间件只有这两个参数,无法添加额外参数;如果上一个中间件处理后的数据,需要在后面中间件中使用,可以将数据挂到 context 对象下面,context 贯穿于整个请求的生命周期;
中间件实践总结
- 即使一个中间件中只包含同步逻辑,仍然会被
koa-compose包装成一个promise对象,实际开发中很容易疏忽下一个中间件是同步执行还是异步执行,所以最好将所有中间件变为异步函数,在调用next()的时候,加上await关键字,如下:
app.use(async (context, next) => {
console.log(111);
await next();
console.log(222);
});
同步和异步,以及 async/await 函数、promise 对象的关系,可以在这里查看:Promise对象、异步操作和Async函数;
next不可以被多次调用;如果不需要调用后面的中间件,可以不调用next,如用户鉴权失败或未登录,直接返回返回失败,则可以在一个中间件中判断后,直接返回,而不用调用next下一个中间件;
查看完整代码: codesandbox.io/s/little-ni…