探索Koa的中间件原理

爱吃猪头爱吃肉

1. 中间件执行顺序

问题1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const Koa = require("koa");
const app = new Koa();

app.use((ctx, next) => {
console.log(1);
next();
console.log(2);
});
app.use(async (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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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

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

img

2. koakoa-compose

中间件注入

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

1
2
3
4
5
6
7
8
9
10
11
12
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中 执行过程如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 实现原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
/**
* 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(其实并没有什么,执行到这里时 中间件已经全部走完了,resolve表示回到第一个中间件的next,执行后续代码)
if (!fn) return Promise.resolve()
try {
// 依次向下调用
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
return Promise.reject(err)
}
}
}
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 简单模拟 
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(); // 此时的next 返回的是 Promise.resolve();
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();

  • 标题: 探索Koa的中间件原理
  • 作者: 爱吃猪头爱吃肉
  • 创建于: 2023-06-04 16:09:55
  • 更新于: 2023-06-04 16:19:15
  • 链接: https://zsasjy.github.io/2023/06/04/探索Koa的中间件原理/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
推荐文章
学习算法 学习算法 webpack常用配置汇总 webpack常用配置汇总 formily2学习笔记 formily2学习笔记 gulp笔记 gulp笔记 搭建react开发环境 搭建react开发环境 Docker基本用法 Docker基本用法
 评论