摘要:函數(shù)表示增加一個路由,函數(shù)表示監(jiān)聽服務(wù)器。內(nèi)部創(chuàng)建一個叫做的類,并為該類添加兩個方法,和。建立類,并將原來內(nèi)的代碼移動到類中。內(nèi)部在寫代碼之前,先梳理一下上面所有的概念之間的關(guān)系和。代表一個應(yīng)用程序。修改原有的函數(shù)。
express源碼閱讀
簡介:這篇文章的主要目的是分析express的源碼,但是網(wǎng)絡(luò)上express的源碼評析已經(jīng)數(shù)不勝數(shù),所以本文章另辟蹊徑,準備仿制一個express的輪子,當然輪子的主體思路是閱讀express源碼所得。
源碼地址:expross
1. 搭建結(jié)構(gòu)有了想法,下一步就是搭建一個山寨的框架,萬事開頭難,就從建立一個文件夾開始吧!
首先建立一個文件夾,叫做expross(你沒有看錯,山寨從名稱開始)。
expross | |-- application.js
接著創(chuàng)建application.js文件,文件的內(nèi)容就是官網(wǎng)的例子。
var http = require("http"); http.createServer(function(req, res) { res.writeHead(200, {"Content-Type": "text/plain"}); res.end("Hello World"); }).listen(3000);
一個簡單的http服務(wù)就創(chuàng)建完成了,你可以在命令行中啟動它,而expross框架的搭建就從這個文件出發(fā)。
1.1 第一劃 Application在實際開發(fā)過程中,web后臺框架的兩個核心點就是路由和模板。路由說白了就是一組URL的管理,根據(jù)前端訪問的URL執(zhí)行對應(yīng)的處理函數(shù)。怎樣管理一組URL和其對應(yīng)的執(zhí)行函數(shù)呢?首先想到的就是數(shù)組(其實我想到的是對象)。
創(chuàng)建一個名稱叫做router的數(shù)組對象。
var http = require("http"); //路由 var router = []; router.push({path: "*", fn: function(req, res) { res.writeHead(200, {"Content-Type": "text/plain"}); res.end("404"); }}, {path: "/", fn: function(req, res) { res.writeHead(200, {"Content-Type": "text/plain"}); res.end("Hello World"); }}); http.createServer(function(req, res) { //自動匹配 for(var i=1,len=router.length; irouter數(shù)組用來管理所有的路由,數(shù)組的每個對象有兩個屬性組成,path表示路徑,fn表示路徑對應(yīng)的執(zhí)行函數(shù)。一切看起來都很不錯,但是這并不是一個框架,為了組成一個框架,并且貼近express,這里繼續(xù)對上面的代碼進一步封裝。
首先定義一個類:Application
var Application = function() {}在這個類上定義二個函數(shù):
Application.prototype.use = function(path, cb) {}; Application.prototype.listen = function(port) {};把上面的實現(xiàn),封裝到這個類中。use 函數(shù)表示增加一個路由,listen 函數(shù)表示監(jiān)聽http服務(wù)器。
var http = require("http"); var Application = function() { this.router = [{ path: "*", fn: function(req, res) { res.writeHead(200, {"Content-Type": "text/plain"}); res.end("Cannot " + req.method + " " + req.url); } }]; }; Application.prototype.use = function(path, cb) { this.router.push({ path: path, fn: cb }); }; Application.prototype.listen = function(port) { var self = this; http.createServer(function(req, res) { for(var i=1,len=self.router.length; i可以像下面這樣啟動它:
var app = new Application(); app.use("/", function(req, res) { res.writeHead(200, {"Content-Type": "text/plain"}); res.end("Hello World"); }); app.listen(3000);看樣子已經(jīng)和express的外觀很像了,為了更像,這里創(chuàng)建一個expross的文件,該文件用來實例化Application。代碼如下:
var Application = require("./application"); exports = module.exports = createApplication; function createApplication() { var app = new Application(); return app; }為了更專業(yè),調(diào)整目錄結(jié)構(gòu)如下:
-----expross | | | |-- index.js | | | |-- lib | | | |-- application.js | |-- expross.js | |---- test.js運行node test.js,走起……
1.2 第二劃 Layer為了進一步優(yōu)化代碼,這里抽象出一個概念:Layer。代表層的含義,每一層就是上面代碼中的router數(shù)組的一個項。
Layer含有兩個成員變量,分別是path和handle,path代表路由的路徑,handle代表路由的處理函數(shù)fn。
------------------------------------------------ | 0 | 1 | 2 | 3 | ------------------------------------------------ | Layer | Layer | Layer | Layer | | |- path | |- path | |- path | |- path | | |- handle| |- handle| |- handle| |- handle| ------------------------------------------------ router 內(nèi)部創(chuàng)建一個叫做layer的類,并為該類添加兩個方法,handle_request和match。match用來匹配請求路徑是否符合該層,handle_request用來執(zhí)行路徑對應(yīng)的處理函數(shù)。
function Layer(path, fn) { this.handle = fn; this.name = fn.name || ""; this.path = path; } //簡單處理 Layer.prototype.handle_request = function (req, res) { var fn = this.handle; if(fn) { fn(req, res); } } //簡單匹配 Layer.prototype.match = function (path) { if(path === this.path) { return true; } return false; } 因為router數(shù)組中存放的將是Layer對象,所以修改Application.prototype.use代碼如下:
Application.prototype.use = function(path, cb) { this.router.push(new Layer(path, cb)); };當然也不要忘記Application構(gòu)造函數(shù)的修改。
var Application = function() { this.router = [new Layer("*", function(req, res) { res.writeHead(200, {"Content-Type": "text/plain"}); res.end("Cannot " + req.method + " " + req.url); })]; };接著改變listen函數(shù),將其主要的處理邏輯抽取成handle函數(shù),用來匹配處理請求信息。這樣可以讓函數(shù)本身的語意更明確,并且遵守單一原則。
Application.prototype.handle = function(req, res) { var self = this; for(var i=0,len=self.router.length; ilisten函數(shù)變得簡單明了。
Application.prototype.listen = function(port) { var self = this; http.createServer(function(req, res) { self.handle(req, res); }).listen(port); };運行node test.js,走起……
1.3 第三劃 router在Application類中,成員變量router負責存儲應(yīng)用程序的所有路由和其處理函數(shù),既然存在這樣一個對象,為何不將其封裝成一個Router類,這個類負責管理所有的路由,這樣職責更加清晰,語意更利于理解。
so,這里抽象出另一個概念:Router,代表一個路由組件,包含若干層的信息。
建立Router類,并將原來Application內(nèi)的代碼移動到Router類中。
var Router = function() { this.stack = [new Layer("*", function(req, res) { res.writeHead(200, {"Content-Type": "text/plain"}); res.end("Cannot " + req.method + " " + req.url); })]; }; Router.prototype.handle = function(req, res) { var self = this; for(var i=0,len=self.stack.length; i為了利于管理,現(xiàn)將路由相關(guān)的文件放到一個目錄中,命名為router。將Router類文件命名為index.js保存到router文件夾內(nèi),并將原來的layer.js移動到該文件夾?,F(xiàn)目錄結(jié)構(gòu)如下:
-----expross | | | |-- index.js | | | |-- lib | | | |-- router | | | | | |-- index.js | | |-- layer.js | | | | | |-- application.js | |-- expross.js | |---- test.js修改原有application.js文件,將代碼原有router的數(shù)組移除,新增加_router對象,該對象是Router類的一個實例。
var Application = function() { this._router = new Router(); }; Application.prototype.use = function(path, fn) { var router = this._router; return router.use(path, fn); }; Application.prototype.handle = function(req, res) { var router = this._router; router.handle(req, res); };到現(xiàn)在為止,整體的框架思路已經(jīng)非常的明確,一個應(yīng)用對象包括一個路由組件,一個路由組件包括n個層,每個層包含路徑和處理函數(shù)。每次請求就遍歷應(yīng)用程序指向的路由組件,通過層的成員函數(shù)match來進行匹配識別URL訪問的路徑,如果成功則調(diào)用層的成員函數(shù)handle_request進行處理。
運行node test.js,走起……
1.4 第四劃 route如果研究過路由相關(guān)的知識就會發(fā)現(xiàn),路由其實是由三個參數(shù)構(gòu)成的:請求的URI、HTTP請求方法和路由處理函數(shù)。之前的代碼只處理了其中兩種,對于HTTP請求方法這個參數(shù)卻刻意忽略,現(xiàn)在是時候把它加進來了。
按照上面的結(jié)構(gòu),如果加入請求方法參數(shù),肯定會加入到Layer里面。但是再加入之前,需要仔細分析一下路由的常見方式:
GET /pages GET /pages/1 POST /page PUT /pages/1 DELETE /pages/1HTTP的請求方法有很多,上面的路由列表是一組常見的路由樣式,遵循REST原則。分析一下會發(fā)現(xiàn)大部分的請求路徑其實是相似或者是一致的,如果將每個路由都建立一個Layer添加到Router里面,從效率或者語意上都稍微有些不符,因為他們是一組URL,負責管理page相關(guān)信息的URL,能否把這樣類似訪問路徑相同而請求方法不同的路由劃分到一個組里面呢?
答案是可以行的,這就需要再次引入一個概念:route,專門來管理具體的路由信息。
------------------------------------------------ | 0 | 1 | 2 | 3 | ------------------------------------------------ | item | item | item | item | | |- method| |- method| |- method| |- method| | |- handle| |- handle| |- handle| |- handle| ------------------------------------------------ route 內(nèi)部在寫代碼之前,先梳理一下上面所有的概念之間的關(guān)系:application、expross、router、route和layer。
-------------- | Application | --------------------------------------------------------- | | | ----- ----------- | 0 | 1 | 2 | 3 | ... | | |-router | ----> | | Layer | --------------------------------------------------------- -------------- | 0 | |-path | | item | item | item | item | | application | | |-route | ----> | |- method| |- method| |- method| |- method| ... | |-----|-----------| | |- handle| |- handle| |- handle| |- handle| | | | Layer | --------------------------------------------------------- | 1 | |-path | route | | |-route | |-----|-----------| | | Layer | | 2 | |-path | | | |-route | |-----|-----------| | ... | ... | ----- ----------- routerapplication代表一個應(yīng)用程序。expross是一個工廠類負責創(chuàng)建application對象。router是一個路由組件,負責整個應(yīng)用程序的路由系統(tǒng)。route是路由組件內(nèi)部的一部分,負責存儲真正的路由信息,內(nèi)部的每一項都代表一個路由處理函數(shù)。router內(nèi)部的每一項都是一個layer對象,layer內(nèi)部保存一個route和其代表的URI。
如果一個請求來臨,會現(xiàn)從頭至尾的掃描router內(nèi)部的每一層,而處理每層的時候會先對比URI,匹配掃描route的每一項,匹配成功則返回具體的信息,沒有任何匹配則返回未找到。
創(chuàng)建Route類,定義三個成員變量和三個方法。path代表該route所對應(yīng)的URI,stack代表上圖中route內(nèi)部item所在的數(shù)組,methods用來快速判斷該route中是是否存在某種HTTP請求方法。
var Route = function(path) { this.path = path; this.stack = []; this.methods = {}; }; Route.prototype._handles_method = function(method) { var name = method.toLowerCase(); return Boolean(this.methods[name]); }; Route.prototype.get = function(fn) { var layer = new Layer("/", fn); layer.method = "get"; this.methods["get"] = true; this.stack.push(layer); return this; }; Route.prototype.dispatch = function(req, res) { var self = this, method = req.method.toLowerCase(); for(var i=0,len=self.stack.length; i在上面的代碼中,并沒有定義前面結(jié)構(gòu)圖中的item對象,而是使用了Layer對象進行替代,主要是為了方便快捷,從另一種角度看,其實二者是存在很多共同點的。另外,為了利于理解,代碼中只實現(xiàn)了GET方法,其他方法的代碼實現(xiàn)是類似的。
既然有了Route類,接下來就改修改原有的Router類,將route集成其中。
Router.prototype.handle = function(req, res) { var self = this, method = req.method; for(var i=0,len=self.stack.length; i代碼中,暫時去除use方法,創(chuàng)建get方法用來添加請求處理函數(shù),route方法是為了返回一個新的Route對象,并將改層加入到router內(nèi)部。
最后修改Application類中的函數(shù),去除use方法,加入get方法進行測試。
Application.prototype.get = function(path, fn) { var router = this._router; return router.get(path, fn); }; Application.prototype.route = function (path) { return this._router.route(path); };測試代碼如下:
var expross = require("./expross"); var app = expross(); app.get("/", function(req, res) { res.writeHead(200, {"Content-Type": "text/plain"}); res.end("Hello World"); }); app.route("/book") .get(function(req, res) { res.writeHead(200, {"Content-Type": "text/plain"}); res.end("Get a random book"); }); app.listen(3000);運行node test.js,走起……
1.5 第五劃 nextnext 主要負責流程控制。在實際的代碼中,有很多種情況都需要進行權(quán)限控制,例如:
var expross = require("./expross"); var app = expross(); app.get("/", function(req, res) { res.writeHead(200, {"Content-Type": "text/plain"}); res.end("first"); }); app.get("/", function(req, res) { res.writeHead(200, {"Content-Type": "text/plain"}); res.end("second"); }); app.listen(3000);上面的代碼如果執(zhí)行會發(fā)現(xiàn)永遠都返回first,但是有的時候會根據(jù)前臺傳來的參數(shù)動態(tài)判斷是否執(zhí)行接下來的路由,怎樣才能跳過first進入second?express引入了next的概念。
跳轉(zhuǎn)到任意layer,成本是比較高的,大多數(shù)的情況下并不需要。在express中,next跳轉(zhuǎn)函數(shù),有兩種類型:
跳轉(zhuǎn)到下一個處理函數(shù)。執(zhí)行 next()。
跳轉(zhuǎn)到下一組route。執(zhí)行 next("route")。
要想使用next的功能,需要在代碼書寫的時候加入該參數(shù):
var expross = require("./expross"); var app = expross(); app.get("/", function(req, res, next) { console.log("first"); next(); }); app.get("/", function(req, res) { res.writeHead(200, {"Content-Type": "text/plain"}); res.end("second"); }); app.listen(3000);而該功能的實現(xiàn)也非常簡單,主要是在調(diào)用處理函數(shù)的時候,除了需要傳入req、res之外,再傳一個流程控制函數(shù)next。
Router.prototype.handle = function(req, res) { var self = this, method = req.method, i = 1, len = self.stack.length, stack; function next() { if(i >= len) { return self.stack[0].handle_request(req, res); } stack = self.stack[i++]; if(stack.match(req.url) && stack.route && stack.route._handles_method(method)) { return stack.handle_request(req, res, next); } else { next(); } } next(); };修改原有Router的handle函數(shù)。因為要控制流程,所以for循環(huán)并不是很合適,可以更換為while循環(huán),或者干脆使用類似遞歸的手法。
代碼中定義一個next函數(shù),然后執(zhí)行next函數(shù)進行自啟動。next內(nèi)部和之前的操作類似,主要是執(zhí)行handle_request函數(shù)進行處理,不同之處是調(diào)用該函數(shù)的時候,將next本身當做參數(shù)傳入,這樣可以在內(nèi)部執(zhí)行該函數(shù)進行下一個處理,類似給handle_request賦予for循環(huán)中++的能力。
按照相同的方式,修改Route的dispatch函數(shù)。
Route.prototype.dispatch = function(req, res, done) { var self = this, method = req.method.toLowerCase(), i = 0, len = self.stack.length, stack; function next(gt) { if(gt === "route") { return done(); } if(i >= len) { return done(); } stack = self.stack[i++]; if(method === stack.method) { return stack.handle_request(req, res, next); } else { next(); } } next(); };代碼思路基本和上面的相同,唯一的差別就是增加route判斷,提供跳過當前整組處理函數(shù)的能力。
Layer.prototype.handle_request = function (req, res, next) { var fn = this.handle; if(fn) { fn(req, res, next); } } Router.prototype.route = function route(path) { var route = new Route(path); var layer = new Layer(path, function(req, res, next) { route.dispatch(req, res, next) }); layer.route = route; this.stack.push(layer); return route; };最后不要忘記修改Layer的handle_request函數(shù)和Router的route函數(shù)。
1.6 后記該小結(jié)基本結(jié)束,當然如果要繼續(xù)還可以寫很多內(nèi)容,包括錯誤處理、函數(shù)重載、高階函數(shù)(生成各種HTTP函數(shù)),以及各種神奇的用法,如繼承、緩存、復(fù)用等等。
但是我覺得搭建結(jié)構(gòu)這一結(jié)已經(jīng)將express的基本結(jié)構(gòu)捋清了,如果重頭到尾的走下來,再去讀框架的源碼應(yīng)該是沒有問題的。
接下來繼續(xù)山寨express 的其他部分。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.hztianpu.com/yun/79822.html
摘要:一點閱讀器源自追書神器,免費使用目前已初步開發(fā)完成項目地址歡迎,,推薦一個之前用文章類閱讀寫的一點閱讀微信小程序一點文章已上線,可以再微信搜索一點文章體驗在線體驗地址點擊這里體驗服務(wù)器太,渲染慢部分效果截圖一點閱讀器優(yōu)勢一點閱讀器追書神 vue-reader 一點閱讀器!API源自追書神器,免費使用!目前已初步開發(fā)完成! Github項目地址:https://github.com/An...
摘要:一點閱讀器源自追書神器,免費使用目前已初步開發(fā)完成項目地址歡迎,,推薦一個之前用文章類閱讀寫的一點閱讀微信小程序一點文章已上線,可以再微信搜索一點文章體驗在線體驗地址點擊這里體驗服務(wù)器太,渲染慢部分效果截圖一點閱讀器優(yōu)勢一點閱讀器追書神 vue-reader 一點閱讀器!API源自追書神器,免費使用!目前已初步開發(fā)完成! Github項目地址:https://github.com/An...
摘要:一點閱讀器源自追書神器,免費使用目前已初步開發(fā)完成項目地址歡迎,,推薦一個之前用文章類閱讀寫的一點閱讀微信小程序一點文章已上線,可以再微信搜索一點文章體驗在線體驗地址點擊這里體驗服務(wù)器太,渲染慢部分效果截圖一點閱讀器優(yōu)勢一點閱讀器追書神 vue-reader 一點閱讀器!API源自追書神器,免費使用!目前已初步開發(fā)完成! Github項目地址:https://github.com/An...
摘要:學習的源代碼的好處自然不少。閱讀源代碼可以幫你實現(xiàn)你的好奇心。本文會推薦一些的源代碼分析文章,可以幫助更快的,更加全方位的理解研讀之。 盡管有Hapi,Koa等有力的競爭者,express.js依然是非常流行的nodejs web服務(wù)器框架,畢竟它早于2007年就已經(jīng)在開發(fā)了。 學習expressjs的源代碼的好處自然不少。 它可以幫你深刻理解HTTP協(xié)議,這個協(xié)議是做前端后端都必然需...
閱讀 3358·2021-10-13 09:39
閱讀 2092·2021-09-27 13:36
閱讀 3146·2021-09-22 16:02
閱讀 2657·2021-09-10 10:51
閱讀 1648·2019-08-29 17:15
閱讀 1589·2019-08-29 16:14
閱讀 3655·2019-08-26 11:55
閱讀 2617·2019-08-26 11:50