Koa中间件原理

2,483 阅读3分钟

背景

Koa官方使用方法

const Koa = require('koa');
var Router = require('koa-router');
var router = new Router();
const app = new Koa();
app.use(async (ctx, next) => {
    await next();
    ctx.response.type = 'text/html';
    ctx.response.body = '<h1>Hello, koa2!</h1>';
});
//路由中间件
app.use(router.routes());
app.listen(3000);
console.log('app started at port 3000...');

分析Koa.use()做了什么

Koa本质上是对node原生http模块的封装,并且引入了中间件机制,通过示例代码我们也知道,中间件就是一个个方法,通过这些方法使得整个web服务模块可以各司其职,有助于开发与调试。在整个http请求中,rquest与response需要经过一系列的中间过程处理然后返回给客户端使用。我们猜想在Koa的内部有这样的一个数组和一个use方法

let middleWare = []
function use(f){
    middleWare.push(f)
}

在使用过一系列中间件之后,我们会得到这样一个middleWare

let middleWare = [f1,f2,f3,f4,f5,...]

举个列子说明

async function f1(ctx,next) {
    console.log('执行到f1')
    next()
}
async function f2(ctx,next) {
    console.log('执行到f2')
    next()
}
async function f3(ctx,next) {
    console.log('执行到f3')
    next()
}
async function f4(ctx,next) {
    console.log('执行到f4')
    next()
}
async function f5(ctx,next) {
    console.log('执行到f5')
    next()
}

以上是我们已经使用的5个中间件 我们怎么做到让这5个中间件以此执行呢?

看下面的代码

let next1 = async function (ctx) {
    await f1(ctx,next2)
}
let next2 = async function (ctx) {
    await f2(ctx,next3)
}
let next3 = async function (ctx) {
    await f3(ctx,next4)
}
let next4 = async function (ctx) {
    await f4(ctx,next5)
}
let next5 = async function (ctx) {
    await f5()
}

next1(ctx)     //  next()  ===  f1(f2(f3(f4(f5)))) ---> true
// ---> 执行到f1,执行到f2,执行到f3,执行到f4,执行到f5

由此看出,每个中间件的参数,是对于下一个中间中间件的引用函数。于是我们抽象一个方法出来来生成这些next,然后最后调用最后一个next就可以达到依次调用的效果

function createNext(middleWare,next){
    return async function(){
        await middleWare(next)
    }
}

我们发现,createNext函数返回值又会成为下一个中间件的参数,next5被next4调用,next4又被next3调用......依次类推,很自然的就想到了数组的reduce方法 因此:

let composeMiddleWare = [f1,f2,f3,f4,f5].reduceRight((pre, cur, i, arr) => {
    return createNext(cur, pre)
}, () => { })

composeMiddleWare()   //   ---> 执行到f1,执行到f2,执行到f3,执行到f4,执行到f5

Koa源码猜想

从上面的分析可得,koa源码中肯定有类似的实现

let middleWare = []
function use(f){
    middleWare.push(f)
}
function createNext(middleWare,next,ctx){
    return async function(){
        await middleWare(ctx,next)
    }
}
//组合中间件方法
let composeMiddleWare = (ctx) =>middleWare.reduceRight((pre, cur, i, arr) => {
    return createNext(cur, pre,ctx)
}, () => { Promise.resolve() })

分析Koa中http模块原生api【createServer】的回调函数callback执行的时候携带了request与response对象,koa对其进行了封装生成了一个ctx对象,挂载了request,response上的部分属性,我们来猜猜callback里面做了什么?

http.createServer((req,res)=>callback(req,res))
function callback(req,res){
    let composeMiddleWare = composeMiddleWare();//上面的composeMiddleWare
    let ctx = {req,res}  //有兴趣去看看源码,这里挂载全部属性
    composeMiddleWare(ctx) //依次执行左右的中间件,并携带ctx
}

结语

Koa本身就是一个及其精简的框架,中间件的引入使它极其容易扩展,因此很受欢迎,也有很多大厂基于KOa的思想封装了自己的框架比如egg.js等等。先分析到这儿,下次写一篇Koa的简单实现。