天天看點

NodeJS架構之Express4.x源碼分析

1.源碼目錄結構

  Express4.x自己實作了一個router元件,實作http請求的順序流程處理,去除了很多綁定的中間件,使代碼更清晰。

  下面是express4.14的檔案結構圖:

NodeJS架構之Express4.x源碼分析

  1.middleware(中間件)下主要有init.js和query.js,init.js的作用是初始化request,response,而query.js中間件的作用是格式化url,将url中的rquest參數剝離,儲存到req.query中;

  2.router檔案夾為router元件,包括index.js、route.js和layer.js,router元件負責中間件的插入和鍊式執行,具體将在下面講解;

  3.express.js和application.js是主要的架構檔案,暴露了express的api;

  4.request.js和response.js提供了一些方法豐富request和response執行個體的功能,如req.is、req.get、req.params、req.originalUrl等;

  5.view.js封裝了模闆渲染引擎,通過res.render()調用引擎渲染網頁。

2. Express啟動過程分析

  先看一下官方示例

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

app.get('/', function(req, res){
  res.send('Hello World');
});

app.listen();
           

  運作後通路localhost:3000顯示Hello World。下面讓我們仔細看一下這段代碼。

  首先第一行

var express = require('express');
           

require(‘express’)載入了express架構,我們來看源代碼中的index.js

'use strict';

module.exports = require('./lib/express');
           

  好吧,還要繼續require,我們來看./lib/express.js

'use strict';

/**
 * 依賴子產品.
 */

var EventEmitter = require('events').EventEmitter;
var mixin = require('merge-descriptors');
var proto = require('./application');
var Route = require('./router/route');
var Router = require('./router');
var req = require('./request');
var res = require('./response');

/**
 * 暴露`createApplication()`.
 */

exports = module.exports = createApplication;

/**
 * Create an express application.
 *
 * @return {Function}
 * @api public
 */

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

  mixin(app, EventEmitter.prototype, false);
  mixin(app, proto, false);

  app.request = { __proto__: req, app: app };
  app.response = { __proto__: res, app: app };
  app.init();
  return app;
}
           

  從 exports = module.exports = createApplication;可以看出

var express =require('express')

最後實際是這個createApplication函數,createApplication就相當于express的’main’函數。

  createApplication的開始定義了一個函數,函數有形參req,res,next為回調函數。函數體隻有一條語句,執行 app.handle , handle 方法在 application.js 檔案中定義, handle 的代碼如下:

/**
 * Dispatch a req, res pair into the application. Starts pipeline processing.
 *
 * If no callback is provided, then default error handlers will respond
 * in the event of an error bubbling through the stack.
 *
 * @private
 */

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)
  });

  // no routes
  if (!router) {
    debug('no routes defined on app');
    done();
    return;
  }

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

  看它的注釋可知app.handle的作用就是将每對 [req,res] 進行逐級分發,作用在每個定義好的路由及中間件上,直到最後完成。

  再看看createApplication方法中間的兩行

mixin(app, EventEmitter.prototype, false);

mixin(app, proto, false);

  mixin是在頭部require載入的merge-descriptors子產品,它的代碼如下

function merge(dest, src, redefine) {
  if (!dest) {
    throw new TypeError('argument dest is required')
  }

  if (!src) {
    throw new TypeError('argument src is required')
  }

  if (redefine === undefined) {
    // Default to true
    redefine = true
  }

  Object.getOwnPropertyNames(src).forEach(function forEachOwnPropertyName(name) {
    if (!redefine && hasOwnProperty.call(dest, name)) {
      // Skip desriptor
      return
    }

    // Copy descriptor
    var descriptor = Object.getOwnPropertyDescriptor(src, name)
    Object.defineProperty(dest, name, descriptor)
  })

  return dest
}
           

  Object.getOwnPropertyNames(src).forEach(function forEachOwnPropertyName(name) {})将src參數的屬性周遊,屬性名稱傳入參數name中,Object.defineProperty(dest, name, descriptor) 則将src的每一個屬性name和name的值descriptor複制到目标參數dest中,是以 mixin(app, proto, false); 的作用即是将proto中所有的property全部導入進app,第三個參數false表示app中已有的屬性不被proto的屬性所覆寫,proto定義了大部分express的public api,如app.set,app.get,app.use…詳見官方的API文檔。 mixin(app, EventEmitter.prototype, false); 則将Node.js的EventEmitter中的原型方法全部導入了app。

  再來看createApplication接下來的兩行

