express.js 路由實現解讀

關於express.js的實現源碼解讀,版本爲 4.14。主要爲路由部分。javascript

一個Web框架最重要的模塊是路由功能,該模塊的目標是:可以根據method、path匹配須要執行的方法,並在定義的方法中提供有關請求和迴應的上下文。java

模塊聲明

express中的路由模塊由Router完成,經過完成調用Router()獲得一個router的實例,router既是一個對象,也是一個函數,緣由是實現了相似C++中的()重載方法,實質指向了對象的handle方法。router的定義位於router/index.js中。正則表達式

// router/index.js - line 42
var proto = module.exports = function(options) {
  var opts = options || {};

  // like operator() in C++
  function router(req, res, next) {
    router.handle(req, res, next);
  }
  //...
}

接口定義

router對外(即開發者)提供了路由規則定義的接口:getput等對應於HTTP method類別,函數簽名都是$method(path, fn(req, res), ...),接口的方法經過元編程動態定義生成,能夠這樣作的根本緣由是方法名可使用變量的值定義和調用,Java中的反射特性也可間接實現這點,從而大量被應用於Spring框架中。算法

// router/index.js - line 507
// create Router#VERB functions
// --> ['get', 'post', 'put', ...].foreach
methods.concat('all').forEach(function(method){    
  // so that we can write like 'router.get(path, ...)'
  proto[method] = function(path){     
    // create a route for the routing rule we defined
    var route = this.route(path)      
    // map the corresponding handlers to the routing rule
    route[method].apply(route, slice.call(arguments, 1));
    return this;
  };
});

路由定義

在規則定義的接口中,路由規則的定義須要router保存路由規則的信息,最重要的是方法、路徑以及匹配時的調用方法(下稱handler),還有其餘一些細節信息,這些信息(也能夠看作是配置)的保存由Route對象完成,一個Route對象包含一個路由規則。Route對象經過router對象的route()方法進行實例化和初始化後返回。express

// router/index.js - line 491
proto.route = function route(path) {
  // create an instance of Route.
  var route = new Route(path);    
  // create an instance of Layer.
  var layer = new Layer(path, {
    sensitive: this.caseSensitive,
    strict: this.strict,
    end: true
  }, route.dispatch.bind(route));
  // layer has a reference to route.
  layer.route = route;
  // router has a list of layers which is created by 'route()'
  this.stack.push(layer);
  return route;
};

Route的成員變量包括路徑path,以及HTTP method的路由配置接口集,這裏和router中同樣的技巧提供了method全部類別的註冊函數,此處可有可無,只要route可以獲得路由配置的method值便可,將method做爲一個參數傳入或者做爲方法名調入均可以。編程

route()方法除了實例化一個Route外,仍是實例化了一個Layer,這個的Layer至關因而對應Route的總的調度器,封裝了handlers的調用過程,先忽略。數組

真正將handlers傳入到route中發生在510行,也即上述route提供的註冊函數。因爲一條路由設置中能夠傳入多個handler,所以須要保存有關handler的列表,每個handler由一個Layer對象進行封裝,用以隱藏異常處理和handler調用鏈的細節。所以,route保存了一個Layer數組,按handler在參數中的聲明順序存放。這裏體現Layer的第一個做用:封裝一條路由中的一個handler,並隱藏鏈式調用和異常處理等細節app

// router/route.js - line 190    
for (var i = 0; i < handles.length; i++) {
  var handle = handles[i];
  /* ... */
  // create a layer for each handler defined in a routing rule
  var layer = Layer('/', {}, handle);
  layer.method = method;

  this.methods[method] = true;
  // add the layer to the list.
  this.stack.push(layer);
}

返回到router中,最初實例化一個route的方法route中,還實例化了一個Layer,而且router保存了關於這些Layer的一個列表,因爲咱們能夠在router定義多個路由規則,所以這是Layer的第二個做用:封裝一條路由中的一個總的handler,一樣也封裝了鏈式調用和異常處理等細節。這個總的handler便是遍歷調用route下的全部的handler的過程,至關於一個總的Controller,每個handler其實是經過對應的小的Layer來完成handler的調用。框架

