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

引言

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

  为了作为对比,先简单介绍一下asp.net webform和asp.net mvc是如何实现路由的。

  asp.net webform比较特殊,由于是postback原理,定位处理程序的过程与mvc是不一样的,对URL的格式没有严格的要求,url是直接对应后台文件的,aspx中的服务器表单默认是发送到对应的aspx.cs文件,它的定位是借助aspx页面中的两个隐藏域(__EVENTTARGET和__EVENTARGUMENT)以及IPostBackEventHandler接口来实现的,通过这两样东西就可以直接定位到某个具体方法中,通常是某个控件的某个事件处理程序。也就是说,在webform中,url仅能将请求定位到类中,要定位到真正的处理程序(方法)中,还需借助其他手段。

  asp.net mvc与webform不同,url不再对应到后台文件,那么就必须通过某种手段来解析url,mvc中的后台处理程序称为Action,位于Controller类中,为了使url能够定位到action,mvc中的url有比较严格的格式要求,在url中需要包含controller和action,这样后台就可以通过反射来动态生成controller实例然后调用对应的action。也就是说,在mvc中完全依靠url来实现后台处理程序的定位。

  通过上面两种方式的分析,我们发现url是不是指向文件是无所谓的,但最终都是要根据其定位到某个具体的处理程序,也就是url到handler有个路由处理过程,只不过不同的框架有不同的处理方法。在express框架的使用过程中,隐隐约约感觉其路由过程如下图所示:

到底是不是这样呢?

源码分析

  我们知道,在使用express的时候,我们可以通过如下的方式来注册路由:

app.get("/",function(req,res){
    res.send("hello啊");
});

从表面上看,get方法可以将url中的path与后台处理程序关联起来,为了弄清楚这个过程,我们可以到application.js文件中查看源码。第一次看了一眼,发现里面居然没有这个方法,app.get(),app.post()等都没找到,仔细再一看,发现了如下方法:

methods.forEach(function(method){
  app[method] = function(path){
    if (‘get‘ == method && 1 == arguments.length) return this.set(path);  //get的特殊处理,只有一个参数时会获取app.settings[path]

   this.lazyrouter();  

    var route = this._router.route(path);
    route[method].apply(route, slice.call(arguments, 1));   //取出第二个参数,即:处理程序,传入route[method]方法中
    return this;
  };
});

原来,这些方法都是动态添加的。methods是一个数组,里面存放了一系列web请求方法,以上方法通过对其进行遍历,给app添加了与请求方法同名的一系列方法,即:app.get()、app.post()、app.put()等,在这些方法中,首先通过调用lazyrouter实例化一个Router对象,然后调用this._router.route方法实例化一个Route对象,最后调用route[method]方法并传入对应的处理程序完成path与handler的关联。

  在这个方法中需要注意以下几点:

  1. lazyrouter方法只会在首次调用时实例化Router对象,然后将其赋值给app._router字段
  2. 要注意Router与Route的区别,Router可以看作是一个中间件容器,不仅可以存放路由中间件(Route),还可以存放其他中间件,在lazyrouter方法中实例化Router后会首先添加两个中间件:query和init;而Route仅仅是路由中间件,封装了路由信息。Router和Route都各自维护了一个stack数组,该数组就是用来存放中间件和路由的。

  这里先声明一下,本文提到的路由容器(Router)代表“router/index.js”文件的到导出对象,路由中间件(Route)代表“router/route.js”文件的导出对象,app代表“application.js”的导出对象。

  Router和Route的stack是有差别的,这个差别主要体现在存放的layer(layer是用来封装中间件的一个数据结构)不太一样,

由于Router.stack中存放的中间件包括但不限于路由中间件,而只有路由中间件的执行才会依赖与请求method,因此Router.stack里的layer没有method属性,而是将其动态添加(layer的定义中没有method字段)到了Route.stack的layer中;layer.route字段也是动态添加的,可以通过该字段来判断中间件是否是路由中间件。

可以通过两种方式添加中间件:app.use和app[method],前者用来添加非路由中间件,后者添加路由中间件,这两种添加方式都在内部调用了Router的相关方法来实现:

