摘要:關(guān)于的實(shí)現(xiàn)源碼解讀,版本為。主要為路由部分。返回到的遍歷是通過(guò)尾遞歸的方式實(shí)現(xiàn)的,注意到被傳入的方法中,中處理事情最后向傳入,從而是否繼續(xù)遍歷取決于的實(shí)現(xiàn)是否調(diào)用的方法。
關(guān)于express.js的實(shí)現(xiàn)源碼解讀,版本為 4.14。主要為路由部分。
一個(gè)Web框架最重要的模塊是路由功能,該模塊的目標(biāo)是:能夠根據(jù)method、path匹配需要執(zhí)行的方法,并在定義的方法中提供有關(guān)請(qǐng)求和回應(yīng)的上下文。
模塊聲明express中的路由模塊由Router完成,通過(guò)完成調(diào)用Router()得到一個(gè)router的實(shí)例,router既是一個(gè)對(duì)象,也是一個(gè)函數(shù),原因是實(shí)現(xiàn)了類似C++中的()重載方法,實(shí)質(zhì)指向了對(duì)象的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對(duì)外(即開(kāi)發(fā)者)提供了路由規(guī)則定義的接口:get、put等對(duì)應(yīng)于HTTP method類別,函數(shù)簽名都是$method(path, fn(req, res), ...),接口的方法通過(guò)元編程動(dòng)態(tài)定義生成,可以這樣做的根本原因是方法名可以使用變量的值定義和調(diào)用,Java中的反射特性也可間接實(shí)現(xiàn)這點(diǎn),從而大量被應(yīng)用于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; }; });路由定義
在規(guī)則定義的接口中,路由規(guī)則的定義需要router保存路由規(guī)則的信息,最重要的是方法、路徑以及匹配時(shí)的調(diào)用方法(下稱handler),還有其他一些細(xì)節(jié)信息,這些信息(也可以看做是配置)的保存由Route對(duì)象完成,一個(gè)Route對(duì)象包含一個(gè)路由規(guī)則。Route對(duì)象通過(guò)router對(duì)象的route()方法進(jìn)行實(shí)例化和初始化后返回。
// 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所有類別的注冊(cè)函數(shù),此處無(wú)關(guān)緊要,只要route能夠得到路由配置的method值即可,將method作為一個(gè)參數(shù)傳入或者作為方法名調(diào)入都可以。
route()方法除了實(shí)例化一個(gè)Route外,還是實(shí)例化了一個(gè)Layer,這個(gè)的Layer相當(dāng)于是對(duì)應(yīng)Route的總的調(diào)度器,封裝了handlers的調(diào)用過(guò)程,先忽略。
真正將handlers傳入到route中發(fā)生在510行,也即上述route提供的注冊(cè)函數(shù)。由于一條路由設(shè)置中可以傳入多個(gè)handler,因此需要保存有關(guān)handler的列表,每一個(gè)handler由一個(gè)Layer對(duì)象進(jìn)行封裝,用以隱藏異常處理和handler調(diào)用鏈的細(xì)節(jié)。因此,route保存了一個(gè)Layer數(shù)組,按handler在參數(shù)中的聲明順序存放。這里體現(xiàn)Layer的第一個(gè)作用:封裝一條路由中的一個(gè)handler,并隱藏鏈?zhǔn)秸{(diào)用和異常處理等細(xì)節(jié)。
// 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中,最初實(shí)例化一個(gè)route的方法route中,還實(shí)例化了一個(gè)Layer,并且router保存了關(guān)于這些Layer的一個(gè)列表,由于我們可以在router定義多個(gè)路由規(guī)則,因此這是Layer的第二個(gè)作用:封裝一條路由中的一個(gè)總的handler,同樣也封裝了鏈?zhǔn)秸{(diào)用和異常處理等細(xì)節(jié)。這個(gè)總的handler即是遍歷調(diào)用route下的所有的handler的過(guò)程,相當(dāng)于一個(gè)總的Controller,每一個(gè)handler實(shí)際上是通過(guò)對(duì)應(yīng)的小的Layer來(lái)完成handler的調(diào)用。
由route()方法可知,總的handler定義在route的dispatch()方法中,該方法中,的確在遍歷route對(duì)象下的Layer數(shù)組(成員變量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; };路由匹配
整理路由配置過(guò)程,思考每個(gè)路由配置信息的保存位置,有:
路由規(guī)則,一條對(duì)應(yīng)于一個(gè)Route中,并包裝一個(gè)Layer。
所有路由規(guī)則保存在Router中的stack數(shù)組中。
對(duì)于一個(gè)路由規(guī)則:
路徑在Route和Layer的成員變量path。
HTTP method在Route下每個(gè)handler對(duì)應(yīng)的Layer中的method成員變量,以及Route下的成員變量methods標(biāo)記了各個(gè)method是否有對(duì)應(yīng)的Layer。
handler,每一個(gè)都包裝成一個(gè)Layer,所有的Layer保存在Route中的stack數(shù)組中。
有了如上信息,當(dāng)一個(gè)請(qǐng)求進(jìn)來(lái)需要尋找匹配的路由變得清晰。路由匹配過(guò)程定義在Router的handle()方法中(router/index.js 135行)(回顧:Router()方法實(shí)際上調(diào)用了handle()方法。)
handle()方法中,不關(guān)注解析url字符串等細(xì)節(jié)。從214行可發(fā)現(xiàn),不考慮異常情況,尋找匹配路由的過(guò)程其實(shí)是遍歷所有Layer的過(guò)程:
對(duì)于每個(gè)Layer,判斷req中的path是否與layer中的path匹配,若不匹配,繼續(xù)遍歷(path匹配過(guò)程后述);
若path匹配,則再取req中的method,通過(guò)route的methods成員變量判斷在該route下是否存在匹配的method,若不匹配,繼續(xù)遍歷。
若都匹配,則提取路徑參數(shù)(形如/:userId的通配符),調(diào)用關(guān)于路徑參數(shù)的handler。(通過(guò)router.param()設(shè)置的中間件)
調(diào)用路由配置route的handlers,這又是遍歷route下的小的Layer數(shù)組的過(guò)程。
決定是否返回1繼續(xù)遍歷。返回到stack的遍歷是通過(guò)尾遞歸的方式實(shí)現(xiàn)的,注意到next被傳入layer.handle_request的方法中,handle_request中處理事情最后向handler傳入next,從而是否繼續(xù)遍歷取決于handler的實(shí)現(xiàn)是否調(diào)用的next()方法。express的實(shí)現(xiàn)大量使用尾遞歸尾調(diào)用的模式,如process_params()方法。
簡(jiǎn)化版的路由匹配過(guò)程如下所示:
// 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); } }); } }特殊路由
在路由匹配的分析中,省略了大量細(xì)節(jié)。
通過(guò)Router.use()配置的普通中間件:默認(rèn)情況下,相當(dāng)于配置了一個(gè)path為"/"的路由,若參數(shù)提供了path,則相當(dāng)于配置了關(guān)于path的全method的路由。不同的是,handlers不使用route封裝,每一個(gè)handler直接使用一個(gè)大的Layer封裝后加入到Router的stack列表中,Layer中的route為undefined。原因是route參雜了有關(guān)http method有關(guān)的判斷,不適用于全局的中間件。
通過(guò)Router.use()配置的子路由, use()方法可以傳入另一個(gè)Router,從而實(shí)現(xiàn)路由模塊化的功能,處理實(shí)際上和普通中間件一樣,但此時(shí)傳入handler為Router,故調(diào)用Router()時(shí)即調(diào)用Router的handle()方法,使用這樣的技巧實(shí)現(xiàn)了子路由的功能。
// 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行),當(dāng)route為undefined時(shí)調(diào)用。直接將req的路徑減去父路由的path即可。為了能夠在子路由結(jié)束時(shí)返回到父路由,需要從子路徑恢復(fù)到帶有父路徑的路徑(信息在req中),結(jié)束時(shí)調(diào)用done(),done指向restore()方法,用于恢復(fù)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); }; }
通過(guò)app配置的應(yīng)用層路由和中間件,實(shí)際上由app里的成員變量router完成。默認(rèn)會(huì)載入init和query中間件(位于middleware/下),分別用于初始化字段操作以及將query解析放在req下。
通過(guò)Router.param()配置的參數(shù)路由,router下params成員變量存放param映射到array[: handler]的map,調(diào)用路由前先調(diào)用匹配參數(shù)的中間件。
路徑參數(shù)現(xiàn)在考慮帶有參數(shù)通配符的路徑配置和匹配過(guò)程。細(xì)節(jié)在Layer對(duì)象中。
路徑的匹配實(shí)際上是通過(guò)正則表達(dá)式的匹配完成的。將形如
"/foo/:bar"
轉(zhuǎn)為
/^/foo/(?:([^/]+?))/?$/i
正則的轉(zhuǎn)換由第三方模塊path-to-regex完成。解析后放在req.params中。
鏈?zhǔn)秸{(diào)用和異常處理在handler的調(diào)用中都使用了尾調(diào)用尾遞歸模式設(shè)計(jì)(也可以理解為責(zé)任鏈模式、管道模式),包括:
Router中的handle方法調(diào)用匹配路由的總handler和中間件。
Router中的路徑參數(shù)路由(params)的調(diào)用過(guò)程。
Route中dispatch方法處理所有的handlers和每一個(gè)Layer中的handle配合。
鏈?zhǔn)秸{(diào)用示意圖:
每一個(gè)節(jié)點(diǎn)都不了解自身的位置以及前后關(guān)系,調(diào)用鏈只能通過(guò)next()調(diào)用下一個(gè),若不調(diào)用則跳過(guò),并調(diào)用done()結(jié)束調(diào)用鏈。
調(diào)用鏈的一個(gè)環(huán)節(jié)仍可以是一個(gè)調(diào)用鏈,形成層次結(jié)構(gòu)(思考上述提到的大Layer和小Layer的關(guān)系)
子調(diào)用鏈中的done()方法即是父調(diào)用鏈中的next()方法。
出現(xiàn)異常則:
若能夠接受繼續(xù)進(jìn)行,不中斷調(diào)用鏈,則可以繼續(xù)調(diào)用next方法,帶上err參數(shù),即next(err)。最終通過(guò)done(err)將異常返回給父調(diào)用鏈。
若不能接受,需要中斷,則調(diào)用done方法,,帶上err參數(shù),即done(err)。
-- Fin --
進(jìn)階視圖渲染模塊 render實(shí)現(xiàn),在applications.js 和 view.js 中。
對(duì)req和res的擴(kuò)展,header處理。
express從0.1、1.0、2.0、3.0、4.0的變化與改進(jìn)思路。
與koa框架的對(duì)比
感想express的代碼其實(shí)不多。
路由部分其實(shí)寫得還是比較亂,大量關(guān)于細(xì)節(jié)的if、else判斷,仍是過(guò)程式的風(fēng)格,功能的實(shí)現(xiàn)并沒(méi)有特別的算法技巧,尤其是路由,直接是一個(gè)一個(gè)試的??蚣艿膶?shí)現(xiàn)并不都是所想的如此神奇或者高超。
一些不當(dāng)?shù)拇a風(fēng)格,如route.get等API中沒(méi)有在函數(shù)簽名中寫明handler參數(shù),直接通過(guò)argument數(shù)組取slice得到,而且為了實(shí)現(xiàn)同一函數(shù)名字的不同函數(shù)參數(shù)的重載,不得不在函數(shù)中判斷參數(shù)的類型再 if、 else 。(js不支持函數(shù)重載)
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/86505.html
摘要:在后續(xù)的總結(jié)中,我會(huì)繼續(xù)分析,并準(zhǔn)備將一些值得分析的逐一解讀,也會(huì)涉及一些。從一個(gè)官方示例開(kāi)始這是官方給出的一個(gè)簡(jiǎn)單程序,運(yùn)行后訪問(wèn)顯示。第一行載入了框架,我們來(lái)看源代碼中的。代碼的開(kāi)始定義了一個(gè)函數(shù),函數(shù)有形參,,為回調(diào)函數(shù)。 這兩天仔細(xì)看了看express的源碼,對(duì)其的整個(gè)實(shí)現(xiàn)有了較清晰的認(rèn)識(shí),所以想總結(jié)一下寫出來(lái),如果有什么不對(duì)的地方,望指出。 這是第一篇,首先介紹一個(gè)最簡(jiǎn)單的...
摘要:載入了框架,我們來(lái)看源代碼中的。函數(shù)函數(shù)代碼如下代碼的開(kāi)始定義了一個(gè)函數(shù),函數(shù)有形參,,為回調(diào)函數(shù)。相應(yīng)的,等同于繼承,從而讓有了事件處理的能力。 此為裁剪過(guò)的筆記版本。 原文在此:https://segmentfault.com/a/11...原文在此: https://cnodejs.org/topic/574... 感謝@YiQi ,@leijianning 帶來(lái)的好文章。我稍作...
摘要:框架核心特性路由定義了路由表用于執(zhí)行不同的請(qǐng)求動(dòng)作。中間件可以設(shè)置中間件來(lái)響應(yīng)請(qǐng)求。注冊(cè)一個(gè)請(qǐng)求路由結(jié)束響應(yīng)開(kāi)啟監(jiān)聽(tīng)端口執(zhí)行上面代碼是一種實(shí)用工具,將為您的源的任何變化并自動(dòng)重啟服務(wù)器監(jiān)控。 Express 簡(jiǎn)介 Express 是一個(gè)簡(jiǎn)潔而靈活的 node.js Web應(yīng)用框架, 提供了一系列強(qiáng)大特性幫助你創(chuàng)建各種 Web 應(yīng)用,和豐富的 HTTP 工具。使用 Express 可以快...
摘要:學(xué)習(xí)的源代碼的好處自然不少。閱讀源代碼可以幫你實(shí)現(xiàn)你的好奇心。本文會(huì)推薦一些的源代碼分析文章,可以幫助更快的,更加全方位的理解研讀之。 盡管有Hapi,Koa等有力的競(jìng)爭(zhēng)者,express.js依然是非常流行的nodejs web服務(wù)器框架,畢竟它早于2007年就已經(jīng)在開(kāi)發(fā)了。 學(xué)習(xí)expressjs的源代碼的好處自然不少。 它可以幫你深刻理解HTTP協(xié)議,這個(gè)協(xié)議是做前端后端都必然需...
摘要:每個(gè)請(qǐng)求都會(huì)對(duì)應(yīng)一個(gè)響應(yīng)。一個(gè)響應(yīng)主要包括狀態(tài)行響應(yīng)頭消息體,將常用的數(shù)據(jù)封裝為類,在上面的代碼中就是該類的一個(gè)對(duì)象。執(zhí)行測(cè)試用例,報(bào)錯(cuò),提示不存在。目前在中,一個(gè)路由是由三個(gè)部分構(gòu)成路徑方法和處理函數(shù)。 1. 簡(jiǎn)介 這篇文章主要的目的是分析理解express的源碼,網(wǎng)絡(luò)上關(guān)于源碼的分析已經(jīng)數(shù)不勝數(shù),這篇文章準(zhǔn)備另辟蹊徑,仿制一個(gè)express的輪子,通過(guò)測(cè)試驅(qū)動(dòng)的開(kāi)發(fā)方式不斷迭代,正...
閱讀 2902·2021-11-25 09:43
閱讀 2320·2021-11-24 09:39
閱讀 2708·2021-09-23 11:51
閱讀 1400·2021-09-07 10:11
閱讀 1449·2019-08-27 10:52
閱讀 1929·2019-08-26 12:13
閱讀 3356·2019-08-26 11:57
閱讀 1393·2019-08-26 11:31