關於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
對外(即開發者)提供了路由規則定義的接口:get
、put
等對應於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定義在route
的dispatch()
方法中,該方法中,的確在遍歷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
數組中。
對於一個路由規則:
路徑在Route
和Layer
的成員變量path
。
HTTP method在Route
下每一個handler對應的Layer
中的method
成員變量,以及Route
下的成員變量methods
標記了各個method是否有對應的Layer
。
handler,每個都包裝成一個Layer
,全部的Layer
保存在Route
中的stack
數組中。
有了如上信息,當一個請求進來須要尋找匹配的路由變得清晰。路由匹配過程定義在Router
的handle()
方法中(router/index.js 135行)(回顧:Router()
方法實際上調用了handle()
方法。)
handle()
方法中,不關注解析url字符串等細節。從214行可發現,不考慮異常狀況,尋找匹配路由的過程實際上是遍歷全部Layer
的過程:
對於每一個Layer
,判斷req
中的path
是否與layer
中的path
匹配,若不匹配,繼續遍歷(path匹配過程後述);
若path匹配,則再取req
中的method
,經過route
的methods
成員變量判斷在該route
下是否存在匹配的method,若不匹配,繼續遍歷。
若都匹配,則提取路徑參數(形如/:userId
的通配符),調用關於路徑參數的handler。(經過router.param()
設置的中間件)
調用路由配置route
的handlers,這又是遍歷route
下的小的Layer
數組的過程。
決定是否返回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
封裝後加入到Router
的stack
列表中,Layer
中的route
爲undefined
。緣由是route
參雜了有關http method有關的判斷,不適用於全局的中間件。
經過Router.use()
配置的子路由, use()
方法能夠傳入另外一個Router
,從而實現路由模塊化的功能,處理實際上和普通中間件同樣,但此時傳入handler爲Router
,故調用Router()時即調用Router
的handle()
方法,使用這樣的技巧實現了子路由的功能。
// 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行),當route
爲undefined
時調用。直接將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
完成。默認會載入init
和query
中間件(位於middleware/下),分別用於初始化字段操做以及將query
解析放在req
下。
經過Router.param()
配置的參數路由,router
下params
成員變量存放param
映射到array[: handler]
的map,調用路由前先調用匹配參數的中間件。
如今考慮帶有參數通配符的路徑配置和匹配過程。細節在Layer
對象中。
路徑的匹配其實是經過正則表達式的匹配完成的。將形如
'/foo/:bar'
轉爲
/^\/foo\/(?:([^\/]+?))\/?$/i
正則的轉換由第三方模塊path-to-regex
完成。解析後放在req.params
中。
在handler的調用中都使用了尾調用尾遞歸模式設計(也能夠理解爲責任鏈模式、管道模式),包括:
Router
中的handle
方法調用匹配路由的總handler和中間件。
Router
中的路徑參數路由(params
)的調用過程。
Route
中dispatch
方法處理全部的handlers和每個Layer
中的handle配合。
鏈式調用示意圖:
每個節點都不瞭解自身的位置以及先後關係,調用鏈只能經過next()
調用下一個,若不調用則跳過,並調用done()
結束調用鏈。
調用鏈的一個環節仍能夠是一個調用鏈,造成層次結構(思考上述提到的大Layer
和小Layer
的關係)
子調用鏈中的done()
方法便是父調用鏈中的next()
方法。
出現異常則:
若可以接受繼續進行,不中斷調用鏈,則能夠繼續調用next
方法,帶上err
參數,即next(err)
。最終經過done(err)
將異常返回給父調用鏈。
若不能接受,須要中斷,則調用done
方法,,帶上err
參數,即done(err)
。
-- Fin --
視圖渲染模塊 render實現,在applications.js 和 view.js 中。
對req
和res
的擴展,header處理。
express從0.一、1.0、2.0、3.0、4.0的變化與改進思路。
與koa框架的對比
express的代碼其實很少。
路由部分其實寫得仍是比較亂,大量關於細節的if、else判斷,還是過程式的風格,功能的實現並無特別的算法技巧,尤爲是路由,直接是一個一個試的。框架的實現並不都是所想的如此神奇或者高超。
一些不當的代碼風格,如route.get等API中沒有在函數簽名中寫明handler參數,直接經過argument數組取slice獲得,並且爲了實現同一函數名字的不一樣函數參數的重載,不得不在函數中判斷參數的類型再 if、 else 。(js不支持函數重載)