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等几个主要的属性:

  1. 其中最重要的就是handle,它就是我们刚刚在route中创建Layer对象传入的中间件函数。
  2. params其实就是req.params,至于如何实现的我们可以以后再做探讨,今天先不做说明。
  3. path就是我们定义路由时传入的path。
  4. 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,也就是调用中间件函数。直到所有的中间件函数被执行完毕,整个路由处理结束。

原文地址:https://www.cnblogs.com/ysk123/p/9818569.html

时间: 2024-08-28 08:37:17

express源码分析之Router的相关文章

五)CodeIgniter源码分析之Router.php

1 <?php if ( ! defined('BASEPATH')) exit('No direct script access allowed'); 2 3 // ------------------------------------------------------------------------ 4 5 /** 6 * Router Class 7 */ 8 class CI_Router { 9 10 /** 11 * Config class 12 */ 13 var $co

基于RTL819X实现的Router/AP的源码分析[一]

*************************************************************************************************************************** 作者:EasyWave                                                                               时间:2015.01.11 类别:路由器类-基于RTL819X实现的Ro

Dubbo 源码分析 - 集群容错之 Router

1. 简介 上一篇文章分析了集群容错的第一部分 – 服务目录 Directory.服务目录在刷新 Invoker 列表的过程中,会通过 Router 进行服务路由.上一篇文章关于服务路由相关逻辑没有细致分析,一笔带过了,本篇文章将对此进行详细的分析.首先,先来介绍一下服务目录是什么.服务路由包含一条路由规则,路由规则决定了服务消费者的调用目标,即规定了服务消费者可调用哪些服务提供者.Dubbo 目前提供了三种服务路由实现,分别为条件路由 ConditionRouter.脚本路由 ScriptRo

从express源码中探析其路由机制

引言 在web开发中,一个简化的处理流程就是:客户端发起请求,然后服务端进行处理,最后返回相关数据.不管对于哪种语言哪种框架,除去细节的处理,简化后的模型都是一样的.客户端要发起请求,首先需要一个标识,通常情况下是URL,通过这个标识将请求发送给服务端的某个具体处理程序,在这个过程中,请求可能会经历一系列全局处理,比如验证.授权.URL解析等,然后定位到某个处理程序进行业务处理,最后将生成的数据返回客户端,客户端将数据结合视图模版呈现出合适的样式.这个过程涉及到的模块比较多,本文只探讨前半部分,

keystone源码分析(一)——Paste Deploy的应用

本keystone源码分析系列基于Juno版Keystone,于2014年10月16日随Juno版OpenStack发布. Keystone作为OpenStack中的身份管理与授权模块,主要实现系统用户的身份认证.基于角色的授权管理.其他OpenStack服务的地址发现和安全策略管理等功能.Keystone作为开源云系统OpenStack中至关重要的组成部分,与OpenStack中几乎所有的其他服务(如Nova, Glance, Neutron等)都有着密切的联系.同时,Keystone作为开源

【Cocos2d-x】源码分析之 2d/ui/Widget

从今天开始 咱也模仿 红孩儿这些大牛分析源码 ,由于水平有限 不对之处欢迎狂喷.哈哈. #ifndef __UIWIDGET_H__ #define __UIWIDGET_H__ #include "ui/CCProtectedNode.h" #include "ui/UILayoutDefine.h" #include "ui/UILayoutParameter.h" #include "ui/GUIDefine.h" NS

Android 4.2 Wifi Display 之 Settings 源码分析(一)

一.简单背景 简单背景:随着无线互联的深入,不管是蓝牙.WIFI或者各种基于此的规范不管是UPNP还是DLNA都随着用户的需求得到了很大的发展,google 自从android 4.0引入wifi direct后,又在11月份公布的android 4.2中引入了Miracast无线显示共享,其协议在此可以下载.具体的协议部分内容比较多,本人由于水平有限,就不在这里罗列协议的内容了,只附上一份架构图供大家对其有个大致的印象. 英文缩写对应如下: HIDC: Human Interface Devi

solr源码分析之solrclound

一.简介 SolrCloud是Solr4.0版本以后基于Solr和Zookeeper的分布式搜索方案.SolrCloud是Solr的基于Zookeeper一种部署方式.Solr可以以多种方式部署,例如单机方式,多机Master-Slaver方式. 二.特色功能 SolrCloud有几个特色功能: 集中式的配置信息使用ZK进行集中配置.启动时可以指定把Solr的相关配置文件上传Zookeeper,多机器共用.这些ZK中的配置不会再拿到本地缓存,Solr直接读取ZK中的配置信息.配置文件的变动,所有

Backbone.js源码分析(珍藏版)

源码分析珍藏,方便下次阅读! // Backbone.js 0.9.2 // (c) 2010-2012 Jeremy Ashkenas, DocumentCloud Inc. // Backbone may be freely distributed under the MIT license. // For all details and documentation: // http://backbonejs.org (function () { // 创建一个全局对象, 在浏览器中表示为w