js之旅:揭开koa的洋葱模型-拦截器的神秘面纱
什么是洋葱模型
- 中间件的执行顺序是从外到内,再从内到外,形成一个类似洋葱的圆环结构。
- 中间件可以通过调用next函数将控制权交给下一个中间件,并等待其完成后继续执行后续逻辑。
什么是拦截器
- 拦截器是一个异步函数,它接收两个参数:上下文ctx对象和next函数。
- 拦截器可以在next函数之前或之后进行一些操作,比如修改ctx对象的属性或方法,抛出异常,返回响应等。
- 拦截器可以通过调用next函数将控制权交给下一个拦截器或路由处理函数,并等待其完成后继续执行后续逻辑。
- 拦截器可以通过app.use方法注册到Koa应用中,也可以通过koa-router模块注册到特定的路由上。
ok,现在我们知道了大致概念,下面开始着手实现了
开始手写洋葱模型的拦截器
盘点需求
实现代码
// 定义一个异步函数,用来实现延时效果
const sleep = (mms = 1000) => {
return new Promise((a, b) => {
setTimeout(() => a(), mms); // 在mms毫秒后调用resolve函数
});
};
// 定义一个高阶函数,用来返回一个类似Koa的拦截器
const oap = (number) => {
return async (ctx, next) => {
console.log("opa " + number); // 打印出"opa " + number
await sleep(); // 等待一秒
await next(); // 调用下一个拦截函数,并等待其完成
console.log("oap " + number); // 打印出"oap " + number
}
}
定义了一个拦截器函数,接收一个全局上下文,和一个next。它返回一个异步函数,这个异步函数接收两个参数:ctx和next,分别表示上下文对象和下一个拦截函数。这个异步函数的作用是打印出"opa " + number,然后调用sleep函数等待一秒,然后调用next函数执行下一个拦截函数,并等待其完成后再打印出"oap " + number。
再来定义一个调用拦截器的函数
class Interceptor {
constructor() {
this.aspects = [];
}
// 添加拦截器,并且返回this,可以实现链式use调用
use(func) {
this.aspects.push(func);
return this;
}
// 运行所有拦截器
async run(context) {
const aspects = this.aspects;
// 组装拦截器
const proc = aspects.reduceRight(
(a, b) => {
return async () => {
try {
await b(context, a);
} catch {
console.log("aspect error");
}
};
},
() => Promise.resolve()
);
// 实际运行
try {
await proc();
} catch (ex) {
console.error(ex);
}
return context;
}
}
这段代码就是一个拦截器调用的实现,它可以在一个上下文对象context上执行一系列的异步函数,形成一个类似Koa的洋葱模型。代码的主要部分是:
- Interceptor类,它是一个拦截器的构造函数,它有一个aspects属性,用来存储所有的拦截函数。
- use方法,它是一个实例方法,用来添加一个拦截函数到aspects数组中,并返回this,实现链式调用。
- run方法,它是一个异步的实例方法,用来运行所有的拦截函数,并传入一个context对象作为参数。
- proc变量,它是一个局部变量,用来组装所有的拦截函数,它使用reduceRight方法从右到左遍历aspects数组,并返回一个异步函数,这个异步函数接收两个参数:context和a,其中a是下一个拦截函数。在这个异步函数中,使用try…catch语句来捕获可能的错误,并使用await语句来等待当前的拦截函数b执行完毕后再执行a。
- proc()调用,它是一个异步操作,用来实际运行所有的拦截函数,并等待其完成后返回context对象。
其中run方法有些复杂,下面详细讲讲run的作用是运行所有的拦截函数,并传入一个context对象作为参数。它的具体步骤如下:
- 首先,它从this对象中获取aspects属性,这是一个存储所有拦截函数的数组。
- 然后,它定义了一个proc变量,这是一个局部变量,用来组装所有的拦截函数,它使用reduceRight方法从右到左遍历aspects数组,并返回一个异步函数。
- 在reduceRight方法的回调函数中,它接收两个参数:a和b,其中a是上一次迭代的结果,也就是下一个拦截函数,b是当前迭代的元素,也就是当前的拦截函数。它返回了一个新的异步函数。
- 最后,它使用await语句来等待proc()调用完成,并返回context对象。proc()调用就是实际运行所有的拦截函数,并传入context对象作为参数。
使用手写的拦截器
// 创建一个Interceptor类的实例
const interceptor = new Interceptor();
// 添加四个拦截函数
interceptor.use(oap(1));
interceptor.use(oap(2));
interceptor.use(oap(3));
interceptor.use(oap(4));
// 打印结果
// opa 1
// opa 2
// opa 3
// opa 4
// oap 4
// oap 3
// oap 2
// oap 1
这个代码是一个使用Interceptor类和oap函数的代码,它的作用是:
- 创建一个Interceptor类的实例,命名为interceptor。
- 调用interceptor的use方法,添加四个拦截函数,分别是oap(1),oap(2),oap(3)和oap(4)。这些拦截函数会按照添加的顺序执行,并打印出相应的数字。
因为拦截函数是按照洋葱模型执行的,也就是从外到内,再从内到外,形成一个类似洋葱的圆环结构。每个拦截函数在调用next函数之前打印出"opa " + number,然后等待一秒,调用next函数去执行下一个拦截函数,并等待next函数完成后,再打印出"oap " + number效果就像下面这张图该拦截还有一个性质,如果其中一个拦截器运行报错,后面拦截器都不执行。来回顾一下这个代码
// 组装拦截器
const proc = aspects.reduceRight(
(a, b) => {
return async () => {
try {
await b(context, a);
} catch {
console.log("aspect error");
}
};
},
() => Promise.resolve()
);
当其中一个拦截器报错就直接结束这个函数,而不是继续往下执行a函数来看个具体的例子。首先修改一下刚才的oap函数
const oap =
(number, isError = false) =>
async (ctx, next) => {
console.log("opa " + number);
await sleep();
if (isError) {
undefined.map();
}
await next();
console.log("oap " + number);
};
oap函数添加了一个参数isError,isError默认false,当其为true的时候,函数内部就会执行undifined.map()
,会直接抛出错误,而不执行下面的next
函数。从而导致后面的拦截器都得不到执行跑个例子看看
const interceptor = new Interceptor();
interceptor.use(oap(1));
interceptor.use(oap(2, true));
interceptor.use(oap(3));
interceptor.use(oap(4));
// 运行结果
// opa 1
// opa 2
// aspect error
// oap 1
第二个拦截器会报错,所以第三个和第四个拦截器就不会执行了。
总结
本篇文章手动实现了洋葱模型拦截器的所有功能:
- 一个请求过来后,多个拦截器依次执行
- 切面函数有两个参数,第一个参数是所有拦截器共享的上下文,第二个参数是执行下一个拦截器的方法
- 当一个拦截器执行报错,后面的切面函数都不执行
不得不说,思维还是挺妙的,不知道这种思维能用到实际业务中去不
转载自:https://juejin.cn/post/7224861543749566524