摘要:關于的實現源碼解讀,版本為。主要為路由部分。返回到的遍歷是通過尾遞歸的方式實現的,注意到被傳入的方法中,中處理事情最后向傳入,從而是否繼續遍歷取決于的實現是否調用的方法。
關于express.js的實現源碼解讀,版本為 4.14。主要為路由部分。
一個Web框架最重要的模塊是路由功能,該模塊的目標是:能夠根據method、path匹配需要執行的方法,并在定義的方法中提供有關請求和回應的上下文。
模塊聲明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()方法進行實例化和初始化后返回。
// 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,并隱藏鏈式調用和異常處理等細節。
// 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++)。
// 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、1.0、2.0、3.0、4.0的變化與改進思路。
與koa框架的對比
感想express的代碼其實不多。
路由部分其實寫得還是比較亂,大量關于細節的if、else判斷,仍是過程式的風格,功能的實現并沒有特別的算法技巧,尤其是路由,直接是一個一個試的。框架的實現并不都是所想的如此神奇或者高超。
一些不當的代碼風格,如route.get等API中沒有在函數簽名中寫明handler參數,直接通過argument數組取slice得到,而且為了實現同一函數名字的不同函數參數的重載,不得不在函數中判斷參數的類型再 if、 else 。(js不支持函數重載)
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/86505.html
摘要:在后續的總結中,我會繼續分析,并準備將一些值得分析的逐一解讀,也會涉及一些。從一個官方示例開始這是官方給出的一個簡單程序,運行后訪問顯示。第一行載入了框架,我們來看源代碼中的。代碼的開始定義了一個函數,函數有形參,,為回調函數。 這兩天仔細看了看express的源碼,對其的整個實現有了較清晰的認識,所以想總結一下寫出來,如果有什么不對的地方,望指出。 這是第一篇,首先介紹一個最簡單的...
摘要:載入了框架,我們來看源代碼中的。函數函數代碼如下代碼的開始定義了一個函數,函數有形參,,為回調函數。相應的,等同于繼承,從而讓有了事件處理的能力。 此為裁剪過的筆記版本。 原文在此:https://segmentfault.com/a/11...原文在此: https://cnodejs.org/topic/574... 感謝@YiQi ,@leijianning 帶來的好文章。我稍作...
摘要:框架核心特性路由定義了路由表用于執行不同的請求動作。中間件可以設置中間件來響應請求。注冊一個請求路由結束響應開啟監聽端口執行上面代碼是一種實用工具,將為您的源的任何變化并自動重啟服務器監控。 Express 簡介 Express 是一個簡潔而靈活的 node.js Web應用框架, 提供了一系列強大特性幫助你創建各種 Web 應用,和豐富的 HTTP 工具。使用 Express 可以快...
摘要:學習的源代碼的好處自然不少。閱讀源代碼可以幫你實現你的好奇心。本文會推薦一些的源代碼分析文章,可以幫助更快的,更加全方位的理解研讀之。 盡管有Hapi,Koa等有力的競爭者,express.js依然是非常流行的nodejs web服務器框架,畢竟它早于2007年就已經在開發了。 學習expressjs的源代碼的好處自然不少。 它可以幫你深刻理解HTTP協議,這個協議是做前端后端都必然需...
摘要:每個請求都會對應一個響應。一個響應主要包括狀態行響應頭消息體,將常用的數據封裝為類,在上面的代碼中就是該類的一個對象。執行測試用例,報錯,提示不存在。目前在中,一個路由是由三個部分構成路徑方法和處理函數。 1. 簡介 這篇文章主要的目的是分析理解express的源碼,網絡上關于源碼的分析已經數不勝數,這篇文章準備另辟蹊徑,仿制一個express的輪子,通過測試驅動的開發方式不斷迭代,正...
閱讀 2910·2021-11-25 09:43
閱讀 2334·2021-11-24 09:39
閱讀 2719·2021-09-23 11:51
閱讀 1410·2021-09-07 10:11
閱讀 1456·2019-08-27 10:52
閱讀 1942·2019-08-26 12:13
閱讀 3361·2019-08-26 11:57
閱讀 1401·2019-08-26 11:31