likes
comments
collection
share

js之旅:揭开koa的洋葱模型-拦截器的神秘面纱

作者站长头像
站长
· 阅读数 20

这篇文章将手写一个非常简单的koa的洋葱模型

什么是洋葱模型

  1. 中间件的执行顺序是从外到内,再从内到外,形成一个类似洋葱的圆环结构。
  2. 中间件可以通过调用next函数将控制权交给下一个中间件,并等待其完成后继续执行后续逻辑。

js之旅:揭开koa的洋葱模型-拦截器的神秘面纱

在koa中,洋葱模型是以拦截器形式来实现的

什么是拦截器

  1. 拦截器是一个异步函数,它接收两个参数:上下文ctx对象和next函数。
  2. 拦截器可以在next函数之前或之后进行一些操作,比如修改ctx对象的属性或方法,抛出异常,返回响应等。
  3. 拦截器可以通过调用next函数将控制权交给下一个拦截器或路由处理函数,并等待其完成后继续执行后续逻辑。
  4. 拦截器可以通过app.use方法注册到Koa应用中,也可以通过koa-router模块注册到特定的路由上。

ok,现在我们知道了大致概念,下面开始着手实现了

开始手写洋葱模型的拦截器

盘点需求

  1. 一个请求过来后,多个拦截器依次执行
  2. 切面函数有两个参数,第一个参数是所有拦截器共享的上下文,第二个参数是执行下一个拦截器的方法
  3. 当一个拦截器执行报错,后面的切面函数都不执行

实现代码

// 定义一个异步函数,用来实现延时效果
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效果就像下面这张图js之旅:揭开koa的洋葱模型-拦截器的神秘面纱该拦截还有一个性质,如果其中一个拦截器运行报错,后面拦截器都不执行。来回顾一下这个代码

// 组装拦截器
		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

第二个拦截器会报错,所以第三个和第四个拦截器就不会执行了。

总结

本篇文章手动实现了洋葱模型拦截器的所有功能:

  1. 一个请求过来后,多个拦截器依次执行
  2. 切面函数有两个参数,第一个参数是所有拦截器共享的上下文,第二个参数是执行下一个拦截器的方法
  3. 当一个拦截器执行报错,后面的切面函数都不执行

不得不说,思维还是挺妙的,不知道这种思维能用到实际业务中去不