//添加非路由中间件proto.use = function use(fn) {
  /* 此处略去部分代码 */

  callbacks.forEach(function (fn) {
    if (typeof fn !== ‘function‘) {
      throw new TypeError(‘Router.use() requires middleware function but got a ‘ + gettype(fn));
    }

    // add the middleware
    debug(‘use %s %s‘, path, fn.name || ‘<anonymous>‘);
    ////实例化layer对象并进行初始化
    var layer = new Layer(path, {
      sensitive: this.caseSensitive,
      strict: false,
      end: false
    }, fn);
    //非路由中间件,该字段赋值为undefined
    layer.route = undefined;

    this.stack.push(layer);
  }, this);

  return this;
};

//添加路由中间件
proto.route = function(path){
  //实例化路由对象
  var route = new Route(path);
  //实例化layer对象并进行初始化
  var layer = new Layer(path, {
    sensitive: this.caseSensitive,
    strict: this.strict,
    end: true
  }, route.dispatch.bind(route));
  //指向刚实例化的路由对象(非常重要),通过该字段将Router和Route关联来起来
  layer.route = route;

  this.stack.push(layer);
  return route;
};

对于路由中间件,路由容器中的stack(Router.stack)里面的layer通过route字段指向了路由对象,那么这样一来,Router.stack就和Route.stack发生了关联,关联后的示意模型如下图所示:

在运行过程中,路由容器(Router)只会有一个实例,而路由中间件会在每次调用app.route、app.use或app[method]的时候生成一个路由对象,在添加路由中间件的时候路由容器相当于是一个代理,Router[method]实际上是在内部调用了Route[method]来实现路由添加的,路由容器中有一个route方法,相当于是路由对象创建工厂。通过添加一个个的中间件,在处理请求的时候会按照添加的顺序逐个调用,如果遇到路由中间件,会逐个调用该路由对象中stack数组里存放的handler,这就是express的流式处理,是不是有点类似asp.net中的管道模型,调用过程如下图所示:

  我们可以做个测试,在终端执行"express -e expresstest"命令(需要先安装express和express-generator),然后在"expresstest/app.js"文件中添加下面代码:

//添加非路由中间件app.use(‘/test‘,function(req,res,next){console.log("app.use(‘/test‘) handler1");next()},function(req,res,next){console.log("app.use(‘/test‘) handler2");next()});

var r = app.route(‘/test‘); //创建路由对象,并通过route[method]来添加路由中间件
r.get(function(req,res,next){
    console.log("route.get(‘/test‘) handler1");
    next();
}).get(function(req,res,next){
    console.log("route.get(‘/test‘) handler2");
    next();
});

/* 还可以这么写,直接传入多个functionr.get(function(req,res,next){    console.log("route.get(‘/test‘) handler1");    next();},function(req,res,next){    console.log("route.get(‘/test‘) handler2");    next();});*/

/*
或者这么写,直接传入function数组,可以是多维数组
r.get([function(req,res,next){    console.log("route.get(‘/test‘) handler1");    next();},[function(req,res,next){    console.log("route.get(‘/test‘) handler2");    next();},function(req,res,next){    console.log("route.get(‘/test‘) handler3");    next();}]]);*/
app.get(‘/test‘,function(req,res,next){ //通过app[method]来添加路由中间件
    console.log("app.get(‘/test‘) handler1");
    next();
}).get(‘/test‘,function(req,res){  console.log("app.get(‘/test‘) handler2");   res.end();});

在终端中输入"cd expresstest"、"npm start"来启动express,然后在浏览器中输入"http://localhost:3000/test",我们发现在终端中输出的内容与我们之前分析的完全一致,如下图所示:

在示例中,我们通过app[method]和route[method]这两种方式来添加了路由中间件,从源码中可以看出这里有个很大的区别,app[method]方法中有这么一句代码:var route = this._router.route(path);,this._router.route()方法内部会实例化一个Route并返回,也就是说,每次调用app[method]都会重新创建一个新的Route对象,后面的处理程序就会添加到这个新Route对象的stack中,虽然可以通过链式写法来添加路由中间件,但每个处理程序都不在一个stack中(不过这样也不影响程序的执行);而route[method]则不同,该方法添加完路由中间件后会返回自身,在路由对象上调用method方法会把所有的处理程序全部添加在该对象的stack中,不过在使用route[method]之前需要先手动实例化一个Route对象。route[method]方法的处理手段与app[method]有所不同,不仅可以同时处理多个function参数,并且通过这句代码:var callbacks = utils.flatten([].slice.call(arguments));可以将arguments中的多位数组转换为一维数组,这样就使得参数的传入变得非常灵活。

  中间件的添加主要依靠application.js、router/index.js和router/route.js这三个文件的导出对象(app,Router,Route)相互调用完成的,从三个文件的require上来看,app依赖Router,Router依赖Route,下面是app.use的代码:

app.use = function use(fn) {
  var offset = 0;  //该变量用来在arguments中定位handler的起始位置,在没有传入path的时候,handler是arguments的第一个元素,所以为0
  var path = ‘/‘;  //没有传入path参数的时候,默认为"/"

  // default path to ‘/‘
  // disambiguate app.use([fn])
  if (typeof fn !== ‘function‘) {
    var arg = fn;

    while (Array.isArray(arg) && arg.length !== 0) { //如果第一个参数是数组的话,取出数组第一个元素
      arg = arg[0];
    }

    // first arg is the path
    if (typeof arg !== ‘function‘) {  //如果arg不是function,将其作为path来处理
      offset = 1;
      path = fn;
    }
  }

  var fns = flatten(slice.call(arguments, offset)); //从参数中取出处理函数列表

  if (fns.length === 0) {
    throw new TypeError(‘app.use() requires middleware functions‘);
  }

  // setup router
  this.lazyrouter();  //实例化Router,并将其赋值给this._router
  var router = this._router;

  fns.forEach(function (fn) {  //遍历参数中的function,逐个调用router.use,从这个地方可以看出,app.use()中可以传入多个function,将其都添加到stack中
    // non-express app
    if (!fn || !fn.handle || !fn.set) {
      return router.use(path, fn);
    }

    debug(‘.use app under %s‘, path);
    fn.mountpath = path;
    fn.parent = this;

    // restore .app property on req and res
    router.use(path, function mounted_app(req, res, next) {
      var orig = req.app;
      fn.handle(req, res, function (err) {
        req.__proto__ = orig.request;
        res.__proto__ = orig.response;
        next(err);
      });
    });

    // mounted an app
    fn.emit(‘mount‘, this);
  }, this);

  return this;
};

从代码中可以看出,调用app.use的时候可以传入多个function,如果给指定路径添加function的话,路径要作为第一个参数,比如:

app.use(‘/test‘,function(req,res,next){console.log("use1");next()},function(req,res,next){console.log("use2");next()});

app.use通过调用this._router.use来实现非路由中间件的添加。this._router.use的代码上面已经贴出,path的判断与app.use前面部分一样,在该该方法中实例化layer并赋值,然后加入this._router.stack中。

app[method]的代码上面已经说过,这里就不再说了,下面是app.use和app[method]的执行流程,从图中可以看出三个文件的联系:

对于Router还有一点需要说明一下,在其构造函数中有这么一句代码:router.__proto__ = proto;,通过router的__proto__属性将其原型指向了proto对象,从而获得了proto中定义的各个方法。

总结

  啰啰嗦嗦了这么多,最后总结一下吧。

  1. 首先对于引言中的那个路由图,基本上是对的,只不过express要面临各种中间件的添加,所以将path与handler做了进一步的封装(Layer),然后将layer保存在Router.stack数组中。
  2. app.use用来添加非路由中间件,app[method]添加路由中间件,中间件的添加需要借助Router和Route来完成,app相当于是facade,对添加细节进行了包装。
  3. Router可以看做是一个存放了中间件的容器。对于里面存放的路由中间件,Router.stack中的layer有个route属性指向了对应的路由对象,从而将Router.stack与Route.stack关联起来,可以通过Router遍历到路由对象的各个处理程序。

  

时间: 2024-10-27 13:19:42

从express源码中探析其路由机制的相关文章

Redis源码中的CRC校验码(crc16、crc64)原理浅析

在阅读Redis源码的时候,看到了两个文件:crc16.c.crc64.c.下面我抛砖引玉,简析一下原理. CRC即循环冗余校验码,是信息系统中一种常见的检错码.大学课程中的"计算机网络"."计算机组成"等课程中都有提及.我们可能都了解它的数学原理,在试卷上手工计算一个CRC校验码,并不是难事.但是计算机不是人,现实世界中的数学原理需要转化为计算机算法才能实现目的.实际上作为计算机专业背景人并不会经常使用或接触到CRC的计算机算法实现的原理,通常是电子学科背景的人士

