源碼學習:一個express().get方法的加載與調用


剛剛接觸express,它的中間件確實把我搞得頭暈。get的回調中要不要加next?不加載還會執行下一個中間件么?給get指定'/'路徑是不是所有以'/'開頭的訪問在沒有確切匹配時都能執行?use件又有什么區別,use中不加next是不是也可以繼續執行下一個next?這些問題就是最困擾我的問題。為了搞清楚這些問題,我開始查看express的源碼。下面就以一次get方法分析express的加載與調用流程。

以下面的代碼為例,命名為test.js:

var express = require('express');
var app = express();

app.get('/',function myFunc(req, res) {
		res.send('this is the Homepage');
	});
	
app.listen('8080');

1.加載

var express = require('express');

導入了express目錄的express.js文件,文件中聲明的輸出為createApplication函數。

var app = express();

調用了上述createApplication函數,來看這個函數(部分):

function createApplication() {
  var app = function(req, res, next) {
    app.handle(req, res, next);
  };

  mixin(app, EventEmitter.prototype, false);
  mixin(app, proto, false);
 
  app.init();
  return app;
}

在這個函數中,初始化了一個app對象。可以看到:

  1. 這個對象是一個遵從http回調簽名的函數,后面可以看到,例程最后一行的代碼app.listen('8080')中,app.listen函數中就是將app最為一個回調函數初始化了一個http server。
  2. 在createApplication函數中,通過mixin(app, proto, false),將proto的屬性和方法傳給了app,而proto即通過require('./application')引用的同目錄下application.js文件。Mixin語句將application.js中為app實例聲明的屬性和方法傳遞給app。之后,createApplication函數又調用了app.init(),這個方法在application.js文件中聲明(在application.js中,聲明了一個app實例的大部分屬性和方法)。

例程test.js第三行調用了app.get方法。application.js對get方法的聲明:

methods.forEach(function(method){
  app[method] = function(path){
    if (method === 'get' && arguments.length === 1) {
      // app.get(setting)
      return this.set(path);
    }

    this.lazyrouter();

    var route = this._router.route(path);
    route[method].apply(route, slice.call(arguments, 1));
    return this;
  };
});

函數首先針對get方法只有一個參數時作出了定義,此時get方法返回app的設定屬性,跟我們的例程沒有關系。

this.lazyrouter()為app實例初始化了基礎router對象,並調用router.use方法為這個router添加了兩個基礎層,回調函數分別為query和middleware.init。我們不去管這個過程。

下一句var route = this._router.route(path)就以第一個參數path調用了router.route方法(router在lazyrouter初始化)。router在router目錄中index.js文件中聲明,它的屬性stack存儲了以layer描述的各個中間層。route方法定義在proto.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;
};

可以看到,首先創建了一個新的route實例;然后將route.dispatch函數作為回調函數創建了一個新的layer實例,並將layer的route屬性設置為這個route實例之后,將這個layer推入router(this.stack的this是router)的stack中。

形象地說,這個過程就是新建了一個layer作為中間層放入了router的stack數組中。這個layer的回調為route.dispatch。

執行完這個router.route方法后,又通過route[method].apply(route, slice.call(arguments, 1));讓生成的這個route(不是router)調用了route.get。route.get中的關鍵流如下:

var handles = flatten(slice.call(arguments));//傳入的回調在route[method].apply(route, slice.call(arguments, 1));中明確,即用戶定義的回調函數myFunc

var layer = Layer('/', {}, handle);//以myFunc作為回調新建一個layer,設置method屬性
      layer.method = method;

      this.methods[method] = true;
      this.stack.push(layer);//這里的this是route對象,它也維護了一個stack(不是router的stack),存放了當前route對象的所有layer,每個layer包裝了一個回調函數。

到此,程序就完成了對get方法的加載。

我們簡短地回顧下這個過程:首先為app實例化一個router對象,這個對象的stack屬性是一個數組,保存了app的不同中間層。一個中間層以一個layer實例表征,這個layer的handle屬性引用了回調函數。對於get等方法創建的layer,它的handle為route.dispatch函數,而在get方法中自定義的回調函數是存放在route的stack中的。如果例程中繼續為app添加其他路由,則router對象會繼續生成新的layer存儲這些中間件,並放入自己的stack中。

2.調用

來看例程最后一句app.listen('8080')。listen方法如下:

app.listen = function listen() {
  var server = http.createServer(this);//this是例程中的app
  return server.listen.apply(server, arguments);
};

listen方法以this為參數生成了一個http server,this是例程中的app,就是以app為回調函數生成了一個http server。前面提到,app是一個遵從http回調簽名的函數,就是因為它就是在request發生時候的回調函數。

當server收到一個request時,調用app函數。app函數內只有一個語句:

app.handle(req, res,next);

handle函數在application.js中,代碼(部分):

app.handle = function handle(req, res, callback) {
  var router = this._router;

  // final handler
  var done = callback || finalhandler(req, res, {
    env: this.get('env'),
    onerror: logerror.bind(this)
  });

  router.handle(req, res, done);
};

