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


    从express源码中探析其路由机制
     发布于 2 年前  作者 zjh-neverstop  6807 次浏览  最后一次编辑是 几秒前  来自 分享

    引言

            在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框架的使用过程中,隐隐约约感觉其路由过程如下图所示: route1.png 到底是不是这样呢?

    源码分析

    我们知道,在使用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是用来封装中间件的一个数据结构)不太一样, route2.png 由于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发生了关联,关联后的示意模型如下图所示: route3.png 在运行过程中,路由容器(Router)只会有一个实例,而路由中间件会在每次调用app.route、app.use或app[method]的时候生成一个路由对象,在添加路由中间件的时候路由容器相当于是一个代理,Router[method]实际上是在内部调用了Route[method]来实现路由添加的,路由容器中有一个route方法,相当于是路由对象创建工厂。通过添加一个个的中间件,在处理请求的时候会按照添加的顺序逐个调用,如果遇到路由中间件,会逐个调用该路由对象中stack数组里存放的handler,这就是express的流式处理,是不是有点类似asp.net中的管道模型,调用过程如下图所示: route4.png   我们可以做个测试,在终端执行"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();
    });
    /* 还可以这么写,直接传入多个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数组,可以是多维数组
    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”,我们发现在终端中输出的内容与我们之前分析的完全一致,如下图所示: route6.png 在示例中,我们通过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]的执行流程,从图中可以看出三个文件的联系: route5.png 对于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遍历到路由对象的各个处理程序。
  • 相关阅读:
    linux 内核定时器 timer_list详解
    linux2.6源码分析之解压内核映像 head.s
    [C#]我自己写的一个对字节中每位进行修改值的函数
    Android Intent调用大全
    proguard 原理
    何为夫妻?何为家?何为幸福?
    生命只是瞬间,而有些人终究是过客?(转)
    bind端口复用
    在android开发中应该如何管理内存或者是在开发过程中应该注意哪些问题来较少OOM?
    W/ActivityManager( 1419): Activity is launching as a new task, so cancelling activity result.
  • 原文地址:https://www.cnblogs.com/benpaodexiaopangzi/p/5848565.html
Copyright © 2020-2023  润新知