connect.js源碼解析

前言

衆所周知,connect是TJ大神所造的一個大輪子,是你們用尾式調用來控制異步流程時最常使用的庫,也是後來著名的express框架的本源。但使人驚訝的是,它的源碼其實只有200多行,今天也來解析一下它的真容。javascript

解析

如下是connect源碼的主要文件結構:java

  • lib目錄

    • connect.js
    • proto.js
  • index.js

是的,就這三個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')到底返回的是何物。數組

index.js

jsmodule.exports = require('./lib/connect');
//好吧。。這只是一個入口,讓咱們跟隨它的腳步進入./lib/connect.js

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.jsapp

lib/proto.js

proto.js中,主要暴露出了3個方法,分別爲use,handlecall,咱們來逐一分解:框架

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的廬山真面目了,其總體的結構大體可歸納爲:

  • 暴露出的app函數(函數體爲本身的handle方法)

    • proto處繼承的屬性(方法)
    • 繼承的EventEmitter的原型
    • route屬性,表示中間件的默認請求路徑
    • stack數組,全部的中間件的存放處,中間件會被格式化成形爲{route: route , handle : fn}的匿名對象存放

而總體的運行過程大體可歸納爲:

  • use註冊中間件
  • Server接受請求
  • 調用handle檢查stack數組中註冊的中間件與此請求的url是否匹配
  • 若匹配到了一箇中間件,則調用call執行
  • 繼續尋找是否還有匹配的中間件並執行...
  • 登記的中間件所有查詢完畢,匹配的中間件所有執行完畢,結束。
相關文章
相關標籤/搜索