首先獲取了app的router對象,然后調用router.handle方法。此時callback繼承express.js中調用時的next,為undifined,所以done就為finalhandler函數,這個函數在服務器結束響應時調用。

繼續看router.handle,這個函數的關鍵是閉包next()函數,此時先執行了next一次。進入while (match !== true && idx < stack.length)循環。通過debug可以知道,此時stack(是router的stack) 有3層,next函數內部:

layer = stack[idx++];
match = matchLayer(layer, path);
//('path matches layer?'+match);
route = layer.route;

先取出第一層,判斷與request的path是否match。第一、二層是router初始化時的query函數和middleware.init函數,它們都會進入執行trim_prefix(layer, layerError, layerPath, path);的分支,並調用其中的layer.handle_request(req,res, next);,這個next就是router.handle函數里的閉包next。執行了這兩層后,繼續回調next函數。

這時就執行到了加載時生成的route所在的層,判斷request路徑是否匹配,這里的匹配執行的是嚴格匹配,比如這層的regexp屬性(從加載時的路由確定)是'/',那么'/a'也不能匹配。

若路徑不匹配,while循環會直接跳過當此循環,對router.stack的下一層進行匹配;如果path與這個route的regexp匹配,就會執行layer.handle_request(req, res, next);。

layer.handle_request函數:

Layer.prototype.handle_request = function handle(req, res, next) {
  var fn = this.handle;

  try {
    fn(req, res, next);
  } catch (err) {
    next(err);
  }
};

執行這層的回調函數fn=this.handle,我們在加載時分析過,這層的回調函數是route.dispatch函數,這個函數用來處理route實例內的路由選擇。來看這個函數(部分):

Route.prototype.dispatch = function dispatch(req, res, done) {
  var idx = 0;
  var stack = this.stack;//this是route,它的stack中存儲的用戶定義的函數myFunc
 
  var method = req.method.toLowerCase();

  req.route = this;

  next();

  function next(err) {
    var layer = stack[idx++];//第一次調用時獲得用戶定義的函數myFunc所在層
    if (!layer) {
      return done(err);
    }

    if (err) {
      layer.handle_error(err, req, res, next);
    } else {
      layer.handle_request(req, res, next);// myFunc所在層調用handle_request
    }
  }
};

到layer.handle_request(req, res, next);時,myFunc所在層調用handle_request。

var fn = this.handle;//fn=myFunc

如果myFunc中沒有引用next,它執行完后就回到了layer.handle_request(req, res, next);中。這樣layer.handle_request(req, res, next);執行結束,回到調用它的route的next函數中,這個next運行結束,dispatch函數也運行結束。由於沒有引用done簽名參數,done所引用的router->next也不再運行。這樣router->layer[dispatch]層的handle_request函數也執行完畢。router->next也執行完畢,從而依次執行完router初始化的兩層[query]和[middleware.init]后,router.handle也執行完畢。整個流程處理結束。

如果myFunc中引用了next,則route.dispatch->next會再次被調用,如果這個route只有這一個handle函數,則在運行到

var layer = stack[idx++];
if (!layer) {
    return done(err);
}

時會返回done(err)。這里的done函數是從layer.handle_request中傳遞來的router->next,於是調用router.handle->next(err);這樣就實現了對router.stack中的下一個中間件的調用。

從上面的分析可以看出,express在處理中間層時,主要用了router、layer、route三個類。一個app實例有一個基礎的router,用來處理所有的中間件;針對每個get等方法請求,都會實例化一個新的route對象。router和route實例都會維護自己的stack數組屬性,以存放其路由信息。stack的每個元素都是一個layer,layer對中間件的回調函數進行了包裝。

而且看到,循環進入下一個中間件的next函數,都是定義在router和route中,而調用一個中間件后再進入下一層則是通過layer實例的接口實現。

3.篇頭的問題

get的回調中要不要加next,不加載next還會執行下一個中間件么?

如果這個方法回調后不需要再執行其他中間件,不需要引用next。但不添加next並不影響其它不同路由的執行,若當前的get方法不匹配請求路徑,router會繼續向下尋找;匹配路徑后,執行完當前回調就不再尋找。

給get指定’/’路徑是不是所有以‘/’開頭的訪問在沒有確切匹配時都能執行?

不行。Router在執行匹配時是嚴格匹配,如果只有’/’,’/a’是不能匹配的。要想所有的請求匹配,可以指定’/*’為最后一個路徑,這樣所有查找不到的請求會被這個回調相應。

use件又有什么區別,use中不加next是不是也可以繼續執行下一個next?

在next的使用上,use與get等方法是一致的。use方法不添加next,則執行完use的回調后不會再執行其他中間件。
use在執行匹配的時候與get等方法是有區別的。例如app.use('/'){}中,請求地址為/aaa也可以訪問到;但如果是app.get('/'),請求中在/之后再加其他字母就無法訪問。

轉載一篇express源碼分析的文章,寫得很好:

從express源碼中探析其路由機制

后記:其實仔細看了express的API,很多問題就清楚了...


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM