轻松搞定!手把手教你打造自定义Express路由中间件

在之前的两篇文中聊过了手写洋葱模型的拦截器,还有参数路由的正则校验。这篇文章我将这两个内容结合起来,手写一个路由中间件。

拦截器

让我们回忆一下拦截器:

  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。

使用拦截器

// 创建一个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函数的代码。
因为拦截函数是按照洋葱模型执行的,也就是从外到内,再从内到外,形成一个类似洋葱的圆环结构。每个拦截函数在调用next函数之前打印出”opa ” + number,然后等待一秒,调用next函数去执行下一个拦截函数,并等待next函数完成后,再打印出”oap ” + number
效果就像下面这张图
轻松搞定!手把手教你打造自定义Express路由中间件

这里只是简略的说下用法,关于拦截器的具体详细代码,请看这篇文章:js之旅:揭开koa的洋葱模型-拦截器的神秘面纱

express路由校验

const check = (rule,pathname)=>{
  // 
};
// 打印check函数的调用结果,第一个参数是规则,第二个参数是要检查的路径

console.log(check("/getName/:name", "/getName/zenos"));
// { name: 'zenos' },因为规则和实际路径匹配,并且捕获了zenos作为name参数的值

console.log(check("/getAge/:age", "/getAge/18")); 
// { age: '18' },因为规则和实际路径匹配,并且捕获了18作为age参数的值

console.log(check("/getInfo/:name/:age", "/getInfo/zenos/18")); 
// { name: 'zenos', age: '18' },
// 因为规则和实际路径匹配,并且捕获了zenos作为name参数的值和18作为age参数的值

console.log(check("/getAge/:age", "/getAge/18/12")); 
// null,因为规则和实际路径不匹配,多了一个/12部分

先是定义了一个函数check,它接受两个参数rule和pathname,对传入的参数进行校验。
如果校验成功,就获取路由中的参数。如果校验失败,就返回null;接着有对check函数的调用,让你更加明白check函数的作用。

关于参数路由校验的详细内容,可以看这里:js路由参数获取的秘密,你知道吗?只需三步!

路由拦截器

// router 函数用于根据指定的请求方法、路由规则和回调函数生成一个中间件
const router = (method, rule, callback) => {
  // 返回一个中间件函数,该函数接受两个参数:ctx(请求上下文)和 next(执行下一个中间件的函数)
  return (ctx, next) => {
    const { req } = ctx;
    const res = check(rule, req.pathname);

    // 检查请求方法和路由规则是否匹配:
    // - res 非空表示路由规则匹配
    // - res.route 为空表示不是一个已经解析过的路由
    // - req.method 与 method 相等,或者 method 为 "*" 表示请求方法匹配
    if (res && !res.route && (req.method === method || method === "*")) {
      ctx.route = res; // 将解析后的路由信息添加到请求上下文中
      callback(ctx, next); // 执行回调函数
      return;
    }

    // 请求方法或路由规则不匹配时,执行下一个中间件
    next();
  };
};

这段代码定义了一个名为 router 的函数,它接受三个参数:

  • method(请求方法,如 “GET”、”POST” 等)
  • rule(路由规则)和 callback(回调函数)
  • router 函数的作用是生成一个中间件

该中间件会根据请求方法和路由规则来处理请求。如果请求方法和路由规则与提供的参数匹配,router 函数将执行回调函数;否则,将执行下一个中间件。
router 函数可用于创建处理不同请求方法和路由规则的拦截器,然后就可以添加至Interceptor的拦截器处理中。
下面来看一个小demo,体会下router函数的用法
虽然说可以用,有点路由中间件的样子,接下来对其做些改造,让其变得更像express的路由中间件

改造路由拦截器

// Route 类用于创建处理不同请求方法的路由处理器
class Route {
  constructor() {}

  // get 方法用于创建处理 GET 请求的路由处理器
  get(rule, callback) {
    return router("GET", rule, callback);
  }

