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