從express源碼中探析其路由機制

引言

  在web開發中,一個簡化的處理流程就是:客戶端發起請求,而後服務端進行處理,最後返回相關數據。無論對於哪一種語言哪一種框架,除去細節的處理,簡化後的模型都是同樣的。客戶端要發起請求,首先須要一個標識,一般狀況下是URL,經過這個標識將請求發送給服務端的某個具體處理程序,在這個過程當中,請求可能會經歷一系列全局處理,好比驗證、受權、URL解析等,而後定位到某個處理程序進行業務處理,最後將生成的數據返回客戶端,客戶端將數據結合視圖模版呈現出合適的樣式。這個過程涉及到的模塊比較多,本文只探討前半部分,也就是從客戶端請求到服務器端處理程序的過程,也能夠叫作路由(其實就是如何定位到服務端處理程序的過程)。web

  爲了做爲對比,先簡單介紹一下asp.net webform和asp.net mvc是如何實現路由的。express

  asp.net webform比較特殊,因爲是postback原理,定位處理程序的過程與mvc是不同的,對URL的格式沒有嚴格的要求,url是直接對應後臺文件的,aspx中的服務器表單默認是發送到對應的aspx.cs文件,它的定位是藉助aspx頁面中的兩個隱藏域(__EVENTTARGET和__EVENTARGUMENT)以及IPostBackEventHandler接口來實現的,經過這兩樣東西就能夠直接定位到某個具體方法中,一般是某個控件的某個事件處理程序。也就是說,在webform中,url僅能將請求定位到類中,要定位到真正的處理程序(方法)中,還需藉助其餘手段。npm

  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()等都沒找到,仔細再一看,發現了以下方法:mvc

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的關聯。app

  在這個方法中須要注意如下幾點:框架

  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(); });

/* 還能夠這麼寫,直接傳入多個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",咱們發如今終端中輸出的內容與咱們以前分析的徹底一致,以下圖所示:

在示例中,咱們經過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遍歷到路由對象的各個處理程序。
相關文章
相關標籤/搜索