在之前的两篇文中聊过了手写洋葱模型的拦截器,还有参数路由的正则校验。这篇文章我将这两个内容结合起来,手写一个路由中间件。
拦截器
让我们回忆一下拦截器:
实现代码
// 定义一个异步函数,用来实现延时效果
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
效果就像下面这张图
这里只是简略的说下用法,关于拦截器的具体详细代码,请看这篇文章: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 作者:慢功夫