Express原始碼的一些理解

ZHONGJIAFENG7發表於2019-02-28

Express框架很早就有接觸,一開始接觸的時候覺得Express框架晦澀難懂繞來繞去有點頭暈。最近血來潮準備再次翻看下Express原始碼,一方面也是為了自己做個筆記,方便以後翻閱檢視,另一方面是為了和大家分享,希望得到各位大牛的指導指正。

示例程式碼

在開始前對於如何啟動express專案在這裡就不再贅訴了,不過一般情況都有以下幾句常見程式碼
var express = require('express');
// index下的路由規則
var index = require('./routes/index');
// user下的路由規則
var users = require('./routes/users');
var app = express();

app.use('/', index);
app.use('/users', users);

// 監聽3000埠
app.listen(3000, () => {
  console.log("listening.... ")
});
複製程式碼

這裡重點講下匯入的users.js依賴包,因為這個關係到get請求的路由,程式碼如下

var express = require('express');
var router = express.Router();

/* GET users listing. */
router.get('/', function (req, res, next) {
    res.json({
        name: 1
    });
    next();
},
function (req, res, next) {
    console.log("next aaaa")
});

router.get('/aaa', function (req, res, next) {
    next();
}, function (req, res, next) {
    res.json({
        a: 1
    });
    next();
}, function (req, res) {
    console.log("next", 2222);
});


module.exports = router;
複製程式碼

原始碼分析

從示例程式碼裡我們不難看最重要的程式碼是`app.use`,use內部存放的都是`middleware`,當路由地址匹配的時候,會依次流過這些`middleware`。不過在進行中介軟體的use的時候,首先是對路由的初始化設定,`lazyrouter`內部就進行了路由的初始化設定,並且自動加入了兩個新的中介軟體。
app.lazyrouter = function lazyrouter() {
  if (!this._router) {
    this._router = new Router({
      caseSensitive: this.enabled('case sensitive routing'),
      strict: this.enabled('strict routing')
    });

    this._router.use(query(this.get('query parser fn')));
    this._router.use(middleware.init(this));
  }
};
複製程式碼

鋪墊了這麼多,最終的構造出來的router物件【1】, 為以下形式

/**
router方法物件內部屬性有:
  router.params = {};
  router._params = [];
  router.caseSensitive = opts.caseSensitive;
  router.mergeParams = opts.mergeParams;
  router.strict = opts.strict;
  router.stack = [];
**/
function router(req, res, next) {
    router.handle(req, res, next);
}
複製程式碼

在這裡值得一提的是express內部的巧妙實現

methods.concat('all').forEach(function(method){
  proto[method] = function(path){
    var route = this.route(path)
    // 這裡將method內部的屬性方法的this改為route物件,並傳值route.get等方法的傳參中除了第一個path引數之外的所有回撥函式
    route[method].apply(route, slice.call(arguments, 1));
    return this;
  };
});
複製程式碼
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;
};
複製程式碼

通過遍歷所有的method,往proto內部新增get方法等method內部的屬性,首先需要搞清楚this指誰,this指代的是當前的router物件即為express.Router(),內部包含【1】中的router物件一樣的資料結構,由於是get請求,所有layer.route即為構造的子路由物件,並且將每個get等請求路由加入到Layer物件中,每一個get,post等請求對應於一個Layer物件,最終push到新的stack中。

來看下內部的Layer物件,我認為最重要的是設定路由正則規則以及存放handle函式

function Layer(path, options, fn) {
  if (!(this instanceof Layer)) {
    return new Layer(path, options, fn);
  }

  debug('new %o', path)
  var opts = options || {};

  this.handle = fn;
  this.name = fn.name || '<anonymous>';
  this.params = undefined;
  this.path = undefined;
  this.regexp = pathRegexp(path, this.keys = [], opts);

  // set fast path flags
  this.regexp.fast_star = path === '*'
  this.regexp.fast_slash = path === '/' && opts.end === false
}
複製程式碼

