探索Koa的中间件原理

141 阅读2分钟

1. 中间件执行顺序

问题1:

const Koa = require("koa");
const app = new Koa();

app.use(async (ctx, next) => {
  console.log(1);
  await next();
  console.log(2);
});
app.use(async (ctx, next) => {
  console.log(3);
  //   await sleep();
  await next();
  console.log(4);
});
app.use(async (ctx, next) => {
  console.log(5);
  await next();
  console.log(6);
});

app.listen(3000, function () {
  console.log("开启成功");
});

// 打印结果 1、3、5、6、4、3

问题2:

const Koa = require("koa");
const app = new Koa();

app.use((ctx, next) => {
  console.log(1);
  next();
  console.log(2);
});
app.use((ctx, next) => {
  console.log(3);
  next();
  console.log(4);
});
app.use((ctx, next) => {
  console.log(5);
  next();
  console.log(6);
});

app.listen(3000, function () {
  console.log("开启成功");
});
// 打印结果 1、3、5、6、4、3

问题3:

const Koa = require("koa");
const app = new Koa();

const sleep = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      console.log("sleep");
      resolve();
    }, 1000);
  });
};

app.use((ctx, next) => {
  console.log(1);
  next();
  console.log(2);
});
app.use((ctx, next) => {
  console.log(3);
  await sleep();
  next();
  console.log(4);
});
app.use((ctx, next) => {
  console.log(5);
  next();
  console.log(6);
});

app.listen(3000, function () {
  console.log("开启成功");
});
// 打印结果 1、3、2、sleep、5、6、4

根据执行结果,我可以把整个执行模型解释为洋葱模型

洋葱模型.jpg

洋葱模型1.jpg

2. koakoa-compose

中间件注入

koa的中间件主要通过use方法进行注入

class Application {
    constructor (){
         this.middleware = []
    }
    use (fn) {
        if (typeof fn !== 'function') throw new TypeError('middleware must be a function!')
        debug('use %s', fn._name || fn.name || '-')
        // 将中间件方法放到middleware上,并返回this方便链式调用
        this.middleware.push(fn)
        return this
    }
}

中间件执行

koa中 执行过程如下

const compose = require('koa-compose')
// listen
class Application {
    constructor (options) {
        this.middleware = []
        this.compose = options.compose || compose
    }
     // 执行listen
    listen (...args) {
        const server = http.createServer(this.callback())
        return server.listen(...args)
    }
    callback () {
        // 生成中间件
        const fn = this.compose(this.middleware)
        // 执行这个函数
        const handleRequest = (req, res) => {
            const ctx = this.createContext(req, res)
            if (!this.ctxStorage) {
				// 核心执行就是handleRequest
                return this.handleRequest(ctx, fn)
            }
            return this.ctxStorage.run(ctx, async () => {
                return await this.handleRequest(ctx, fn)
            })
        }
        return handleRequest
    }
    handleRequest (ctx, fnMiddleware) {
        const res = ctx.res
        res.statusCode = 404 // 修改状态码 默认是404
        const onerror = err => ctx.onerror(err)
        const handleResponse = () => respond(ctx)
        onFinished(res, onerror)
        // 执行中间件
        return fnMiddleware(ctx).then(handleResponse).catch(onerror)
    }
}

我们此时可以知道核心方法是compose。在执行listen的时候,生成一个context上下文,并且将上下文传递到中间件里,此时我们可以理解为 通过compose将中间件进行联合,生成context,处理请求,监听server。

const compose = require('koa-compose')
// listen
class Application {
    constructor() {
        this.middlerware = [];
    }
	listen(...args) {
        const middlerFn = compose(this.midderware);
        const server = http.createServer((req,res) => {
            const ctx = this.createContext(req, res);
            return middlerFn(ctx).then(res => {
                // 响应处理
            }).catch(err => {
                // 错误处理
            })
        })
        server.listen(...args);
    }
}

compose 实现原理

/**
* middleware 必须是 函数数组
*/
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
       */
	// 这个函数就是中间件实际处理函数 
    // app.use((ctx, next) => ...); 其中next就是下面的dispatch方法
    // 这个dispatch 会调用middleware对应的那个方法
    // next 上文中middlerFn的第二个参数,也就是说执行完middleware之后可以执行自定义逻辑
    return function (context, next) {
        // 默认 index是 -1 表示未执行的状态
        let index = -1
        // 执行第一个middlerwaree
        return dispatch(0)
        function dispatch (i) {
            // 出现index = i的场景 某一个中间件里调用了两次next函数
            if (i <= index) return Promise.reject(new Error('next() called multiple times'))
            // 赋值index;
            index = i
            // 找到middleware方法
            let fn = middleware[i]
            // 执行完middleware了,看一下next有没有传,如果有传在走一遍逻辑
            if (i === middleware.length) fn = next
           	// 如果没有middleware 返回一个空的promise
            if (!fn) return Promise.resolve()
            try {
                // 依次向下调用
                return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
            } catch (err) {
                return Promise.reject(err)
            }
        }
    }
}

此时可以知道koa作者借助promise的特性实现洋葱模型

// 简单模拟 
async function p1 (next) {
    console.log(1);
    await next(); // 此时的next就是p2
    console.log(2)
}

async function p2 (next) {
    console.log(3);
    await next(); // 此时的next就是p3
    console.log(4)
}

async function p3 (next) {
    console.log(5);
    await next();
    console.log(6)
}
const list = [p1,p2,p3];

function compose(list) {
    let index = -1;
    return () => {
         function next(i) {
            if (index >= i) throw new Error('调用了多次next');
            index = i;
            const fn = list[i];
            if(!fn) {
                return Promise.resolve();
            }
            try {
                return Promise.resolve(fn(next.bind(null, i+1)));
            } catch (err) {
                return Promise.reject(err);
            }
        }
        return next(0);
    }
}

const fn = compose(list);
fn();