app.request = { __proto__: req, app: app };
app.response = { __proto__: res, app: app };
           

  這裡定義了app的 request 和 response 對象,使其分别繼承自req(頂部導入的 request.js )和res(頂部導入的 response.js ),另外,把app對象指派給app參數是為了後面在 request 和 response 對象中能夠通過 this.app 獲得所建立的express執行個體。

  接下來是

app.init();

  顯然,作用是初始化,做哪些工作呢?

app.init = function(){
  this.cache = {};
  this.settings = {};
  this.engines = {};
  this.defaultConfiguration();
};
           

  設定了cache對象(render的時候用到),各種setting的存儲對象,engines對象(模闆引擎),最後進行預設的配置,代碼有點長這裡就不貼了,就是做一些預設的配置。

看官網示例下一句

app.get(‘/’, function(req, res){

res.send(‘Hello World’);

});

  app.get可以擷取app.set設定的全局變量,也可以設定路由的處理函數,下面是get實作的源碼

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

    this.lazyrouter();

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

  methods是一個數組,存儲了http所有請求的類型,在method子產品裡定義,除了基本的get、post請求外,還有多達十幾種請求,可能是為了相容新的http标準吧。app[method]中,method==’get’且隻有一個參數,則執行set,将route和回調存儲進一個棧中。遇到http請求時觸發執行,app.get也将産生一條路由中間件,執行後傳回浏覽器html頁面。具體的路由元件代碼将在後面分析。

   還有最後一句

app.listen(3000);

  listen方法的代碼如下

app.listen = function listen() {
  var server = http.createServer(this);
  return server.listen.apply(server, arguments);
};
           

  這裡其實是調用了Node.js原生的http子產品的CreatServer方法建立伺服器。

3.中間件

  所謂中間件,就是在收到請求後和發送響應之前這個階段執行的一些函數。

  express對象的use方法可以在一條路由的處理鍊上插入中間件,如

//加載路由
app.use( '/', require( './routes/index' ) );
           

  當你為某個路徑安裝了中間件,則當以該路徑為基礎的路徑被通路時,都會應用該中間件。比如你為“/”設定了中間件,那麼所有請求都會應用該中間件。

  中間件函數的原型如下:

function (req, res, next)

  第一個參數是Request對象req。第二個參數是Response對象res。第三個則是用來驅動中間件調用鍊的函數next,如果你想讓後面的中間件繼續處理請求,就需要調用next方法。

app.static中間件

  Express提供了一個static中間件,可以用來處理網站裡的靜态檔案的GET請求,可以通過express.static通路。express.static的用法如下:

express.static(root, [options])

  第一個參數root,是要處理的靜态資源的根目錄,可以是絕對路徑,也可以是相對路徑。第二個可選參數用來指定一些選項,比如maxAge、lastModified等,一個典型的express.static應用如下:

//全局變量
global.ABSPATH = path.join( __dirname, '/' );

//express應用static中間件
const staticOptions = {
    dotfiles: "ignore", //allow,deny,ignore
    etag: true,
    extensions: false,
    index: "index.html",    //set false to disable directory indexing
    lastModified: true,
    maxAge: ,
    redirect: true,
    setHeaders () {}
};

app.use( express.static( path.join( __dirname, 'public' ), staticOptions ) );
app.use( express.static( path.join( __dirname, 'upload' ), staticOptions ) );
           

  上面這段代碼将相對路徑下的public和upload目錄作為靜态檔案,并設定staticOptions 那些屬性,如Cache-Control頭部的max-age選項為0天。還有其它一些屬性,請對照express.static的文檔來了解。

