Skip to main content

koa code analyse

· 13 min read
liaohuanyu

Koa2官网 ——> github地址

Koa2进阶学习笔记 ——> github地址

Koa.js 设计模式-学习笔记 ——> github地址

koa类的结构

module.exports = class Application extends Emitter{
constructor() {
super();
this.proxy = false;
this.middleware = [];
this.subdomainOffset = 2;
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
listen(...args) {...}
toJSON() {...}
inspect() {...}
use(fn) {...}
callback() {...}
handleRequest(ctx, fnMiddleware) {...}
createContext(req, res) {...}
onerror(err) {...}

简单示例出发

如下为官方给出的级联形式的koa示例,

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);

上述示例的工作过程可以分为准备阶段与处理reques事件这两个阶段。

准备阶段

准备阶段会创建koa实例,注册中间件,监听指定端口,中间件的处理方式为洋葱模型设计模式:

  1. 创建一个koa实例为app;
  2. 注册logger中间件,也就是将中间件函数添加到一个名为middleware的数组中。
  3. 注册x-response-time中间件。
  4. 注册response中间件。
  5. 监听3000端口。

1. 创建koa实例,调用constructor,定义实例属性middleware等等

2. koa实例的koa类的原型对象方法use添加中间件,use方法只是简单的将中间件添加到实例的属性middleware数组中,然后返回实例自身的this。

  use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}

3. 监听端口3000,调用原型对象方法listen,该方法内部会调用node的http模块,调用实例方法this.callback,然后将返回结果作为http.createServer的传入参数即作为端口触发的request事件的回调函数。http.createServer运行后返回一个http服务器,然后将该http服务器监听端口3000。在3000端口有请求的时候,触发node机制的request事件,并调用this.callback()返回的函数。this.callback函数会将中间件函数组织成洋葱模型,并返回一个执行入口函数,用于按照特定顺序执行中间件。

在执行this.callback()函数的过程中,首先会调用compose函数处理中间件,然后返回handleRequest函数,该函数是request事件的回调函数,用于执行中间件对req与res进行处理。

  listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
callback() {
//compose函数将中间件数组转换成执行链函数fn
const fn = compose(this.middleware);

if (!this.listenerCount('error')) this.on('error', this.onerror);

const handleRequest = (req, res) => {
const ctx = this.createContext(req, res);
return this.handleRequest(ctx, fn);
};

return 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如何组织中间件数组middleware

如何实现一个compose,该函数的功能是按顺序执行middleware中的中间件:

  1. 假设中间件函数都是一步到底全部执行完后再执行下一个中间件
  2. 中间件函数首先执行一部分然后执行其他中间件,然后再回来执行执行剩下的部分。
  3. 中间件函数是异步的情况
  4. 限制一个中间件只能执行一次
  5. 允许中间件截获信息以及处理错误

中间件函数都是一步到底全部执行完后再执行下一个中间件:

function myCompose(ctx = [],middleware){
return dispatch(0)
function dispatch(i){
let fn = middleware[i];
if(!fn) return;
return fn(ctx, dispatch.bind(null,i+1));
}
}

let fn0 = function(ctx,next){
console.log(0);
ctx.push(0)
next()
}

let fn1 = function(ctx,next){
console.log(1);
ctx.push(1)
next();
}

let fn2 = function(ctx,next){
console.log(2);
ctx.push(2)
next();
}

let middleware = [fn0,fn1,fn2]
let ctx = []
myCompose(ctx,middleware);
console.log(ctx);

中间件函数首先执行一部分然后执行其他中间件,然后再回来执行执行剩下的部分

上面的代码能否实现这样的一个功能呢?测试如下:

let fn0 = function(ctx,next){
console.log(0);
ctx.push(0)
next()
console.log("fn0");
}

let fn1 = function(ctx,next){
console.log(1);
ctx.push(1)
next();
console.log("fn1");
}

let fn2 = function(ctx,next){
console.log(2);
ctx.push(2)
next();
console.log("fn2");
}
let middleware = [fn0,fn1,fn2]
let ctx = []
myCompose(ctx,middleware);
console.log(ctx);

返回的结果:

0
1
2
fn2
fn1
fn0
(3) [0, 1, 2]

显然上面的myCompose函数是可以实现这样的功能的。

中间件函数是异步的情况

测试异步的情况:

let fn0 = async function(ctx,next){
console.log(0);
ctx.push(0)
await next()
console.log("fn0");
}

let fn1 = async function(ctx,next){
console.log(1);
ctx.push(1)
await next();
console.log("fn1");
}

let fn2 = async function(ctx,next){
console.log(2);
ctx.push(2)
await next();
console.log("fn2");
}
let middleware = [fn0,fn1,fn2]
let ctx = []
myCompose(ctx,middleware);
console.log(ctx);

返回的结果:

0
1
2
(3) [0, 1, 2]
fn2
fn1
fn0

限制一个中间件只能执行一次

function myCompose(ctx = [],middleware){
return dispatch(0)
function dispatch(i){
let fn = middleware[i];
if(!fn) return;
return fn(ctx, dispatch.bind(null,i+1));
}
}
let fn0 = async function(ctx,next){
console.log(0);
ctx.push(0)
await next()
await next()
console.log("fn0");
}

let fn1 = async function(ctx,next){
console.log(1);
ctx.push(1)
await next();
console.log("fn1");
}

let fn2 = async function(ctx,next){
console.log(2);
ctx.push(2)
await next();
console.log("fn2");
}
let middleware = [fn0,fn1,fn2]
let ctx = []
myCompose(ctx,middleware);
console.log(ctx);

返回的结果:

0
1
2
(3) [0, 1, 2]
fn2
fn1
1
2
fn2
fn1
fn0

在fn0函数中调用了两次next(),这样会导致重新执行了fn0后面的所有中间件函数。为了避免这样情况的发生,需要限制一个中间件只能执行一次。myCompose函数是通过调用dispatch函数来执行中间件函数的,因此可以通过阻止dispatch函数来阻止中间件函数的执行。从myCompose函数可知,在中间件函数中执行到第二次next的时候,必定已经调用了middleware.length次dispatch,即执行过了所有中间件函数next之前的逻辑。执行next()函数其实就是执行dispatch.bind(null, i + 1) ,由此可以看出,每个中间件函数执行第二次 next 的函数的时候,传入的第一个参数就是 i+1 ,i+1 的范围是[0,3]。因此可以记录一下已经执行了多少次dispatch到index中,然后每次dispatch的时候比较第一个参数 i+1 与 index 的值,来判断是否存在同一个中间件函数被执行了多次的情况。因此修改后myCompose如下:

function myCompose(ctx = [],middleware){
let index = -1;
return dispatch(0)
function dispatch(i){
if(i <= index) return;
index = i;
let fn = middleware[i];
if(!fn) return;
return fn(ctx, dispatch.bind(null,i+1));
}
}

返回的结果:

0
1
2
(3) [0, 1, 2]
fn2
fn1
fn0

允许中间件截获信息以及处理错误

对于一个中间件而言,比如

let fn0 = async function(ctx,next){
console.log(0);
ctx.push(0)
await next()
console.log("fn0");
}

如果下一个中间件出现了问题,或者fn0中间件函数需要下一个中间件函数返回一些数据进行处理,那么如何实现呢?利用promise.resolve以及reject来处理数据以及错误信息。修改后的代码为:

function myCompose(ctx = [],middleware){
let index = -1;
return dispatch(0)
function dispatch(i){
if(i <= index) return Promise.reject(new Error('next() called multiple times in a middleware'));
index = i;
let fn = middleware[i];
if(!fn) return Promise.resolve();
return Promise.resolve(fn(ctx, dispatch.bind(null,i+1)));
}
}

如果return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));出现错误,利用try-catch进行捕获并且将错误信息reject到上层中间件。修改后的代码为:

