这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战
我是小十七_,今天和大家一起阅读 koa 中间件源码,通俗易懂,包教包会~
Koa 的中间件不同于 Express,Koa 使用洋葱模型原理。它的源码只包含四个文件,对于初读源码的同学非常友好,今天我们只看主文件 - application.js
,它已经包含了中间件是如何工作的核心逻辑。
前置准备
首先 clone koa 源码
1 | js复制代码git clone git@github.com:koajs/koa.git |
然后我们在项目的根目录添加一个 index.js 用于测试
1 | js复制代码// index.js |
运行下面的命令来启动服务器:
1 | js复制代码node index.js |
接着访问 http://localhost:3000
,你会看到 1, 2, 3, 4, 5, 6
输出。这称为洋葱模型(中间件)
洋葱模型的工作原理
让我们一起阅读 koa 的核心代码,看看中间件是如何工作的。在 index.js 中,我们这样使用中间件:
1 | ini复制代码const app = new Koa(); |
让我们来看看 application.js
,它在源代码的 lib
目录下,这里是与中间件相关的代码,我把代码进行了简化,保留了中间件的核心逻辑,并在代码中添加了一些注释。
1 | js复制代码module.exports = class Application extends Emitter { |
我们把代码简化为只有关于 compose 函数的部分的伪代码:
1 | js复制代码 listen(...args) { |
从上面的代码可以猜测到:compose 函数,执行后返回了一个函数(这里叫 fn),fn 函数执行后,返回的是一个 promise。
关于 compose 函数
更多关于 compose
函数的信息,我们可以看一下 koa-compose
包的源码
1 | js复制代码module.exports = compose |
和上面猜测的一样,我们简称 compose 返回的函数叫 fn,所有中间件都被 compose 传递到 了 fn 函数中,它返回了 dispatch(0)
,也就是立即执行了 dispatch
函数并返回了一个 promise。在了解 dispatch
函数的内容之前,我们先要了解 promise
的语法。
关于 Promise
通常我们会像这样使用 promise:
1 | js复制代码const promise = new Promise(function(resolve, reject) { |
在 Koa 中,它是这样使用的:
1 | js复制代码let testPromise = new Promise((resolve, reject) => { |
因此,我们知道在 compose
函数中,它返回一个 promise
。
回到 Koa - compose 中间件
1 | js复制代码module.exports = compose |
dispatch 是一个递归函数,它将循环所有中间件。其中我们简化一下递归的部分:
1 | js复制代码let fn = middleware[i] |
这里 fn 是当前的中间件函数,执行了 fn,参数分别是 context
和 dispatch.bind(null, i + 1)
(也就是我们传给中间价的 next),中间件执行这个函数,也就递归执行了 dispatch
函数,具体看下面的分析:
在我们的测试文件 index.js 中,我们有 3 个中间件,所有 3 个中间件都会在 await next()
之前执行这些代码;
1 | js复制代码app.use(async (ctx, next) => { |
我们可以看一下 index.js
中这三个中间件的执行顺序:
- 执行
dispatch(0)
时,会执行 Promise.resolve(fn(context, dispatch.bind(null, 0 + 1)))
- 第一个中间件内容将运行直到
await next()
next()
=dispatch.bind(null, 0 + 1)
,这是第二个中间件- 第二个中间件将运行直到
await next()
next()
=dispatch.bind(null, 1 + 1)
,这是第三个中间件- 第三个中间件将一直运行到
await next()
next() = dispatch.bind(null, 2 + 1)
,没有第四个中间件,会立即通过if (!fn) return Promise.resolve()
返回,第三个中间件中的await next()
被解析,剩余执行第三个中间件中的代码。- 第二个中间件中的
await next()
被解析,第二个中间件中的剩余代码被执行。 - 第一个中间件中的
await next()
被解析,第一个中间件中的剩余代码被执行。
为什么使用洋葱模型?
如果我们在中间件中有 async/await,编码会更简单。当我们想为 api 请求编写一个时间记录器时,通过添加这个中间件可以非常容易:
1 | js复制代码app.use(async (ctx, next) => { |
本文转载自: 掘金