Koa中间件模型分析

section1

koa 的中间件原理,其实就是 koa 的中间件实现原理,大家可能会很惊讶 koa 的中间件怎么会有原理呢?但事实就是这样,小编也感到非常惊讶。

这就是关于 koa 的中间件原理的事情了,大家有什么想法呢,欢迎在评论区告诉小编一起讨论哦!

看到这里如果你想走?你真的想走?,不想多看看我的文章了解一些好康的?

好了,正经点,我的这博客是个本质上可是个传(自)播(娱)知(自)识(乐)的平台,怎么可能有什么乱七八糟的小编。下面我将正式带你了解了解 Koa 的中间件模型。

section 2

如果 0202 年还有前端老哥问 Koa 是个啥,我想这也不用混了。简单说 Koa 就是个高可扩展的 node 框架。而如果问 Koa 最迷人的地方,除了让人舒服的异步写法,那就是优雅的中间件处理了。

以下默认读者了解 Koa 的圆葱模型,若是不知道也莫得问题,下面贴心的 小编 作者给你挂个图:

清晰明了简单易懂,这时候可能你就要问了:这么优雅的中间件处理是怎么实现的呢?

按照往常的习惯,遇事不决,手撕源码。

section 3

众所周知,想要看如何实现的前提是知道怎么用。(因为懒得下 Koa 了)我就简单写一些伪代码:

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

// Todo others

app.use(async (ctx, next) => {
console.log(1);
await next();
console.log(4);
});

app.use(async (ctx, next) => {
console.log(2);
await next();
console.log(3);
});

// Todo others

app.listen(3000, () => {
console.log("listen on 3000 port");
});

假装的测一下:

1
curl http://localhost:3000

下面是装作有输出:

1
console => // others..... // 1 // 2 // 3 // 4 // others.....

可以看到,Koa 的使用优雅简单,简直吹爆好吗。对了,Koa 是也是 tj 写的,小伙子又帅代码写的又好,让我这种码狗肥宅情何以堪。

下面正式从源码看看 Koa 对中间件的处理。

section 4

打开 Koa 项目目录:

纳尼?没有 src?出于经验判断,放源码的应该是 lib 目录了,不过出于 水文章 专业性考虑,这里必须先看一下 package.json。

可以看到,项目入口确实是 lib 下的 application.js。

不多 bb,直接打开它:

因为这篇文章主要讲中间件,也不需要看没用的地方了,直接锁定 use 方法和 middleware 处理逻辑,无耻的我当然选择复制粘贴:

use:

1
2
3
4
5
6
7
8
9
10
use(fn) {
if (typeof fn !== 'function') throw new TypeError('middleware must be a function!');
if (isGeneratorFunction(fn)) {
deprecate('...');
fn = convert(fn);
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
}

虽然已经可以猜到,但是 use 的逻辑就是如此简单:把中间件方法 push 放到数组就 OK 了。而下面那个东东才是本文重点

callback:

1
2
3
4
5
6
7
8
9
10
11
12
callback() {
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;
}

这个 compose 是什么东东?知道函数式编程的铁汁们肯定都很大概都能猜到这是干啥子的了。我们看看它是哪里来的

1
const compose = require("koa-compose");

纳尼,竟然是另一个项目引入的。好嘛,github 直接搜索,可以看到这个项目:

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

没错,就是这么短

我们来看看他做了什么?简单说就是返回一个 function,compose 在函数式编程里作用是代码的组合,这里把 middleware 给用一个闭包保存,然后就可以在新获取的函数肆意发挥了。

再来看看它返回的函数到底做了啥:

嗯~,一个变量,一个 return,构成了 JS 最本来的味道。

index 变量我理解是用来防止访问反向执行 middleware 数组属于对安全性的封装?(其实不太理解),不过无伤大局。这个 dispatch 函数显然才是重点:

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
}
}

没错,为了读者老爷着想,我又复制了一份,绝不是为了水长度哈。

我们可以看到,这个 dispatch 函数本质上就是获取每个已经注册的中间件,然后把这个中间件执行的同时,把自己的参数(dispatch(i+1))作为这个中间件的第二个参数(next)传入。

那么我们可以想到,如果在这个执行的中间件中调用 next 方法,就等同于调用下一个中间件了。然后就造成了圆葱模型的效果。