function myCompose(ctx = [],middleware){
let index = -1;
return dispatch(0)
function dispatch(i){
if(i <= index) return Promise.reject(new Error('next() called multiple times in a middleware'));
index = i;
let fn = middleware[i];
if(!fn) return Promise.resolve();
try {
return Promise.resolve(fn(ctx, dispatch.bind(null,i+1)));
} catch(err){
return Promise.reject(err)
}
}
}

官方给出的compose实现如下:

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
*/

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)
}
}
}
}

与之前的myCompose版本比较多了一个if (i === middleware.length) fn = next;这样处理是为了检测如果执行到middleware最后一个中间件,则将compose(this.middleware)(ctx,fnLast);中的fnLast最为最后一个中间件。koa源码中这个next为undefined。

处理reques事件

当监听到3000端口有请求过来的时候,触发request事件,然后开始执行各个中间件,实际在之前实现myCompose函数的过程中可以看出中间件执行的顺序。

总结

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);

这里中间件级联的方式对ctx进行逐步处理,根据compose.js可知传入中间件的next其实就是:

dispatch.bind(null, i + 1)

工作原理

  1. 创建一个koa对象,然后调用use(fn)将fn push到该koa对象的中间件数组中
  2. 接着调用listen创建一个服务器容器,然后调用this.callback(),然后监听指定端口
  3. this.callback()首先会调用compose对中间件数组进行处理,返回一个洋葱模型的入口函数。然后返回一个handleRequest
  4. handleRequest函数会在监听到端口有请求的时候调用,该函数最终会调用洋葱模型的入口函数。
  5. handleRequest函数接收两个参数req, res;该函数执行的时候首先会根据传入的req, res创建一个ctx;然后调用koa对象的handleRequest函数,将结果返回。
  6. koa对象的handleRequest函数接收两个参数(ctx, fn),ctx就是根据req, res创建的ctx,fn就是调用compose返回的洋葱模型入口函数。 koa对象的handleRequest函数最终会调用fn(ctx)

在中间件需要级联的时候,需要给中间件传入第二个参数next

技巧

let fn0 = async function(ctx,next){
console.log(0);
ctx.push(0)
await next()
await next().catch(err=>console.log(err))
console.log("fn0");
}

注意这里,中间件系统带来的问题,中间件可以随意给ctx定义实例方法以及属性,这样会导致中间件之间的覆盖的问题,很难检查不错误。通过在中间件对next进行catch来进行错误检查。

await next().catch(err=>console.log(err))

会打印错误,但是程序不会crash掉。