  // post 方法用于创建处理 POST 请求的路由处理器
  post(rule, callback) {
    return router("POST", rule, callback);
  }

  // all 方法用于创建处理任意请求方法的路由处理器
  all(rule, callback) {
    return router("*", rule, callback);
  }
}

我们定义了一个名为 Route 的类。这个类提供了三个方法:get、post 和 all,分别用于处理 GET、POST 和任意方法的请求。这些方法内部调用了 router 函数,并传入相应的请求方法、路由规则和回调函数。
下面看看具体用法:

const route = new Route();

// 创建 Interceptor 实例
const interceptor = new Interceptor();

// 添加一个处理特定 GET 请求的拦截器
interceptor.use(
  route.get("getInfo/name/:name/age/:age", (ctx, next) => {
    // 路由逻辑...
    console.log("name: ", ctx.route);
    // 执行下一个中间件
    next();
  })
);

// 添加一个处理特定 POST 请求的拦截器
interceptor.use(
  route.post("postAge/:age", (ctx, next) => {
    // 路由逻辑...
    console.log("age: ", ctx.route);
    // 执行下一个中间件
    next();
  })
);

这里我们创建了一个 Interceptor 类的实例,并通过调用 use 方法添加了多个拦截器。这些拦截器使用了 Route 类的方法,例如 route.get() 和 route.post(),来处理特定的路由请求。
下面就要对加入路由拦截器进行测试了

测试路由拦截器

interceptor.run({
  req: {
    pathname: "getInfo/name/zenos/age/18",
    method: "GET",
  },
});

//name: { params: { name: 'zenos', age: '18' } }

这个测试用例模拟了一个 GET 请求,路径为 “getInfo/name/zenos/age/18″。
这个请求与我们添加的第一个拦截器相匹配(处理特定 GET 请求的拦截器),因此执行了这个拦截器内的回调函数。在回调函数中,我们打印了 ctx.route。在这个例子中,ctx.route 包含了从路径中解析出的参数(name 和 age),因此我们看到了输出中的 “name: { params: { name: ‘zenos’, age: ’18’ } }”。

interceptor.run({
  req: {
    pathname: "getInfo/name/zenos",
    method: "GET",
  },
});

// null

这个测试用例也模拟了一个 GET 请求,路径为 “getInfo/name/zenos”。
但是这个请求的路径与我们添加的拦截器都不匹配,因此没有执行任何回调函数,输出为null

interceptor.run({
  req: {
    pathname: "postAge/18",
    method: "POST",
  },
});

// age: { params: { age: '18' } }

这个测试用例模拟了一个 POST 请求,路径为 “postAge/18”
这个请求与我们添加的第二个拦截器相匹配(处理特定 POST 请求的拦截器),因此执行了这个拦截器内的回调函数。在回调函数中,我们打印了 ctx.route。在这个例子中,ctx.route 包含了从路径中解析出的参数(age),因此我们看到了输出中的 “age: { params: { age: ’18’ } }”

interceptor.run({
  req: {
    pathname: "postAge/18",
    method: "GET",
  },
});
// null

这个测试用例模拟了一个 GET 请求,路径为 “postAge/18″。
这个请求与我们添加的拦截器都不匹配,因为路径虽然匹配第二个拦截器,但请求方法不匹配(第二个拦截器仅处理 POST 请求)。因此没有执行任何回调函数,输出为null

总结

这篇文章讲述了如何手写express路由中间件,并且其中内容涉及到其他两篇文章的代码。
先是回顾了下拦截器和路由校验的代码,之后再手写router的函数,该函数返回一个关键性的拦截器,有了这个拦截器,中间件的手写就成功一大半了。
总体来说思路还是清晰的,还提供了大量的测试用例,有不明白的可以评论区留言,本人活跃在掘金社区。

其他两篇文章链接

原文链接:https://juejin.cn/post/7225139862222389306 作者:慢功夫

(0)
上一篇 2023年4月24日 上午10:20
下一篇 2023年4月24日 上午10:30

相关推荐

发表回复

登录后才能评论