持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第 5 天,点击查看活动详情
王志远,微医前端技术部
先看应用
关于路由,上文只是简单的实现了单一请求匹配单一应答,对于express的路由系统而言,其实还有更重要的两个特性没有体现
- 多匹配机制
- 多回调机制
多匹配机制
即同一个方法下的同一个路径被监听了多次,此时如果上层函数不执行next方法,下层监听函数是不会被执行的;如下
app.flage = false;
app.get("/hello", function (req, res, next) {
app.flage = !app.flage;
function isOk() {
// 判断是否向下执行的逻辑
return app.flage;
}
// res.end('/hello')
console.log("我是第一个监听函数", app.flage);
if (isOk()) {
next();
} else {
res.end("not continue");
}
});
app.get("/hello", function (req, res) {
console.log("我是第二个监听函数");
res.end("/hello");
});
上面代码很简单,就是设定了一个开关,为true时则执行next函数继续向下执行,为false则直接返回,启动服务请求接口,会发现如下现象
控制台显示如下
多回调机制
即回调函数传递多个,同样采用next控制是否执行栈中的下一个
app.get(
"/",
function (req, res, next) {
console.log(1);
next();
},
function (req, res, next) {
console.log(2);
next();
},
function (req, res, next) {
console.log(3);
next();
},
function (req, res, next) {
console.log(4);
next();
res.end("/ matched");
}
);
效果和上文相似,调动next则会继续向下执行
再看实现
理解 next 原理
要实现这些效果,我们先想想这样一个需求
有一个数组,组成数据结构如下,{match(){},handler(next){}} , 我希望当上一个 handler 调用 next 了之后,才执行数组的下一项
如何实现?
了解过CO(就是大神 TJ 实现的【对generator迭代器实现处理】的库,个人认为async+await是这个库的一个官方版)就会发现,逻辑完全一致,都是【将是否继续循环的控制权反转给被循环者本身】
既然要控制,我们自然不能简单的循环了,而是得递归。举例论证:
let stack = [
{
handle(next) {
console.log("1, begin");
setTimeout(() => {
next();
console.log("1, done");
}, 5000);
},
},
{
handle(next) {
console.log("2,done");
},
},
];
那我们需要实现的就是
- 打印
'1, begin' - 过五秒打印
1, done和2, done
首先,我们很自然的能想到,如果next就是下一个函数呢?这样不就满足【执行 next 时执行下一个函数】的需求了吗?于是我们写出如下代码
let index = 0; // 当前正在执行的数组项索引
function dispatch(stack) {
let item = stack[index];
item.handle(stack[++index]); // 先自增 再取值
}
这样,我们就执行了第一项和第二项;接着,我们要递归所有
let index = 0; // 当前正在执行的数组项索引
function dispatch(stack, out) {
if (stack.length === index) return typeof out == "Function" && out(); // 递归结束 ,如果存在结束钩子 则执行
let item = stack[index++]; // 取值完后自增
item.handle(dispatch.bind(null, stack));
}
有了这个基础,我们再去看express的路由next就能更轻松一点了。
实现 express-router
先思考实现思路
- 每个【路径+方法】对应一个路由表(数组),路由表由处理函数组成,形成一个
层(Layer) Router持有路由表(数组),路由表由上面的Layer组成
我们先来看张图
角色梳理
这里面有三个角色
- 路由表:由层组成,是个数组,其层的组成是指【处理函数】
- 路由对象:持有路由表,其路由表的层的组成是指【路径+方法 对应着 路由表】
- 层:为了统一数组组成而抽象出来的数据结构
Route.js
const Layer = require("./layer");
const methods = require("methods");
// 每个层都有一个 route 属性
function Route() {
this.stack = [];
}
// 处理函数
Route.prototype.dispatch = function (req, res, out) {
let idx = 0;
let method = req.method.toLowerCase(); // 获取请求的方法
let dispatch = () => {
if (idx === this.stack.length) {
return out();
}
let layer = this.stack[idx++];
if (layer.method === method) {
// 获取内部的第一层判断方法是否匹配
layer.handle_request(req, res, dispatch);
} else {
dispatch();
}
};
dispatch();
};
methods.forEach((method) => {
// 被调用时 将对应的处理函数数组映射成多个层,并存入路由表对象持有的队列中
Route.prototype[method] = function (handlers) {
handlers.forEach((handler) => {
let layer = new Layer("", handler);
layer.method = method;
this.stack.push(layer);
});
};
});
module.exports = Route;
Layer.js
function Layer(path, handler) {
this.path = path;
this.handler = handler;
this.method = "";
}
Layer.prototype.match = function (pathname) {
return this.path === pathname;
};
Layer.prototype.handler_request = function (req, res, next) {
this.handler((req, res, next));
};
module.exports = Layer;
逻辑梳理
首先,一个应用得有一个对应的路由
function Application() {
this._router = new Router();
}
其次,就是请求的订阅发布了。
订阅:得在应用初始化时进行方法的监听,即app上的get、post等路由方法交由路由处理
methods.forEach((method) => {
Application.prototype[method] = function (path, ...handlers) {
this._router[method](path, handlers);
};
});
进入router层,在方法被触发时做三件事
- 创建对应的层,存入自己的队列中
- 对层进行初始化,赋值上对应的【path+method】和处理函数
- 将处理逻辑交由层里面的路由表(
route)进行处理
function Router() {
this.stack = [];
}
methods.forEach((method) => {
Router.prototype[method] = function (path, handlers) {
// 用户调[method]方法时,传递了多个处理函数
let route = this.route(path); // 构建一个 route
// 将处理逻辑交由层里面的路由表进行处理
route[method](handlers);
};
});
Router.prototype.route = function (path) {
let route = new Route();
let layer = new Layer(path, route.dispatch.bind(route)); // 给当前调用 get 方法 放入一层
layer.route = route; // 每个层都有一个 route 属性
this.stack.push(layer);
return route;
};
发布:得改写应用对象的监听逻辑,将逻辑处理交由路由
Application.prototype.listen = function (port, cb) {
let server = http.createServer((req, res) => {
// 应用提供一个对均无法匹配的兼容处理函数
function done() {
res.end(`Cannot ${req.method} ${req.url}`);
}
this._router.handle(req, res, done);
});
server.listen(...arguments);
};
路由的handle方法只做一件事,递归存储的Layer,所有匹配到的Layer依次执行
重点【依次】,其实就是在说【多匹配机制】,这个时候我们就要采用next的思路了。
Router.prototype.handle = function (req, res, out) {
let { pathname } = url.parse(req.url); // 获取请求的路径
let idx = 0;
let dispatch = () => {
// exress 需要通过 next 函数来迭代
if (idx === this.stack.length) {
return out();
}
let layer = this.stack[idx++];
if (layer.match(pathname)) {
layer.handler_request(req, res, dispatch);
} else {
dispatch();
}
};
dispatch();
};
而Layer的handler_request其实就是调用了一下初始化Layer时存入的handle函数,回想一下上面,那不就是路由表的next的吗?这就通了,相当了是:Application -> Router -> Layer -> Route -> Layer 的处理逻辑
Layer 间会需要一次next逻辑,Route 间也会需要一次,此时需要注意的是,Layer 的out函数是 express 在listen中提供的done方法,而 Route 的out函数则是 Layer 的 next 方法
至此,我们就基本理解了epxress的运作流程啦