这是我参与更文挑战的第11天,活动详情查看:更文挑战
koa-compose 是用于将 Koa 中间件进行合并的工具,是实现 Koa 中间件洋葱模型的核心代码。
Koa 简单介绍
在分析 koa-compose 源码之前,先了解一下 koa 的相关知识。
Koa 应用程序是一个包含一组中间件函数的对象,它是按照类似堆栈的方式组织和执行的。中间件以 async 函数的形式,通过 use 方法添加。
当请求开始时,请求流会优先通过第一个中间件,当该中间件调用 next()
则该函数暂停并将控制传递给定义的下一个中间件。当没有下一个中间件时,堆栈将展开并且每个中间件恢复执行其上游行为。
这种中间件模型叫做「洋葱模型」,靠前的中间件处于「洋葱」的外层,靠后的中间件则处于「洋葱」的里层,如下图所示:
示例代码如下:
const koa = require('koa');
const app = new koa();
app.use(async (ctx, next) => {
console.log('first begin')
next();
console.log('first end')
})
app.use(async (ctx, next) => {
console.log('second begin')
next();
console.log('second end')
})
app.use((ctx, next) => {
console.log('third begin')
next();
console.log('third end')
})
app.use(ctx => {
console.log('response');
ctx.body = 'hello'
})
app.listen(3000)
// 运行后,访问 localhost:3000,结果如下:
// first begin
// second begin
// third begin
// response
// third end
// second end
// first end
复制代码
简单看一下 Koa 的源码,它是定义了一个 middleware
数组用来存放所有的中间件,然后通过 koa-compose
将 middleware
中的所有中间件组成洋葱模型的形式,返回一个大的中间件。相关代码如下所示:
module.exports = class Application extends Emitter {
constructor(options) {
// ......
this.middleware = []; // 初始化一个中间件数组
//...
}
use(fn) {
// ......
this.middleware.push(fn); // 将中间件函数放入数组中
// .....
}
callback() {
// 使用koa-compose 将中间件们以洋葱模型的方式组合
const fn = compose(this.middleware);
//......
}
listen(...args) {
debug('listen');
const server = http.createServer(this.callback());
return server.listen(...args);
}
}
复制代码
koa-compose 源码
koa-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!')
}
return function (context, next) {
// 用于确保 next 只调用一次
let index = -1
return dispatch(0) // 返回一个 Promise.resolve(fn0(context, ...))
function dispatch (i) {
// 第一次 next 递归完后,index === middleware.length
// 通过 i 与 index 比较确保中间件只调用了一次 next
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
let fn = middleware[i]
// 如果是最后一个中间件,就把参数 next 放到最后
if (i === middleware.length) fn = next
if (!fn) return Promise.resolve()
try {
// 递归调用 dispatch,通过 bind 绑定参数
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
} catch (err) {
return Promise.reject(err)
}
}
}
}
复制代码
可以看到,koa-compose
是借助函数dispatch
的递归跟promise.resolve()
方法,将每一个中间件都封装到Promise.resolve()
然后传递到上一个中间件的 next 方法中,通过层层嵌套实现了 Koa
中间件的洋葱模型。
回过头来看开篇的那个简单的示例,经过 compose
方法转换后的结构如下所示:
return function (context, next) {
// dispatch(0) 返回 Promise.resolve(中间件1)
Promise.resolve(async (context, dispatch.bind(null, 1)) => {
console.log('second begin')
// next(), 即 dispatch.bind(null,1)(),返回 Promise.resolve(中间件2)
Promise.resolve(async (context, dispatch.bind(null, 2)) => {
console.log('second begin')
// next(),即 dispatch.bind(null, 2)(),返回 Promise.resolve(中间件3)
Promise.resolve(async (context, dispatch.bind(null, 3)) => {
console.log('third begin')
// next(),即 dispatch.bind(null, 3)(), 返回 Promise.resolve(中间件4)
Promise.resolve(async (context) => {
console.log('response');
context.body = 'hello'
})
console.log('third end')
})
console.log('second end')
})
console.log('second end')
})
}
复制代码
资料
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END