具体执行模拟可以看这段伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
function middleware1(){
// do something
middleware2()
// do something
}

function middleware2(){
// do something
middleware3()
// do something
}

function middleware3{}{.....}

肉眼可见的可扩展与优雅好吗。但是就我个人来讲,我还是不太喜欢这种写法,我在网上看到另一个人对这种原理的抽象,我很喜欢,所以想给你们康康:原文地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
compose() {
return async ctx => {
function createNext(middleware, oldNext) {
return async () => {
await middleware(ctx, oldNext);
}
}
let len = this.middlewares.length;
let next = async () => {
return Promise.resolve();
};
for (let i = len - 1; i >= 0; i--) {
let currentMiddleware = this.middlewares[i];
next = createNext(currentMiddleware, next);
}
await next();
};
}

我觉得这个 compose 更加符合我的直觉。当然,每个人有自己的看法,这段代码我也就不解释了,本质上它们还是做着同一件事情。

自己写一个测试一下,毕竟要有图有真相啊:

可见没啥毛病。

那么在座的好学的小老弟可能会问了,这么好的写法(思想)我们哪里可以用到呢?

前一阵子看 Vue-Router 源码时候我发现,Vue-Router 同样使用了这个思想。

section forget(加餐)

用过 Vue-Router 的小伙伴一定会知道,Vue-Router 有个叫守卫的东东,注册了一个 router.beforeEach 必须调用其中的 next 参数继续执行下一个守卫,下面代码来自官方文档:

1
2
3
4
5
6
// BAD
router.beforeEach((to, from, next) => {
if (to.name !== "Login" && !isAuthenticated) next({ name: "Login" });
// 如果用户未能验证身份,则 `next` 会被调用两次
next();
});

怎么样,是不是感觉似曾相识?

无凭无据,不看代码怎敢章口就来?

下面直接看源码:

由于 Vue-Router 这部分处理有很多与本文主题无关的东西,我们不去纠结细节直接省略它们,同样这里我只粘需要看的地方:

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
const queue: Array<?NavigationGuard> = [].concat(
// ....
// global before hooks
this.router.beforeHooks,
// in-component update hooks
extractUpdateHooks(updated)
// ...
);

runQueue(queue, iterator, () => {
const postEnterCbs = [];
const isValid = () => this.current === route;
// wait until async components are resolved before
// extracting in-component enter guards
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid);
const queue = enterGuards.concat(this.router.resolveHooks);
runQueue(queue, iterator, () => {
if (this.pending !== route) {
return abort(createNavigationCancelledError(current, route));
}
this.pending = null;
onComplete(route);
if (this.router.app) {
this.router.app.$nextTick(() => {
postEnterCbs.forEach((cb) => {
cb();
});
});
}
});
});

接下来我们再把 runQueue 拿过来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export function runQueue(
queue: Array<?NavigationGuard>,
fn: Function,
cb: Function
) {
const step = (index) => {
if (index >= queue.length) {
cb();
} else {
if (queue[index]) {
fn(queue[index], () => {
step(index + 1);
});
} else {
step(index + 1);
}
}
};
step(0);
}

我就问你他俩是不是一回事?

下面我就简单解释一下这段代码:

简单说就是遍历 beforeEnter 守卫每一项,把它传给 iterator,不好意思,忘贴 iterator 了, 为了简化代码,我们假设路由的 next 函数不能传参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const iterator = (hook: NavigationGuard, next) => {
if (this.pending !== route) {
return abort(createNavigationCancelledError(current, route))
}
try {
hook(route, current, (to: any) => {
// 对有参数的判断处理
next(to)
}
})
} catch (e) {
abort(e)
}
}

发现了吗,每个守卫执行完场以后会直接去调用延迟执行的 step(n+1),一模一样好伐。等到所有的 enter hooks 执行结束以后,调用回调(cb),处理组件(onComplete)与其他守卫。

所以说看没看到,这种设计模式多么重要,前端小伙伴们赶快搞起。

section -1

如果对我的文章感兴趣记得点赞投币三连。啊,不对,走错片场了…..

那么只要感谢 CV 大哥手下留情别盗我文章就好了。

查看评论