4.Router元件

  Router是Express中一個非常核心的東西,基本上就是一個簡化版的Express架構。下面我們來一起看看,

app.get()

是如何實作的,之前我們在application.js中已經發現

app.get

的實作代碼:

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

    this.lazyrouter();

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

  從上面的代碼可以看出

app.get()

函數如果參數長度是1,則傳回

app.set()

定義的變量,如果參數長度大于1,則進行路由處理。繼續往下看

this.lazyrouter()

,從名字來看,好像是懶加載router,那我們看看源碼:

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是Router的執行個體,如果_router不存在,就new一個Router出來,而這個Router就是我們剛才在目錄結構中看到的router目錄,也就是今天的主角Router元件。繼續上邊的代碼,加載完_router之後,執行了this._router.route(path)這樣一行代碼,那這行代碼有做了什麼呢,我們再繼續往下挖,我們在router目錄下的index.js中找到了它的實作:

//Create a new Route for the given path.
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;
};
           

  這裡new了一個Route對象,并且new了一個Layer對象,然後将Route對象指派給layer.route,最後将這個Layer添加到stack數組中。我們先來看看Route,這個Route是什麼呢,它和Router元件有什麼關系呢?

  這裡先聲明一下,本文提到的路由容器(Router)代表“router/index.js”檔案的到導出對象,路由中間件(Route)代表“router/route.js”檔案的導出對象。

  首先,Router是怎麼來的呢,Router對象隻會在首次調用lazyrouter時被執行個體化,然後指派給app._router字段。而Route隻是路由中間件,封裝了路由資訊,這裡要特别注意 Router與Route的差別,Router可以看作是一個中間件容器,不僅可以存放路由中間件(Route),還可以存放其他中間件,在lazyrouter方法中執行個體化Router後會首先添加兩個中間件:query和init;而Route 僅僅是路由中間件,封裝了路由資訊。Router和Route都各自維護了一個stack數組,該數組就是用來存放中間件和路由的。

  Router 和 Route 的stack是有差别的,這個差别主要展現在存放的 layer(layer是用來封裝中間件的一個資料結構)不太一樣,如下所示:

NodeJS架構之Express4.x源碼分析

  由于Router.stack中存放的中間件包括但不限于路由中間件,而隻有路由中間件的執行才會依賴與請求method,是以Router.stack裡的layer沒有method屬性,而是将其動态添加到了Route.stack的layer中;layer.route字段也是動态添加的,可以通過該字段來判斷中間件是否是路由中間件。可以通過兩種方式添加中間件:app.use和app[method],前者用來添加非路由中間件,後者添加路由中間件,這兩種添加方式都在内部調用了Router的相關方法來實作:

/*在router目錄下的index.js*/

/*添加非路由中間件*/
proto.use = function use(fn) {
  var offset = ;
  var path = '/';

  /* 此處略去部分代碼 */

  callbacks.forEach(function (fn) {
    if (typeof fn !== 'function') {
      throw new TypeError('Router.use() requires middleware function but got a ' + gettype(fn));
    }
    // 添加中間件
    debug('use %s %s', path, fn.name || '<anonymous>');
    //執行個體化layer對象并進行初始化
    var layer = new Layer(path, {
      sensitive: this.caseSensitive,
      strict: false,
      end: false
    }, fn);

    layer.route = undefined;
    this.stack.push(layer);
  }
  return this;


/*添加路由中間件*/
proto.route = function(path){
  //執行個體化路由對象
  var route = new Route(path);
  //執行個體化layer對象并進行初始化
  var layer = new Layer(path, {
    sensitive: this.caseSensitive,
    strict: this.strict,
    end: true
  }, route.dispatch.bind(route));
  //指向剛執行個體化的路由對象(非常重要),通過該字段将Router和Route關聯來起來
  layer.route = route;
  this.stack.push(layer);
  return route;
};
           

  對于路由中間件,路由容器中的stack(Router.stack)裡面的layer通過route字段指向了路由對象,那麼這樣一來,Router.stack就和Route.stack發生了關聯,關聯後的示意模型如下圖所示:

NodeJS架構之Express4.x源碼分析

  這裡大家就會發現,express執行個體在處理路由的時候,會先建立一個Router對象,然後用Router對象和對應的path來生成一個Route對象,最後由Route對象來處理具體的路由實作。

  好了,那接下來我們繼續深入研究,看看route.method究竟做了什麼,我們找到route.js檔案,發現如下的代碼:

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 callback functions but got a ' + type;
        throw new Error(msg);
      }

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

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

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

    return this;
  };
});
           

  原來route和application運用了同樣的技巧,通過循環methods來動态添加method函數,我們直接看函數内部實作,首先通過入參擷取到handles,這裡的handles就是我們定義的路由中間件函數,這裡我們可以看到是一個數組,是以我們可以給一個路由添加多個中間件函數。接下來循環handles,在每個循環中利用handle來建立一個Layer對象,然後将Layer對象push到stack中去,這個stack其實是Route内部維護的一個數組,用來存放所有的Layer對象。現在你一定想這道這個Layer到底是什麼東西,那我們來看看layer.js的源代碼:

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

  debug('new %s', 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);

  if (path === '/' && opts.end === false) {
    this.regexp.fast_slash = true;
  }
}
           

  上邊是Layer的構造函數,我們可以看到這裡定義handle,params,path和regexp等幾個主要的屬性:

  1.其中最重要的就是handle,它就是我們剛剛在route中建立Layer對象傳入的中間件函數。

  2.params其實就是req.params,至于如何實作的我們可以以後再做探讨,今天先不做說明。

  3.path就是我們定義路由時傳入的path。

  4.regexp對于Layer來說是比較重要的一個屬性,因為下邊進行路由比對的時候就是靠它來搞定的,而它的值是由pathRegexp得來的,其實這個pathRegexp對應的是一個第三方子產品path-to-regexp,它的功能是将path轉換成regexp。

  我們再來看看Layer有什麼方法:

/**
 * Check if this route matches `path`, if so
 * populate `.params`.
 *
 * @param {String} path
 * @return {Boolean}
 * @api private
 */

Layer.prototype.match = function match(path) {
  if (path == null) {
    // no path, nothing matches
    this.params = undefined;
    this.path = undefined;
    return false;
  }

  if (this.regexp.fast_slash) {
    // fast path non-ending match for / (everything matches)
    this.params = {};
    this.path = '';
    return true;
  }

  var m = this.regexp.exec(path);

  if (!m) {
    this.params = undefined;
    this.path = undefined;
    return false;
  }

  // store values
  this.params = {};
  this.path = m[];

  var keys = this.keys;
  var params = this.params;

  for (var i = ; i < m.length; i++) {
    var key = keys[i - ];
    var prop = key.name;
    var val = decode_param(m[i]);

    if (val !== undefined || !(hasOwnProperty.call(params, prop))) {
      params[prop] = val;
    }
  }

  return true;
};
           

  match函數主要用來比對path的,當我們向express發送一個http請求時,目前請求對應的是哪個路由,就是通過這個match函數來判斷的,如果path中帶有參數,match還會把參數提取出來指派給params,是以說match是整個路由中很重要的一點。還有下面一個進行中間件的函數:

/**
 * Handle the request for the layer.
 *
 * @param {Request} req
 * @param {Response} res
 * @param {function} next
 * @api private
 */

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

  if (fn.length > ) {
    // not a standard request handler
    return next();
  }

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

  從上邊的代碼我們可以看到調用了fn,而這個fn就是layer的handle屬性,就是我們定義路由時傳入的路由中間件,到這裡我們總算找到了我們的路由中間件被執行的地方。那Layer和Route之間又有什麼千絲萬縷的聯系呢?

  每個 Route 都會維護一個Layer數組,每一個Layer對應一個中間件函數,Layer存儲了每個路由的path和handle等資訊,并且實作了match和handle的功能,是以可以發現Route和Layer是一對多的關系,每個Route 代表一個路由,而每個Layer對應的是路由的每一個中間件函數。

  講完了Route和Layer的關系,我們再來回頭看看 Router 和Layer的關系,從index.js中prop.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;
};
           

  我們可以看出來Router每次添加一個route,都會把route包裝到layer中,并且将layer添加到自己的stack中,那為什麼要把route包裝到layer中呢,前邊我們已經仔細研究了Layer子產品的代碼,我們發現Layer具有match和handle的功能,這樣我們就可以通過Layer的match來進行route的比對了。這裡有一個關鍵點我們需要特别講解下,上邊的代碼中在建立Layer對象的時候傳入的handle函數為route.dispatch.bind(route),route.dispatch是通過next()擷取stack中的每一個layer來執行相應的路由中間件,這樣就保證了我們定義在路由上的多個中間件函數被按照定義的順序依次執行。

  我們接下來來重新梳理一下router相關的所有内容,看看express究竟是如何對http請求進行路由的。

  當用戶端發送一個http請求後,會先進入express執行個體對象對應的router.handle函數中,router.handle函數會通過next()周遊stack中的每一個layer進行match,如果match傳回true,則擷取layer.route,執行route.dispatch函數,route.dispatch同樣是通過next()周遊stack中的每一個layer,然後執行layer.handle_request,也就是調用中間件函數。直到所有的中間件函數被執行完畢,整個路由處理結束。

5.View的實作

  渲染模闆使用的是 res.render(),它實作總體來說經過三次封裝,進行了一些配置,調用鍊條為

res.render() => app.render() =>view.render()=> require("jade")/reqiure("ejs").render()

  首先看app.engine,将jade或ejs模闆引擎的render函數存入了engines數組中

app.engine = function engine(ext, fn) {
  if (typeof fn !== 'function') {
    throw new Error('callback function required');
  }

  // get file extension
  var extension = ext[] !== '.'
    ? '.' + ext
    : ext;

  // store engine
  this.engines[extension] = fn;

  return this;
};
           

  

app.defaultConfiguration()

(application.js中初始化的一個函數),把View的構造函數儲存。

// default configuration
  this.set('view', View);
           

  

app.render()

将其取出并調用,初始化一個View執行個體,并執行

view.render()

渲染模闆,注意初始化函數将engines傳入了View執行個體,裡面儲存了模闆引擎的render函數。

app.render = function render(name, options, callback) {
  var cache = this.cache;
  var done = callback;
  var engines = this.engines;
  var opts = options;
  var renderOptions = {};
  var view;

  /*此處省略部分代碼*/

  // view
  if (!view) {
    var View = this.get('view');

    view = new View(name, {
      defaultEngine: this.get('view engine'),
      root: this.get('views'),
      engines: engines
    });

    if (!view.path) {
      var dirs = Array.isArray(view.root) && view.root.length > 
        ? 'directories "' + view.root.slice(, -).join('", "') + '" or "' + view.root[view.root.length - ] + '"'
        : 'directory "' + view.root + '"'
      var err = new Error('Failed to lookup view "' + name + '" in views ' + dirs);
      err.view = view;
      return done(err);
    }

    // prime the cache
    if (renderOptions.cache) {
      cache[name] = view;
    }
  }

  // render
  tryRender(view, renderOptions, done);
};
           

  

view.render()

執行的便是模闆引擎的render函數,callback為渲染完成後的回調函數。

View.prototype.render = function render(options, callback) {
  debug('render "%s"', this.path);
  this.engine(this.path, options, callback);
};
           

  總結視圖渲染的流程為, res.render() 調用了 app.render() 。在 app.render() 中,先建立一個 view 對象(相關源碼為 view.js ),然後調用 view.render() 。如果允許緩存,即 app.enabled(‘view cache’) 的話,則會優先檢查緩存,如果緩存中已有相關視圖,則直接取出;否則才會新建立一個視圖對象。

  最後總結一下,其實整個Express執行過程就是往req、res不停地添加和修改屬性;中間件也是通過app作為回調,進而修改req、res;其中app.use和app.static用來添加中間件,app.handle則将每對[req,res]進行逐級分發,作用在每個定義好的路由及中間件上,直至最後完成分發。

繼續閱讀