衆所周知,connect是TJ大神所造的一個大輪子,是你們用尾式調用
來控制異步流程時最常使用的庫,也是後來著名的express框架的本源。但使人驚訝的是,它的源碼其實只有200多行,今天也來解析一下它的真容。javascript
如下是connect源碼的主要文件結構:java
是的,就這三個js文件。。express
如下是一個connect的經典用法:npm
jsvar app = require('connect'); var http = require('http'); app.use('/',function(req,res){ res.send("haha"); }); app.use('/',function(req,res){ res.end("hoho"); }) http.createServer(app).listen(3000)
能夠看到,全部的奧祕,都在於app
這個變量。讓咱們先來看看require('connect')
到底返回的是何物。數組
jsmodule.exports = require('./lib/connect'); //好吧。。這只是一個入口,讓咱們跟隨它的腳步進入./lib/connect.js
jsvar EventEmitter = require('events').EventEmitter; var merge = require('utils-merge'); var proto = require('./proto'); module.exports = createServer;//對外暴露createServer函數 /** * 這個函數return出來的app對象即是咱們在以前例子中的見到的那個app對象, * 能夠看到他自身即是一個帶req,res參數的函數,因此這也是它能夠直接 * 做爲參數被傳遞給http.createServer的緣由。並且,因爲在javascript * 中,函數也是對象,因此app函數也有本身的屬性,他繼承了./lib/proto.js * 中暴露出來的方法,也繼承了EventEmitter的原型。能夠看到,route屬性是 * 用來表示請求路徑。stack屬性,則是一個存放全部中間件的容器數組。 */ function createServer() { function app(req, res, next){ app.handle(req, res, next); } merge(app, proto); merge(app, EventEmitter.prototype); app.route = '/'; app.stack = []; return app; }
從上面的代碼中咱們能夠發現,自把app對象
做爲參數傳遞給了http.createServer
方法造成httpServer實例,並監聽了某個端口以後,咱們的Server實際上是在全部請求的callback裏,都執行了app.handle(req,res,next)
。這個handle
函數究竟是在哪定義的呢?從merge(app, proto)
這裏不難看出,它是從./lib/proto.js
這裏暴露出來的方法。讓咱們來看看最後還剩的這個./lib/proto.js
。app
在proto.js
中,主要暴露出了3個方法,分別爲use
,handle
和call
,咱們來逐一分解:框架
js/** * 這就是咱們最後使用app.use()函數,用做添加中間件,route默認爲「/」,其最終 * 任務爲將請求路由與其處理函數綁定爲一個形爲 * {route: route , handle : fn}的匿名函數,推入自身的stack數組中。 */ app.use = function(route, fn){ //若是第一個參數不是字符串,則路由默認爲"/" if ('string' != typeof route) { fn = route; route = '/'; } //若是fn爲一個app的實例,則將其自身handle方法的包裹給fn if ('function' == typeof fn.handle) { var server = fn; server.route = route; fn = function(req, res, next){ server.handle(req, res, next); }; } //若是fn爲一個http.Server實例,則fn爲其request事件的第一個監聽器 if (fn instanceof http.Server) { fn = fn.listeners('request')[0]; } //若是route參數的以"/"結尾,則刪除"/" if ('/' == route[route.length - 1]) { route = route.slice(0, -1); } //輸出測試信息 debug('use %s %s', route || '/', fn.name || 'anonymous'); //將一個包裹route和fn的匿名對象推入stack數組 this.stack.push({ route: route, handle: fn }); //返回自身,以便繼續鏈式調用 return this; };
能夠看到這個use
方法的任務即是中間件的登記
,這樣一來,自身的stack
數組中變充滿了一個個登記了的{route: route , handle : fn}
匿名函數。爲請求到達時,匹配URL,並執行對應的函數,作好了在一個地點,統一格式化,統一存放
。異步
接下來咱們就看看真正掛在Server裏的handle
處理函數:函數
js/** * 這個函數的是爲當前請求路徑尋找出在stack裏全部與之相匹配的中間件, * 並依次調用call * 方法執行(主要作的是大量的縝密的字符串匹配工做,詳看內部註釋) */ app.handle = function(req, res, out) { var stack = this.stack //req中「?」字符的位置索引,用來判斷是否有query string , searchIndex = req.url.indexOf('?') //獲取url的長度(除去query string) , pathlength = searchIndex !== -1 ? searchIndex : req.url.length //若url以「/」開頭,則爲false,不然爲"://"字符串的位置索引 , fqdn = req.url[0] !== '/' && 1 + req.url.substr(0, pathlength).indexOf('://') //若url不以「/」開頭,則protohost爲 協議:/(如https:/) , protohost = fqdn ? req.url.substr(0, req.url.indexOf('/', 2 + fqdn)) : '' , removed = '' // 標記:url是否以"/"結尾 , slashAdded = false , index = 0; //若含有next(第三個)參數,則繼續調用,若無,則使用finalhandler庫,做爲請求最後的處理函數,如有err則拋出,不然則報404 var done = out || finalhandler(req, res, { env: env, onerror: logerror }); req.originalUrl = req.originalUrl || req.url; function next(err) { //若salshAdded標記爲真,則去除最前面的「/」 if (slashAdded) { req.url = req.url.substr(1); slashAdded = false; } if (removed.length !== 0) { req.url = protohost + removed + req.url.substr(protohost.length); removed = ''; } //取本index的中間件,以後把index+1 var layer = stack[index++]; //若是已沒有更多中間件,則結束 if (!layer) { defer(done, err); return; } //路由路徑 var path = parseUrl(req).pathname || '/'; //此中間件的route,用做與path匹配比較 var route = layer.route; //查看當前請求路由是否匹配route,只匹配route長度的字符串,如"/foo/bar"與"/foo"是匹配的 if (path.toLowerCase().substr(0, route.length) !== route.toLowerCase()) { return next(err); } //若是匹配到的路徑不以'/'與‘.’結尾,或已結束,則報錯(即上一個if保證了頭匹配,這裏保證了尾部匹配) var c = path[route.length]; if (c !== undefined && '/' !== c && '.' !== c) { return next(err); } //去除與route不匹配的其餘部分 if (route.length !== 0 && route !== '/') { removed = route; req.url = protohost + req.url.substr(protohost.length + removed.length); //保證路徑以"/"開頭 if (!fqdn && req.url[0] !== '/') { req.url = '/' + req.url; slashAdded = true; } } //調用call函數執行layer call(layer.handle, route, err, req, res, next); } next(); };
因此這個handle
方法的角色只是一個對請求路徑
和中間件註冊路徑
的一個匹配者,找出全部相匹配的中間件,並負責把它們一個個有序(由於中間件也是有序的push進的stack,handle又是靠索引來取的stack裏的匿名對象)
傳入call
方法執行。測試
好,咱們來看最後的call
方法:
js/** * 主要任務即是執行handler中匹配到的中間件 */ function call(handle, route, err, req, res, next) { //handle函數的參數個數(3個參數爲通常中間件,4個參數爲錯誤處理中間件) var arity = handle.length; //是否有錯 var hasError = Boolean(err); //輸出測試信息 debug('%s %s : %s', handle.name || '<anonymous>', route, req.originalUrl); try { //執行錯誤處理中間件 if (hasError && arity === 4) { handle(err, req, res, next); return; } else if (!hasError && arity < 4) { //執行通常中間件 handle(req, res, next); return; } } catch (e) { // reset the error err = e; } next(err); }
因此,可喜可賀,看到這裏,咱們大概已經摸清了connect
的廬山真面目了,其總體的結構大體可歸納爲:
handle
方法)proto
處繼承的屬性(方法)EventEmitter
的原型route
屬性,表示中間件的默認請求路徑stack
數組,全部的中間件的存放處,中間件會被格式化成形爲{route: route , handle : fn}
的匿名對象存放而總體的運行過程大體可歸納爲:
use
註冊中間件handle
檢查stack
數組中註冊的中間件與此請求的url是否匹配call
執行