摘要:載入了框架,我們來看源代碼中的。函數(shù)函數(shù)代碼如下代碼的開始定義了一個函數(shù),函數(shù)有形參,,為回調(diào)函數(shù)。相應的,等同于繼承,從而讓有了事件處理的能力。
此為裁剪過的筆記版本。
原文在此:https://segmentfault.com/a/11...
原文在此: https://cnodejs.org/topic/574...
感謝@YiQi ,@leijianning 帶來的好文章。我稍作修改和合并,只是為了更加清晰一點點。
基于的版本tags:4.4.2。
把express代碼跑起來從一個官方示例開始:
var express = require("express"); var app = express(); app.get("/", function(req, res){ res.send("Hello World"); }); app.listen(3000);
代碼運行后,訪問localhost:3000顯示Hello World。
逐行分析首先第一行,典型的Node.js模塊載入代碼。載入了express框架,我們來看express源代碼中的index.js。
module.exports = require("./lib/express");
只是簡單的導入了./lib/express.js,所以繼續(xù)深挖看此代碼。
exports = module.exports = createApplication;
從這里我們可以看出,實例程序的第一行導入了函數(shù)createApplication函數(shù)。第二行則是運行了這個函數(shù),然后返回值賦給了app。
函數(shù)createApplication函數(shù)createApplication代碼如下
var EventEmitter = require("events").EventEmitter; var mixin = require("utils-merge"); var proto = require("./application"); var req = require("./request"); var res = require("./response"); function createApplication() { var app = function(req, res, next) { app.handle(req, res, next); }; mixin(app, proto); mixin(app, EventEmitter.prototype); app.request = { __proto__: req, app: app }; app.response = { __proto__: res, app: app }; app.init(); return app; }
代碼的開始定義了一個函數(shù),函數(shù)有形參req,res,next為回調(diào)函數(shù)。函數(shù)體只有一條語句,執(zhí)行app.handle,此函數(shù)在application.js文件中定義,此處是通過mixin導入,它的作用就是將每對[req,res]進行逐級分發(fā),作用在每個定義好的路由及中間件上,直到最后完成。接下來會對此函數(shù)分析。
然后來看看中間的兩行:
mixin(app, proto); mixin(app, EventEmitter.prototype);
函數(shù)mixin,從功能上來說,就是其實就是讓app拷貝proto的所有屬性,等同于app繼承自proto。是的,JavaScript這樣的動態(tài)語言,可以動態(tài)的指定繼承的基礎類。proto在頭部的require處載入的是./lib/application.js文件,其中定義了大部分express的public api,如app.set,app.get,app.use等。相應的,mixin(app, EventEmitter.prototype)等同于繼承EventEmitter,從而讓app有了事件處理的能力。
想要具體了解mixin的同學,可以看到,此函數(shù)為在頭部的require處載入的utils-merge模塊,它的代碼如下
exports = module.exports = function(a, b){ if (a && b) { for (var key in b) { a[key] = b[key]; } } return a; };
再來看接下來的兩行:
app.request = { __proto__: req, app: app }; app.response = { __proto__: res, app: app };
這里定義了app的request和response對象,使用了對象的字面量表示法,使其分別繼承自req(頂部導入的request.js)和res(頂部導入的response.js),并反向引用了app自身。
比如此官方實例中調(diào)用了res.send,此方法就在response.js內(nèi)定義,因為指定了app.response的原型為response.js,就等于res也有了response.js的全部屬性和方法,自然也就有了send方法。
接下來是app.init();。顯然,作用是初始化,做哪些工作呢?
app.init = function(){ this.cache = {}; this.settings = {}; this.engines = {}; this.defaultConfiguration(); };
設定了cache對象(render的時候用到),各種setting的存儲對象,engines對象(模板引擎),最后進行默認的配置。
好了,createApplication函數(shù)就是這些
函數(shù)get實例程序第三行中調(diào)用了從app.get()方法。才函數(shù)是動態(tài)定義的。定義在此:
methods.forEach(function(method){ app[method] = function(path){ if ("get" == method && 1 == arguments.length) return this.set(path); this.lazyrouter(); var route = this._router.route(path); route[method].apply(route, [].slice.call(arguments, 1)); return this; }; });
methods在頂部模塊引入中定義,其實是一個包含各個HTTP請求方法的數(shù)組,代碼在此https://github.com/jshttp/met... 。數(shù)組內(nèi)包括get,put等元素。
而且get方法是被"重載"的,即當app.get();的參數(shù)只有一個時候,執(zhí)行的是獲取變量的功能,否則,執(zhí)行route組件中的route.get方法,將該路由和回調(diào)函數(shù)(即第二個參數(shù))存儲進一個棧中(后續(xù)會進一步分析)。回到原來的問題,在這里,關鍵是看中間的
this.lazyrouter();
我們看它的具體代碼
app.lazyrouter = function() { if (!this._router) { this._router = new Router({ caseSensitive: this.enabled("case sensitive routing"), strict: this.enabled("strict routing") }); this._router.use(query()); this._router.use(middleware.init(this)); } };
此代碼在第一次執(zhí)行時,如果this._route沒有定義的話,就定義它,并添加基本的路由。
注意最后一句用到了middleware模塊的init方法,繼續(xù)上代碼:
exports.init = function(app){ return function expressInit(req, res, next){ if (app.enabled("x-powered-by")) res.setHeader("X-Powered-By", "Express"); req.res = res; res.req = req; req.next = next; req.__proto__ = app.request; res.__proto__ = app.response; res.locals = res.locals || Object.create(null); next(); }; };
expressInit函數(shù)是一個中間件,可以給req設置X-Powered-By的值,也會初始化request和response,通過設置屬性__proto__,把app.request和app.respone繼承到request.js和response.js上。
函數(shù)listen最開頭的官方示例中還有最后一句app.listen(3000),實現(xiàn)代碼如下:
app.listen = function(){ var server = http.createServer(this); return server.listen.apply(server, arguments); };
實際上是調(diào)用了Node.js原生的http模塊的CreatServer方法,代碼:
http.createServer(this);
中的this,就是app,也就是createApplication返回的函數(shù),這個函數(shù)指向到app.handle(),因此,app.handle()就是所有請求的主入口。
路由模塊出場,談及Router,Route的關系重新再看this.lazyrouter(),從名字來看,好像是懶加載router,那我們看看源碼:
app.lazyrouter = function lazyrouter() { if (!this._router) { this._router = new Router({ caseSensitive: this.enabled("case sensitive routing"), strict: this.enabled("strict routing") }); this._router.use(query(this.get("query parser fn"))); this._router.use(middleware.init(this)); } };
果然是,如果_router不存在,就new一個Router出來,而這個Router就是我們剛才在目錄結構中看到的router目錄,也就是今天的主角Router模塊。
Router.route繼續(xù)上邊的代碼,加載完_router之后,執(zhí)行了this._router.route(path)這樣一行代碼,那這行代碼做了什么呢?我們在router目錄下的index.js中找到了它的實現(xiàn):
proto.route = function route(path) { var route = new Route(path); var layer = new Layer(path, { sensitive: this.caseSensitive, strict: this.strict, end: true }, route.dispatch.bind(route)); layer.route = route; this.stack.push(layer); return route; };
我們可以看到,這里new了一個Route對象,并且new了一個Layer對象,然后將Route對象賦值給layer.route,最后將這個Layer添加到stack數(shù)組中。那這個Route又是什么呢,它和Router模塊有什么關系呢,我來說下我的理解:
Route模塊對應的是route.js,主要是來處理路由信息的,每條路由都會生成一個Route實例。
Router模塊對應的是index.js,Router是一個route的集合,在Router模塊下可以定義多個route
每個express創(chuàng)建的實例都會懶加載一個_router來進行路由處理,這個_router就是一個Router類型。
這就是Route和Router的關系。
好了,我們接著看函數(shù)get()的代碼,拿到route對象之后,通過apply的方式調(diào)用了route的對應method函數(shù),假如我們現(xiàn)在使用的是get函數(shù),那現(xiàn)在method就等于get。看到這里大家就會發(fā)現(xiàn),express實例在處理路由的步驟是這樣的:
先創(chuàng)建一個Router對象
然后用Router對象和對應的path來生成一個Route對象
最后由Route對象來處理具體的路由實現(xiàn)
route.method好了,那接下來我們繼續(xù)深入研究,看看route.method究竟做了什么,我們找到route.js文件,發(fā)現(xiàn)如下的代碼:
methods.forEach(function(method){ Route.prototype[method] = function(){ var handles = flatten(slice.call(arguments)); for (var i = 0; i < handles.length; i++) { var handle = handles[i]; if (typeof handle !== "function") { var type = toString.call(handle); var msg = "Route." + method + "() requires callback functions but got a " + type; throw new Error(msg); } debug("%s %s", method, this.path); var layer = Layer("/", {}, handle); layer.method = method; this.methods[method] = true; this.stack.push(layer); } return this; }; });
原來route和application運用了同樣的技巧,通過循環(huán)methods來動態(tài)添加method函數(shù)。
我們直接看函數(shù)內(nèi)部實現(xiàn),首先通過入?yún)@取到handles,這里的handles就是我們定義的路由中間件函數(shù),這里我們可以看到是一個數(shù)組,所以我們可以給一個路由添加多個中間件函數(shù)。常用的設置路由函數(shù)是這樣的,
route.get("/hello",function(){})
但是,為了方便,設置多個也是可以的:
route.get("/hello",function(){},function(){},function(){}))
因此,handles是一個數(shù)組。接下來循環(huán)handles,在每個循環(huán)中利用handle來創(chuàng)建一個Layer對象,然后將Layer對象push到stack中去,這個stack其實是Route內(nèi)部維護的一個數(shù)組,用來存放所有的Layer對象。那么,對象Layer是什么東西呢?
我們可以route對象設置Layer的代碼:
route.get("/hello",function(){})
我們可以app對象設置Layer的代碼:
app.get("/hello",function(){})
也就是說,在route層面,也可以如同app一樣的設置Layer,因此官方文檔中提到了,route被認為是mini app,就是這樣來的。
Layer對象那我們繼續(xù)往下看,看看layer.js的源代碼:
function Layer(path, options, fn) { if (!(this instanceof Layer)) { return new Layer(path, options, fn); } debug("new %s", path); var opts = options || {}; this.handle = fn; this.name = fn.name || ""; this.params = undefined; this.path = undefined; this.regexp = pathRegexp(path, this.keys = [], opts); if (path === "/" && opts.end === false) { this.regexp.fast_slash = true; } }
上邊是Layer的構造函數(shù),我們可以看到這里定義handle,params,path和regexp等幾個主要的屬性:
handle,它就是我們剛剛在route中創(chuàng)建Layer對象傳入的中間件函數(shù)
params其實就是req.params
path就是我們定義路由時傳入的path。
regexp進行路由匹配的時候就是靠它來搞定的,而它的值是由pathRegexp得來的,其實這個pathRegexp對應的是一個第三方模塊path-to-regexp,它的功能是將path轉(zhuǎn)換成regexp,具體用法大家可以自行查看。
Layer.match()看完屬性,我們再來看看Layer有什么方法:
Layer.prototype.match = function match(path) { if (path == null) { // no path, nothing matches this.params = undefined; this.path = undefined; return false; } if (this.regexp.fast_slash) { // fast path non-ending match for / (everything matches) this.params = {}; this.path = ""; return true; } var m = this.regexp.exec(path); if (!m) { this.params = undefined; this.path = undefined; return false; } // store values this.params = {}; this.path = m[0]; var keys = this.keys; var params = this.params; for (var i = 1; i < m.length; i++) { var key = keys[i - 1]; var prop = key.name; var val = decode_param(m[i]); if (val !== undefined || !(hasOwnProperty.call(params, prop))) { params[prop] = val; } } return true; };
match函數(shù)主要用來匹配path的,當我們向express發(fā)送一個http請求時,當前請求對應的是哪個路由,就是通過這個match函數(shù)來判斷的,如果path中帶有參數(shù),match還會把參數(shù)提取出來賦值給params,所以說match是整個路由中很重要的一點。
Layer.prototype.handle_error = function handle_error(error, req, res, next) { var fn = this.handle; if (fn.length !== 4) { // not a standard error handler return next(error); } try { fn(error, req, res, next); } catch (err) { next(err); } };
這個是錯誤處理函數(shù),專門用來處理錯誤的。
Layer.prototype.handle_request = function handle(req, res, next) {
var fn = this.handle; if (fn.length > 3) { // not a standard request handler return next(); } try { fn(req, res, next); } catch (err) { next(err); }
};
從上邊的代碼我們可以看到調(diào)用了fn,而這個fn就是layer的handle屬性,就是我們定義路由時傳入的路由中間件,現(xiàn)在總結下,Layer到底是做什么的呢,它和Route之間的關系如何。說說我的理解:
可以發(fā)現(xiàn)Route和Layer是一對多的關系,每個Route都會維護一個Layer數(shù)組
每個Route代表一個路由
每個Layer對應的是路由的每一個中間件函數(shù)。Layer存儲了每個路由的path和handle等信息,并且實現(xiàn)了match和handle的功能。
講完了Route和Layer的關系,我們再來回頭看看Router和Layer的關系,我們再來看看index.js中prop.route的代碼:
proto.route = function route(path) { var route = new Route(path); var layer = new Layer(path, { sensitive: this.caseSensitive, strict: this.strict, end: true }, route.dispatch.bind(route)); layer.route = route; this.stack.push(layer); return route; };
從代碼我們可以看出來Router每次添加一個route,都會把route包裝到layer中,并且將layer添加到自己的stack中。
那為什么要把route包裝到layer中呢,前邊我們已經(jīng)仔細研究了Layer模塊的代碼,我們發(fā)現(xiàn)Layer具有match和handle的功能,這樣我們就可以通過Layer的match來進行route的匹配了。
route.dispatch()這里有一個關鍵點我們需要特別講解下,上邊的代碼中在創(chuàng)建Layer對象的時候傳入的handle函數(shù)為route.dispatch.bind(route),我們來看看route.js中的route.dispatch:
Route.prototype.dispatch = function dispatch(req, res, done) { var idx = 0; var stack = this.stack; if (stack.length === 0) { return done(); } var method = req.method.toLowerCase(); if (method === "head" && !this.methods["head"]) { method = "get"; } req.route = this; next(); function next(err) { if (err && err === "route") { return done(); } var layer = stack[idx++]; if (!layer) { return done(err); } if (layer.method && layer.method !== method) { return next(err); } if (err) { layer.handle_error(err, req, res, next); } else { layer.handle_request(req, res, next); } } };
我們發(fā)現(xiàn)dispatch中通過next()獲取stack中的每一個layer來執(zhí)行相應的路由中間件,這樣就保證了我們定義在路由上的多個中間件函數(shù)被按照定義的順序依次執(zhí)行。到這里我們已經(jīng)知道了單個路由是被如何執(zhí)行的,那我們定義的多個路由之間又是如何被依次執(zhí)行的呢,現(xiàn)在我們來看看index.js中的handle函數(shù)(有刪減):
proto.handle = function handle(req, res, out) { // middleware and routes var stack = self.stack; next(); function next(err) { // find next matching layer var layer; var match; var route; while (match !== true && idx < stack.length) { layer = stack[idx++]; match = matchLayer(layer, path); route = layer.route; if (match !== true) { continue; } if (!route) { // process non-route handlers normally continue; } } // no match if (match !== true) { return done(layerError); } // this should be done for the layer self.process_params(layer, paramcalled, req, res, function (err) { if (err) { return next(layerError || err); } if (route) { return layer.handle_request(req, res, next); } trim_prefix(layer, layerError, layerPath, path); }); } };
此處代碼也是利用next(),來處理stack中的每一個Layer,這里的stack是Router.stack,stack中存貯了多個route對應的layer
獲取到每個layer對象
用請求的path與layer進行匹配,此處匹配用的是layer.match
3.1 如果能匹配到對應的layer,則獲得layer.route
3.2 如果route不為空則執(zhí)行對應的layer.handle_request()
3.3 如果route為空說明這個layer是通過use()添加的非路由中間件
需要特別說明的是,如果通過use()添加的非路由中間件沒有指定path,則會在layer.match中默認返回true,也就是說,沒有指定path的非路由中間件會匹配所有的http請求。
總結我們接下來來重新梳理一下。看看express究竟是如何對http請求進行路由的。
當客戶端發(fā)送一個http請求后,會先進入express實例對象對應的router.handle函數(shù)中
router.handle函數(shù)會通過next()遍歷stack中的每一個layer進行match
如果match返回true,則獲取layer.route,執(zhí)行route.dispatch函數(shù)
route.dispatch同樣是通過next()遍歷stack中的每一個layer,然后執(zhí)行l(wèi)ayer.handle_request,也就是調(diào)用中間件函數(shù)。
直到所有的中間件函數(shù)被執(zhí)行完畢,整個路由處理結束。
文章版權歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/93035.html
摘要:在后續(xù)的總結中,我會繼續(xù)分析,并準備將一些值得分析的逐一解讀,也會涉及一些。從一個官方示例開始這是官方給出的一個簡單程序,運行后訪問顯示。第一行載入了框架,我們來看源代碼中的。代碼的開始定義了一個函數(shù),函數(shù)有形參,,為回調(diào)函數(shù)。 這兩天仔細看了看express的源碼,對其的整個實現(xiàn)有了較清晰的認識,所以想總結一下寫出來,如果有什么不對的地方,望指出。 這是第一篇,首先介紹一個最簡單的...
摘要:就是每一個教程里面開始教學的事例,啟動服務器的回調(diào)函數(shù)。,從入口開始分析源碼首先是把模塊的屬性全部進里面去,在把事件的屬性全部進里面去,這是為了給增加事件功能。 express4.X源碼解讀第一天 express4.X 跟3.X 有很大區(qū)別,4.X 去除了connect的依賴,3.X基于connect的中間件基本全部不能用,如果還有可以使用的,也是4.X重寫的。所以要想繼續(xù)使用這些熟悉...
摘要:本筆記共四篇源碼閱讀筆記源碼閱讀筆記源碼閱讀筆記服務器啟動與請求處理源碼閱讀筆記對象起因前兩天閱讀了的基礎,和中間件的基礎。的前端樂園原文鏈接源碼閱讀筆記服務器啟動與請求處理 本筆記共四篇Koa源碼閱讀筆記(1) -- coKoa源碼閱讀筆記(2) -- composeKoa源碼閱讀筆記(3) -- 服務器の啟動與請求處理Koa源碼閱讀筆記(4) -- ctx對象 起因 前兩天閱讀了K...
摘要:接下來我們深入函數(shù),看看它干了什么。在我們寫的代碼里,我們會手動將元素掛載到樹上。到這里,我們已經(jīng)完成了元素掛載的全過程,接下來我們看一看更新的時候會發(fā)生什么。這部分應該是負責的,我們要在組件的方法中調(diào)用。 etch簡介 首先我們有必要介紹一下etch。 etch是atom團隊下的開源項目,是一套非常簡潔然而功能十分完善的virtualDOM機制。我在偶然的情況下接觸到了這個開源項...
摘要:如果此時我們不想把文件輸出到內(nèi)存里,可以通過修改的源代碼來實現(xiàn)。服務啟動成功。。。根據(jù)請求的,拼接出 ? webpack-dev-middleware 是express的一個中間件,它的主要作用是以監(jiān)聽模式啟動webpack,將webpack編譯后的文件輸出到內(nèi)存里,然后將內(nèi)存的文件輸出到epxress服務器上;下面通過一張圖片來看一下它的工作原理: showImg(https:...
閱讀 2879·2021-11-16 11:55
閱讀 2629·2021-09-29 09:34
閱讀 3448·2021-09-01 14:21
閱讀 3783·2019-08-29 12:36
閱讀 708·2019-08-26 10:55
閱讀 4002·2019-08-26 10:20
閱讀 1039·2019-08-23 18:19
閱讀 1206·2019-08-23 17:56