之後第二個神奇巧妙的地方就是var route = new Route(path);,裝載內部的子路由的時候,就像多米勒牌的第一張被觸發了以後出現排山倒海般紙牌組成的美景一樣,內部的實現檔案一被碰到就自動實現好了。

function Route(path) {
  this.path = path;
  this.stack = [];

  debug('new %o', path)

  // route handlers for various http methods
  this.methods = {};
}
複製程式碼
methods.forEach(function(method){
  Route.prototype[method] = function(){
    var handles = flatten(slice.call(arguments));

    for (var i = 0; i < handles.length; i++) {
      var handle = handles[i];

      if (typeof handle !== 'function') {
        var type = toString.call(handle);
        var msg = 'Route.' + method + '() requires a callback function but got a ' + type
        throw new Error(msg);
      }

      debug('%s %o', method, this.path)

      var layer = Layer('/', {}, handle);
      layer.method = method;

      this.methods[method] = true;
      this.stack.push(layer);
    }

    return this;
  };
});
複製程式碼

通過遍歷所有的method,往Route內部新增get方法等method內部的屬性,首先需要搞清楚this指誰,this指代的是new Route(path)構造出來的當前子路由物件, handle方法即為get方法的第一個引數之後的回撥函式,可以有多個,多個回撥函式是惰性的,內部將next方法作為引數傳入了回撥函式,需要使用next函式來進行呼叫回撥函式。

初始化好了router,接下來就是需要將不同route裝載進入express設定的資料結構中,express採用了陣列的形式來進行存放,首先讓我們來看下app.use的內部部分原始碼,app.use可以有兩個引數,第一個為非必須引數path,第二個為fn,在最開始的時候會進行路由的初始化操作,最終app.use會呼叫router檔案內部的router.use,這裡會將每個中介軟體的fn存入到Layer中,一箇中介軟體對應於一個Layer,由於是中介軟體,所有Layer物件下面的route設定為undefined,區別get等其他請求,最終會將Layer物件push到stack。

app.use = function use(fn) {
  // ...
  // setup router
  this.lazyrouter();
  var router = this._router;

  fns.forEach(function (fn) {
    // non-express app
    if (!fn || !fn.handle || !fn.set) {
      return router.use(path, fn);
    }
  }, this);

  return this;
};
複製程式碼
proto.use = function use(fn) {
  for (var i = 0; i < callbacks.length; i++) {
    var fn = callbacks[i];

    // add the middleware
    debug('use %o %s', path, fn.name || '<anonymous>')

    var layer = new Layer(path, {
      sensitive: this.caseSensitive,
      strict: false,
      end: false
    }, fn);

    layer.route = undefined;

    this.stack.push(layer);
  }

  return this;
};
複製程式碼

最終組裝好的第一層的形式如下

Express原始碼的一些理解

最終效果的列印結果

Express原始碼的一些理解

從這裡我們看到內部巢狀的還是比較多的,內部的stack至少有三層,一開始看到時候還是比較頭暈。

關於路由匹配之後會補上》》》》》to be continued...

總結

總之,要理解這麼多層巢狀的關鍵是需要搞清楚,三層中this的指代,第一層是初始化路由, this指代router物件,第二層是內部自動組裝的get等一些子路由請求,this指代的是express.Router(); 第三層是也是內部自動組裝,不過這一層會放上真正的請求地址,stack放置在layer.route下,this指代的是Route物件(new Route(path);),每一層都有一個stack陣列進行Layer的存放,之後就會通過解析正規表示式的路由規則和監聽的請求地址進行匹配,最終會呼叫Layer內部的handle函式,從而進行頁面互動和渲染。

學習原始碼的過程是痛苦的,但理解完以後會對整個機制有更加深刻的理解,最後如果有什麼不足之處還需要各位大牛的指正。

相關文章