express源码分析之Router
express作为nodejs平台下非常流行的web框架,相信大家都对其已经很熟悉了,对于express的使用这里不再多说,如有需要可以移步到www.expressjs.com自行查看express的官方文档,今天主要是想说下express的路由机制。
最近抽时间看了下express的源码,看完源码体会最深刻的还是express的路由机制,感觉搞懂了express的路由就算是基本搞懂了express,而express的路由机制都是router模块来实现,所以在这里对express的router模块实现进行一下简单的整理,所有理解都来自自己对源码的理解,如有不对的地方,还请各位多多拍砖。
好了,废话不多说了,进入正题,首先先了解一下express源码的目录结构,如下图:
application.js为express的主文件,express.js对application.js进行了包装,对外提供各种API,这里我们不多做说明,我们今天要说的就是router目录下的内容,express关于路由的具体实现都是由这个目录完成。我们先看一个简单的express路由的例子:
var app = express();
app.get('/hello',function(req,res){
res.send('hello everyone!!!');});
上边就是一个最简单的express路由的例子,将path为 ‘/hello’ 的请求路由到当前的处理函数,并返回 ‘hello everyone!!!’ ,那么我们来一起看看,app.get()何实现的,通过查看代码我们发现源码里并没有app.get()的实现,但仔细找找你会在application.js中发现如下的代码:
methods.forEach(function(method){
app[method]=function(path){if(method ==='get'&& arguments.length ===1){// app.get(setting)returnthis.set(path);}this.lazyrouter();var route =this._router.route(path);
route[method].apply(route, slice.call(arguments,1));returnthis;};});
(⊙o⊙)哦,隐藏的好深,原来express对get,post等方法的添加都是动态的,methods来自methods这个模块,他提供了和nodejs http.METHODS 相似的东西,返回了http协议的所有method,这样一个循环搞定了所有method函数的定义,赞一个。
接下来我们主要分析下函数内部的实现,首先判断如果method等于get,并且参数的长度是1,则直接返回this.set(path),大家查看express官网的API就可以发现,app.get()函数其实实现了两种功能,如果参数长度是1,则返回app.set()定义的变量,如果参数长度大于1,则进行路由处理。
继续往下看,this.lazyrouter(),从名字来看,好像是懒加载router,那我们看看源码:
app.lazyrouter =function lazyrouter(){if(!this._router){this._router =newRouter({
caseSensitive:this.enabled('case sensitive routing'),
strict:this.enabled('strict routing')});this._router.use(query(this.get('query parser fn')));this._router.use(middleware.init(this));}};
果然是,如果_router不存在,就new一个Router出来,而这个Router就是我们刚才在目录结构中看到的router目录,也就是今天的主角Router模块。继续上边的代码,加载完_router之后,执行了this._router.route(path)这样一行代码,那这行代码有做了什么呢,我们再继续往下挖,我们在router目录下的index.js中找到了它的实现:
proto.route =function route(path){var route =newRoute(path);var layer =newLayer(path,{
sensitive:this.caseSensitive,
strict:this.strict,end:true}, route.dispatch.bind(route));
layer.route = route;this.stack.push(layer);return route;};
我们可以看到,这里new了一个Route对象,并且new了一个Layer对象,然后将Route对象赋值给layer.route,最后将这个Layer添加到stack数组中。在这里我们先不对Layer进行说明,后边会有专门的介绍,我们先来看看Route,那这个Route又是什么呢,它和Router模块有什么关系呢,我来说下我的理解:
Route模块对应的是route.js,主要是来处理路由信息的,每条路由都会生成一个Route实例。而Router模块对应的是index.js,Router是一个路由的集合,在Router模块下可以定义多个路由,也就是说,一个Router模块会包含多个Route模块。通过上边的代码我们已经知道,每个express创建的实例都会懒加载一个_router来进行路由处理,这个_router就是一个Router模块。
理解了Route和Router的关系,感觉一下子清爽了有木有,O(∩_∩)O哈哈~~~
好了,我们接着看代码,拿到route对象之后,通过apply的方式调用了route的对应method函数,假如我们现在使用的是get函数,那现在method就等于get。看到这里大家就会发现,express实例在处理路由的时候,会先创建一个Router对象,然后用Router对象和对应的path来生成一个Route对象,最后由Route对象来处理具体的路由实现。
好了,那接下来我们继续深入研究,看看route.method究竟做了什么,我们找到route.js文件,发现如下的代码:
methods.forEach(function(method){Route.prototype[method]=function(){var handles = flatten(slice.call(arguments));for(var i =0; i < handles.length; i++){var handle = handles[i];if(typeof handle !=='function'){var type = toString.call(handle);var msg ='Route.'+ method +'() requires callback functions but got a '+ type;thrownewError(msg);}
debug('%s %s', method,this.path);var layer =Layer('/',{}, handle);
layer.method = method;this.methods[method]=true;this.stack.push(layer);}returnthis;};});
啊啊啊,原来route和application运用了同样的技巧,通过循环methods来动态添加method函数,我们直接看函数内部实现,首先通过入参获取到handles,这里的handles就是我们定义的路由中间件函数,这里我们可以看到是一个数组,所以我们可以给一个路由添加多个中间件函数。接下来循环handles,在每个循环中利用handle来创建一个Layer对象,然后将Layer对象push到stack中去,这个stack其实是Route内部维护的一个数组,用来存放所有的Layer对象。现在你一定想这道这个Layer到底是什么东西,那我们继续往下看,看看layer.js的源代码:
functionLayer(path, options, fn){if(!(thisinstanceofLayer)){returnnewLayer(path, options, fn);}
debug('new %s', path);var opts = options ||{};this.handle = fn;this.name = fn.name ||'<anonymous>';this.params=undefined;this.path =undefined;this.regexp = pathRegexp(path,this.keys =[], opts);if(path ==='/'&& opts.end===false){this.regexp.fast_slash =true;}}
上边是Layer的构造函数,我们可以看到这里定义handle,params,path和regexp等几个主要的属性:
- 其中最重要的就是handle,它就是我们刚刚在route中创建Layer对象传入的中间件函数。
- params其实就是req.params,至于如何实现的我们可以以后再做探讨,今天先不做说明。
- path就是我们定义路由时传入的path。
- regexp对于Layer来说是比较重要的一个属性,因为下边进行路由匹配的时候就是靠它来搞定的,而它的值是由pathRegexp得来的,其实这个pathRegexp对应的是一个第三方模块path-to-regexp,它的功能是将path转换成regexp,具体用法大家可以自行查看。
看完属性,我们再来看看Layer有什么方法:
Layer.prototype.match =function match(path){if(path ==null){// no path, nothing matchesthis.params=undefined;this.path =undefined;returnfalse;}if(this.regexp.fast_slash){// fast path non-ending match for / (everything matches)this.params={};this.path ='';returntrue;}var m =this.regexp.exec(path);if(!m){this.params=undefined;this.path =undefined;returnfalse;}// store valuesthis.params={};this.path = m[0];var keys =this.keys;varparams=this.params;for(var i =1; i < m.length; i++){var key = keys[i -1];var prop = key.name;var val = decode_param(m[i]);if(val !==undefined||!(hasOwnProperty.call(params, prop))){params[prop]= val;}}returntrue;};
match函数主要用来匹配path的,当我们向express发送一个http请求时,当前请求对应的是哪个路由,就是通过这个match函数来判断的,如果path中带有参数,match还会把参数提取出来赋值给params,所以说match是整个路由中很重要的一点。
Layer.prototype.handle_error =function handle_error(error, req, res,next){var fn =this.handle;if(fn.length !==4){// not a standard error handlerreturnnext(error);}try{
fn(error, req, res,next);}catch(err){next(err);}};
这个是错误处理函数,专门用来处理错误的。
Layer.prototype.handle_request =function handle(req, res,next){var fn =this.handle;if(fn.length >3){// not a standard request handlerreturnnext();}try{
fn(req, res,next);}catch(err){next(err);}};
从上边的代码我们可以看到调用了fn,而这个fn就是layer的handle属性,就是我们定义路由时传入的路由中间件,到这里我们总算找到了我们的路由中间件被执行的地方,是不是很兴奋。好了,到这里我们已经看完了Layer的代码,但Layer到底是做什么的呢,它和Route之间又有什么千丝万缕的联系呢,说说我的理解:
每一个Layer对应一个中间件函数,Layer存储了每个路由的path和handle等信息,并且实现了match和handle的功能。而从前边我们已经知道,每个Route都会维护一个Layer数组,所有可以发现Route和Layer是一对多的关系,每个Route代表一个路由,而每个Layer对应的是路由的每一个中间件函数。
讲完了Route和Layer的关系,我们再来回头看看Router和Layer的关系,我们再来看看index.js中prop.route的代码:
proto.route =function route(path){var route =newRoute(path);var layer =newLayer(path,{
sensitive:this.caseSensitive,
strict:this.strict,end:true}, route.dispatch.bind(route));
layer.route = route;this.stack.push(layer);return route;};
从代码我们可以看出来Router每次添加一个route,都会把route包装到layer中,并且将layer添加到自己的stack中,那为什么要把route包装到layer中呢,前边我们已经仔细研究了Layer模块的代码,我们发现Layer具有match和handle的功能,这样我们就可以通过Layer的match来进行route的匹配了。这里有一个关键点我们需要特别讲解下,上边的代码中在创建Layer对象的时候传入的handle函数为route.dispatch.bind(route),我们来看看route.js中的route.dispatch:
Route.prototype.dispatch =function dispatch(req, res,done){var idx =0;var stack =this.stack;if(stack.length ===0){returndone();}var method = req.method.toLowerCase();if(method ==='head'&&!this.methods['head']){
method ='get';}
req.route =this;next();functionnext(err){if(err && err ==='route'){returndone();}var layer = stack[idx++];if(!layer){returndone(err);}if(layer.method && layer.method !== method){returnnext(err);}if(err){
layer.handle_error(err, req, res,next);}else{
layer.handle_request(req, res,next);}}};
我们发现dispatch中通过next()获取stack中的每一个layer来执行相应的路由中间件,这样就保证了我们定义在路由上的多个中间件函数被按照定义的顺序依次执行。到这里我们已经知道了单个路由是被如何执行的,那我们定义的多个路由之间又是如何被依次执行的呢,现在我们来看看index.js中的handle函数:
proto.handle =function handle(req, res,out){// middleware and routesvar stack =self.stack;next();functionnext(err){// find next matching layervar layer;var match;var route;while(match !==true&& idx < stack.length){
layer = stack[idx++];
match = matchLayer(layer, path);
route = layer.route;if(match !==true){continue;}if(!route){// process non-route handlers normallycontinue;}}// no matchif(match !==true){returndone(layerError);}// this should be done for the layerself.process_params(layer, paramcalled, req, res,function(err){if(err){returnnext(layerError || err);}if(route){return layer.handle_request(req, res,next);}
trim_prefix(layer, layerError, layerPath, path);});}};
上边的代码我进行了处理,删除了一些逻辑,只留下关键部分。从上边的代码我们可以看出,这里也是利用next(),来处理stack中的每一个Layer,这里的stack是Router的stack,stack中存贮了多个route对应的layer,获取到每个layer对象之后,用请求的path与layer进行匹配,此处匹配用的是layer.match,如果能匹配到对应的layer,则获得layer.route,如果route不为空则执行对应的layer.handle_request(),如果route为空说明这个layer是通过use()添加的非路由中间件,需要特别说明的是,如果通过use()添加的非路由中间件没有指定path,则会在layer.match中默认返回true,也就是说,没有指定path的非路由中间件会匹配所有的http请求。
到这里,我们基本已经说明了router相关的所有内容,想必看到这里你一定会有点晕,我们接下来来重新梳理一下。看看express究竟是如何对http请求进行路由的。
当客户端发送一个http请求后,会先进入express实例对象对应的router.handle函数中,router.handle函数会通过next()遍历stack中的每一个layer进行match,如果match返回true,则获取layer.route,执行route.dispatch函数,route.dispatch同样是通过next()遍历stack中的每一个layer,然后执行layer.handle_request,也就是调用中间件函数。直到所有的中间件函数被执行完毕,整个路由处理结束。
express源码分析之Router的更多相关文章
- express源码分析---merge-descriptors
在express.js里 我们看到这样的代码: 顾名思义,我们知道是将proto,EventEmitter.prototype 上的属性复制一份给app上. 那它具体实现的原理是怎么样的? 'use ...
- express源码分析
参考:http://www.cnblogs.com/ginobilee/p/6906204.html https://www.cnblogs.com/zhusheng2008/p/5264096.ht ...
- Appium Server 源码分析之启动运行Express http服务器
通过上一个系列Appium Android Bootstrap源码分析我们了解到了appium在安卓目标机器上是如何通过bootstrap这个服务来接收appium从pc端发送过来的命令,并最终使用u ...
- Dubbo 源码分析 - 集群容错之 Router
1. 简介 上一篇文章分析了集群容错的第一部分 -- 服务目录 Directory.服务目录在刷新 Invoker 列表的过程中,会通过 Router 进行服务路由.上一篇文章关于服务路由相关逻辑没有 ...
- .5-浅析express源码之Router模块(1)-默认中间件
模块application已经完结,开始讲Router路由部分. 切入口仍然在application模块中,方法就是那个随处可见的lazyrouter. 基本上除了初始化init方法,其余的app.u ...
- nodejs的Express框架源码分析、工作流程分析
nodejs的Express框架源码分析.工作流程分析 1.Express的编写流程 2.Express关键api的使用及其作用分析 app.use(middleware); connect pack ...
- 从express源码中探析其路由机制
引言 在web开发中,一个简化的处理流程就是:客户端发起请求,然后服务端进行处理,最后返回相关数据.不管对于哪种语言哪种框架,除去细节的处理,简化后的模型都是一样的.客户端要发起请求,首先需要一个标识 ...
- 使用react全家桶制作博客后台管理系统 网站PWA升级 移动端常见问题处理 循序渐进学.Net Core Web Api开发系列【4】:前端访问WebApi [Abp 源码分析]四、模块配置 [Abp 源码分析]三、依赖注入
使用react全家桶制作博客后台管理系统 前面的话 笔者在做一个完整的博客上线项目,包括前台.后台.后端接口和服务器配置.本文将详细介绍使用react全家桶制作的博客后台管理系统 概述 该项目是基 ...
- Express4.x之中间件与路由详解及源码分析
Application.use() Application.router() express核心源码模拟 一.express.use() 1.1app.use([path,] callback [, ...
随机推荐
- HTML网页设计基础笔记 • 【第3章 表单】
全部章节 >>>> 本章目录 3.1 表单 3.1.1 表单概述 3.1.1 表单概述(续) 3.1.2 表单标签 3.1.3 表单数据的提交方式 3.2 输入框和按钮 3 ...
- 编写Java程序,利用List维护用户信息
返回本章节 返回作业目录 需求说明: 将新增的用户信息添加到List集合. 用户信息包括用户编号.姓名和性别. 按照姓名和性别查找用户信息. 实现思路: 创建类UserInfo,在该类中定义3个Str ...
- Roslyn+T4+EnvDTE项目完全自动化(3) ——生成c++代码
C++语法复杂,写一个示例通过T4可生成c++代码 需求:数据库,生成c++增,删,改,查代码 数据生成c++类,包含所有字段 自动识别数据的主键Key 查询生成赋值类字段,类型转换 通过类自动生成s ...
- 使用yum安装php*时报错的解决办法
# yum -y install php* 注意: php53-odbc64-5.3.3-2.el5.x86_64 from base has depsolving problems --> ...
- textarea换行符转换
/** * @description textarea换行符转指定字符 * @param str:要放到textarea的字符串 * @param code:要转换成换行的字符,默认为',' */ e ...
- Centos7安装erlang以及RabbitMQ Centos启动rabbitmq
本文使用版本: rabbitmq-server-3.8.3-1.el7.noarch.rpm Centos7 erlang 22.3.1 在线安装 yum install esl-erlan ...
- Blinn-Phong反射模型实践(web实现)
Blinn-Phong反射模型实践(web实现) games101 第四次作业 最终完成带贴图的 Blinn-Phong 模型,产生光照效果 完成了 不带贴图的 Blinn-Phone 反射模型 带贴 ...
- 内核内存分配器SLAB和SLUB
内核分配器的功能 在操作系统管理的虚拟内存中,用于内存管理的最小单位是页,大多数传统的架构是4KB.由于进程每次申请分配4KB是不现实的,比如分配几个字节或几十个字节,这时需要中间机制来管理页面的微型 ...
- ctfshow web2 web3
ctfshow web2 1.手动注入题.先用万能密码admin' or 1=1%23,有回显 2.union select注入,2处有回显 3.依次查找数据库.表.字段 得到flag ctfshow ...
- 【刷题-LeetCode】121 Best Time to Buy and Sell Stock
Best Time to Buy and Sell Stock Say you have an array for which the ith element is the price of a gi ...