从Android4.0源码中提取的截图实现(在当前activity中有效,不能全局截图)

原文:http://blog.csdn.net/xu_fu/article/details/39268771 从这个大神的博客看到了这篇文章,感觉写的挺好的.挺实用的功能.虽然是从源码中提取的,但是看得出费了一番心思.而且讲解的很透彻.我这里补充的是这个仅仅能在一个acitvity中使用,不能实现在服务中截图.getWindow().getDecorView()这个方法得到的是当前根视图,这样等于得到了当前屏幕展示的图片,截取即可.这里为了方便没有做图片保存的工作.仅仅作为演示. 一.使用方式

The Independent JPEG Group&#39;s JPEG software Android源码中 JPEG的ReadMe文件

The Independent JPEG Group's JPEG software========================================== README for release 6b of 27-Mar-1998==================================== This distribution contains the sixth public release of the Independent JPEGGroup's free JPEG

express源码分析之Router

express作为nodejs平台下非常流行的web框架,相信大家都对其已经很熟悉了,对于express的使用这里不再多说,如有需要可以移步到www.expressjs.com自行查看express的官方文档,今天主要是想说下express的路由机制. 最近抽时间看了下express的源码,看完源码体会最深刻的还是express的路由机制,感觉搞懂了express的路由就算是基本搞懂了express,而express的路由机制都是router模块来实现,所以在这里对express的router模

Express源码解析之next

最近公司在使用node做前后端分离,采用的web框架是express,所以对express框架进行了深入的了解,前段时间写了篇关于express路由的文章,但是在那篇文章中貌似少了一个很重要的内容,就是express的next,所以今天单独来说说express的next. 关于next主要从三点来进行说明: next的作用是什么? 我们应该在何时使用next? next的内部实现机制是什么? Next的作用 我们在定义express中间件函数的时候都会将第三个参数定义为next,这个next就是

Redis源码中探秘SHA-1算法原理及其编程实现

导读 SHA-1算法是第一代"安全散列算法"的缩写,其本质就是一个Hash算法.SHA系列标准主要用于数字签名,生成消息摘要,曾被认为是MD5算法的后继者.如今SHA家族已经出现了5个算法.Redis使用的是SHA-1,它能将一个最大2^64比特的消息,转换成一串160位的消息摘要,并能保证任何两组不同的消息产生的消息摘要是不同的.虽然SHA1于早年间也传出了破解之道,但作为SHA家族的第一代算法,对我们仍然很具有学习价值和指导意义. SHA-1算法的详细内容可以参考官方的RFC:ht

从源码中浅析Android中如何利用attrs和styles定义控件

一直有个问题就是,Android中是如何通过布局文件,就能实现控件效果的不同呢?比如在布局文件中,我设置了一个TextView,给它设置了textColor,它就能够改变这个TextView的文本的颜色.这是如何做到的呢?我们分3个部分来看这个问题1.attrs.xml  2.styles.xml  3.看组件的源码. 1.attrs.xml: 我们知道Android的源码中有attrs.xml这个文件,这个文件实际上定义了所有的控件的属性,就是我们在布局文件中设置的各类属性 你可以找到attr

[C/C++]_[VS2010源码中使用UTF8中文字符串被转码为ANSI的问题]

场景: 1.本以为vs设置了源文件的UTF8编码,代码中出现的中文字符串就一定是utf8编码了,可惜不是,如果源码中出现了中文字符串,会在内存中转码为ANSI编码. Unicode(UTF8带签名) 代码页(65001),从菜单->文件->高级保存选项 设置. 例子: char path[] = "resources\\中文\\"; for(int i = 0; i < strlen(path); ++i) { printf("0x%x,",(un

android四大组件之Activity - (1)从源码中理解并巧用onWindowFocusChanged(boolean hasFocus)

这里开始到后面,想趁着有时间,将Android四大组件和一些系统组件做一些总结和记录.由于网上已经有很多写的很好并且总结也全面的文章.小弟我也囊中羞涩不敢献丑,就记录一些自己觉得重要的有用的知识点,顺便大家一起学习讨论啥的也好 Activity作为四大组件之一,对于整个Android开发有多重要就无需赘言了.关于它的生命周期,这里借用下官网的图,便一目了然: 那么它的生命周期和我们所说的onWindowFocusChanged(boolean hasFocus)方法有何关系? Activity生