route()方法可知,總的handler定義在routedispatch()方法中,該方法中,的確在遍歷route對象下的Layer數組(成員變量stack以及方法中的idx++)。koa

// router/index.js - line 491
proto.route = function route(path) {
  var route = new Route(path);    
  
  var layer = new Layer(path, {
    sensitive: this.caseSensitive,
    strict: this.strict,
    end: true
  // the 'big' layer's handler is the method 'dispatch()' defined in route 
  }, route.dispatch.bind(route));

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

路由匹配

整理路由配置過程,思考每一個路由配置信息的保存位置,有:

  • 路由規則,一條對應於一個Route中,幷包裝一個Layer

  • 全部路由規則保存在Router中的stack數組中。

  • 對於一個路由規則:

    • 路徑在RouteLayer的成員變量path

    • HTTP method在Route下每一個handler對應的Layer中的method成員變量,以及Route下的成員變量methods標記了各個method是否有對應的Layer

    • handler,每個都包裝成一個Layer,全部的Layer保存在Route中的stack數組中。

有了如上信息,當一個請求進來須要尋找匹配的路由變得清晰。路由匹配過程定義在Routerhandle()方法中(router/index.js 135行)(回顧Router()方法實際上調用了handle()方法。)

handle()方法中,不關注解析url字符串等細節。從214行可發現,不考慮異常狀況,尋找匹配路由的過程實際上是遍歷全部Layer的過程:

  1. 對於每一個Layer,判斷req中的path是否與layer中的path匹配,若不匹配,繼續遍歷(path匹配過程後述);

  2. 若path匹配,則再取req中的method,經過routemethods成員變量判斷在該route下是否存在匹配的method,若不匹配,繼續遍歷。

  3. 若都匹配,則提取路徑參數(形如/:userId的通配符),調用關於路徑參數的handler。(經過router.param()設置的中間件)

  4. 調用路由配置route的handlers,這又是遍歷route下的小的Layer數組的過程。

  5. 決定是否返回1繼續遍歷。返回到stack的遍歷是經過尾遞歸的方式實現的,注意到next被傳入layer.handle_request的方法中,handle_request中處理事情最後向handler傳入next,從而是否繼續遍歷取決於handler的實現是否調用的next()方法。express的實現大量使用尾遞歸尾調用的模式,如process_params()方法。

簡化版的路由匹配過程以下所示:

// router/index.js - line 214
   proto.handle = function handle(req, res, out) {
    
     // middleware and routes
     var stack = self.stack;
     next();
     
     // for each layer in stack     
     function next(err) {  
       // idx is 'index' of the stack
       if (idx >= stack.length) {            
         setImmediate(done, layerError);
         return;
       }
       // get pathname of request
       var path = getPathname(req);
       
       // find next matching layer
       var layer;
       var match;
       var route;
       while (match !== true && idx < stack.length) {
         layer = stack[idx++];
         
         // match the path ?
         match = matchLayer(layer, path);    
         route = layer.route;      
         if (match !== true) {
           continue;
         }
         
         // match the method ?
         var method = req.method;
         var has_method = route._handles_method(method);
         if (!has_method && /**/) {          
           match = false;
           continue;
         }           
       }

       // no match
       if (match !== true) {
         return done(layerError);
       }

       // Capture one-time layer values
       // get path parameters.
       req.params = /*...*/;      
       
       // this should be done for the layer
       // invoke relative path parameters middleware, or handlers 
       self.process_params(layer, paramcalled, req, res, function (err) {
         if (route) {
           // invoke all handlers in a route
           // then invoke the 'next' recursively
           return layer.handle_request(req, res, next);    
         }
       });
     }
   }

特殊路由

在路由匹配的分析中,省略了大量細節。

  • 經過Router.use()配置的普通中間件:默認狀況下,至關於配置了一個path'/'的路由,若參數提供了path,則至關於配置了關於path的全method的路由。不一樣的是,handlers不使用route封裝,每個handler直接使用一個大的Layer封裝後加入到Routerstack列表中,Layer中的routeundefined。緣由是route參雜了有關http method有關的判斷,不適用於全局的中間件。

  • 經過Router.use()配置的子路由use()方法能夠傳入另外一個Router,從而實現路由模塊化的功能,處理實際上和普通中間件同樣,但此時傳入handler爲Router,故調用Router()時即調用Routerhandle()方法,使用這樣的技巧實現了子路由的功能。

    // router/index.js - line 276
    // if it is a route, invoke the handlers in the route. 
    if (route) {
        return layer.handle_request(req, res, next);
    }
    // if it is a middlewire (including router), invoke Router().
    trim_prefix(layer, layerError, layerPath, path);

子路由功能還須要考慮父路徑和子路徑的提取。這在trim_prefix方法(router/index.js 212行),當routeundefined時調用。直接將req的路徑減去父路由的path便可。爲了可以在子路由結束時返回到父路由,須要從子路徑恢復到帶有父路徑的路徑(信息在req中),結束時調用done()done指向restore()方法,用於恢復req的屬性值。

// router/index.js - line 602
// restore obj props after function
function restore(fn, obj) {
  var props = new Array(arguments.length - 2);
  var vals = new Array(arguments.length - 2);
  // save vals.
  for (var i = 0; i < props.length; i++) {
    props[i] = arguments[i + 2];
    vals[i] = obj[props[i]];
  }

  return function(err){
    // restore vals when invoke 'done()'
    for (var i = 0; i < props.length; i++) {
      obj[props[i]] = vals[i];
    }

    return fn.apply(this, arguments);
  };
}
  • 經過app配置的應用層路由和中間件,實際上由app裏的成員變量router完成。默認會載入initquery中間件(位於middleware/下),分別用於初始化字段操做以及將query解析放在req下。

  • 經過Router.param()配置的參數路由routerparams成員變量存放param映射到array[: handler]的map,調用路由前先調用匹配參數的中間件。

路徑參數

如今考慮帶有參數通配符的路徑配置和匹配過程。細節在Layer對象中。

路徑的匹配其實是經過正則表達式的匹配完成的。將形如

'/foo/:bar'

轉爲

/^\/foo\/(?:([^\/]+?))\/?$/i

正則的轉換由第三方模塊path-to-regex完成。解析後放在req.params中。

鏈式調用和異常處理

在handler的調用中都使用了尾調用尾遞歸模式設計(也能夠理解爲責任鏈模式、管道模式),包括:

  • Router中的handle方法調用匹配路由的總handler和中間件。

  • Router中的路徑參數路由(params)的調用過程。

  • Routedispatch方法處理全部的handlers和每個Layer中的handle配合。

鏈式調用示意圖:

  • 每個節點都不瞭解自身的位置以及先後關係,調用鏈只能經過next()調用下一個,若不調用則跳過,並調用done()結束調用鏈。

  • 調用鏈的一個環節仍能夠是一個調用鏈,造成層次結構(思考上述提到的大Layer和小Layer的關係

  • 子調用鏈中的done()方法便是父調用鏈中的next()方法。

  • 出現異常則:

    1. 若可以接受繼續進行,不中斷調用鏈,則能夠繼續調用next方法,帶上err參數,即next(err)。最終經過done(err)將異常返回給父調用鏈。

    2. 若不能接受,須要中斷,則調用done方法,,帶上err參數,即done(err)

圖片描述

-- Fin --


進階

  • 視圖渲染模塊 render實現,在applications.js 和 view.js 中。

  • reqres的擴展,header處理。

  • express從0.一、1.0、2.0、3.0、4.0的變化與改進思路。

  • 與koa框架的對比

感想

  • express的代碼其實很少。

  • 路由部分其實寫得仍是比較亂,大量關於細節的if、else判斷,還是過程式的風格,功能的實現並無特別的算法技巧,尤爲是路由,直接是一個一個試的。框架的實現並不都是所想的如此神奇或者高超。

  • 一些不當的代碼風格,如route.get等API中沒有在函數簽名中寫明handler參數,直接經過argument數組取slice獲得,並且爲了實現同一函數名字的不一樣函數參數的重載,不得不在函數中判斷參數的類型再 if、 else 。(js不支持函數重載)

相關文章
相關標籤/搜索