從源碼上理解express中間件

本篇文章從express源碼上來理解中間件,同時能夠對express有更深層的理解javascript

前言

中間件函數能夠執行哪些任務?java

  • 執行任何代碼。
  • 對請求和響應對象進行更改。
  • 結束請求/響應循環。
  • 調用堆棧中的下一個中間件函數。

咱們從一個app.use開始,逐步分析到下一個中間件函數的執行。git

初始化服務器

首先從github上下載express源碼。github

創建一個文件test.js文件,引入根目錄的index.js文件,實例化express,啓動服務器。正則表達式

let express = require('../index.js');
let app = express()

function middlewareA(req, res, next) {
  console.log('A1');
  next();
  console.log('A2');
}

function middlewareB(req, res, next) {
  console.log('B1');
  next();
  console.log('B2');
}

function middlewareC(req, res, next) {
  console.log('C1');
  next();
  console.log('C2');
}

app.use(middlewareA);
app.use(middlewareB);
app.use(middlewareC);

app.listen(8888, () => {
  console.log("服務器已經啓動訪問http://127.0.0.1:8888");
})

複製代碼

啓動服務器,經過訪問http://127.0.0.1:8888服務,打開終端,看看終端日誌執行順序。express

從日誌咱們能夠看出,每次next()以後,都會按照順序依次調用下中間件函數,而後按照執行順序依次打印A1,B1,C1,此時中間件已經調用完成,再依次打印C2,B2,A2npm

目錄結構

--lib
    |__ middleware
        |__ init.js
        |__ query.js
    |__ router
        |__ index.js
        |__ layer.js
        |__ route.js
    |__ application.js
    |__ express.js
    |__ request.js
    |__ response.js
    |__ utils.js
    |__ view.js

複製代碼

經過實例化的express,咱們能夠看到,index.js文件其實是暴露出lib/express的文件。json

實例化express

express,經過mixin繼承appLication,同時初始化application。數組

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

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

  // expose the prototype that will get set on requests
  app.request = Object.create(req, {
    app: { configurable: true, enumerable: true, writable: true, value: app }
  })

  // expose the prototype that will get set on responses
  app.response = Object.create(res, {
    app: { configurable: true, enumerable: true, writable: true, value: app }
  })

  app.init();
  return app;
}

複製代碼

而mixin是merge-descriptorsnpm模塊。Merge objects using descriptors.bash

打開application.js文件,發現express的實例化源自var app = exports = module.exports = {}
進一步搜索app.use,找到app.use,而app.use又只是嚮應用程序路由器添加中間件的Proxy

/** * Proxy `Router#use()` to add middleware to the app router. * See Router#use() documentation for details. * * If the _fn_ parameter is an express app, then it will be * mounted at the _route_ specified. * * @public */

app.use = function use(fn) {
  var offset = 0;
  var path = '/';

  // 默認path 爲 '/'
  // app.use([fn])
  //判斷app.use傳進來的是不是函數
  if (typeof fn !== 'function') {
    var arg = fn;

    while (Array.isArray(arg) && arg.length !== 0) {
      arg = arg[0];
    }

    // 第一個參數是路徑
    //取出第一個參數,將第一個參數賦值給path。
    if (typeof arg !== 'function') {
      offset = 1;
      path = fn;
    }
  }
  //slice.call(arguments,offset),經過slice轉爲數據,slice能夠改變具備length的類數組。
  //arguments是一個類數組對象。
  
  //處理多種中間件使用方式。
  // app.use(r1, r2);
  // app.use('/', [r1, r2]);
  // app.use(mw1, [mw2, r1, r2], subApp);
  
  var fns = flatten(slice.call(arguments, offset));//[funtion]

  //拋出錯誤
  if (fns.length === 0) {
    throw new TypeError('app.use() requires a middleware function')
  }

  //設置router
  this.lazyrouter();
  var router = this._router;

  fns.forEach(function (fn) {
    // 處理不是express的APP應用的狀況,直接調用route.use。
    if (!fn || !fn.handle || !fn.set) {
    //path default to '/'
      return router.use(path, fn);
    }
    
    debug('.use app under %s', path);
    fn.mountpath = path;
    fn.parent = this;
    router.use(path, function mounted_app(req, res, next) {
      var orig = req.app;
      fn.handle(req, res, function (err) {
        setPrototypeOf(req, orig.request)
        setPrototypeOf(res, orig.response)
        next(err);
      });
    });

    // app mounted 觸發emit
    fn.emit('mount', this);
  }, this);

  return this;
};


複製代碼

定義默認參數offerpath。而後處理fn形參不一樣類型的狀況。將不一樣類型的中間件使用方式的形參轉爲扁平化數組,賦值給fns

forEach遍歷fns,判斷若是fnfn.handlefn.set參數不存在,return出去router.use(path, fn)

不然繼續執行router.use

調用handle函數,執行中間件。

代碼以下:

/** *將一個req, res對分派到應用程序中。中間件執行開始。 *若是沒有提供回調,則默認錯誤處理程序將做出響應 *在堆棧中冒泡出現錯誤時。 */
app.handle = function handle(req, res, callback) {
  var router = this._router;

  // 最後報錯處理error。
  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);
};

複製代碼

惰性添加Router。

從上述代碼中能夠知道,app.use的做用其實是將各類應用函數傳遞給router的一箇中間層代理。

並且,在app.use中有調用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')));
    //初始化router
    this._router.use(middleware.init(this));
  }
};

複製代碼

這裏對Router進行了實例化,同時設置基本的optioncaseSensitive 是否區分大小寫,strict 是否設置嚴格模式。

Router初始化以下:

/**
 * 用給定的「選項」初始化一個新的「路由器」。
 * 
 * @param {Object} [options] [{ caseSensitive: false, strict: false }]
 * @return {Router} which is an callable function
 * @public
 */

var proto = module.exports = function(options) {

  var opts = options || {};

  function router(req, res, next) {
    router.handle(req, res, next);
  }

  // 混合路由器類函數
  setPrototypeOf(router, proto)

  router.params = {};
  router._params = [];
  router.caseSensitive = opts.caseSensitive;
  router.mergeParams = opts.mergeParams;
  router.strict = opts.strict;
  router.stack = [];

  return router;
};

複製代碼

調用app.use時,參數都會傳遞給router.use,所以,打開router/index.js文件,查找router.use

/** *使用給定的中間件函數,具備可選路徑,默認爲「/」。 * Use(如' .all ')將用於任何http方法,但不會添加 *這些方法的處理程序,因此選項請求不會考慮「。use」 *函數,即便它們能夠響應。 *另外一個區別是_route_ path被剝離,不可見 *處處理程序函數。這個特性的主要做用是安裝 *不管「前綴」是什麼,處理程序均可以在不更改任何代碼的狀況下操做 *路徑名。 * * @public */

proto.use = function use(fn) {
  var offset = 0;
  var path = '/';

  // 默認路徑 '/'
  // 消除歧義 router.use([fn])
  // 判斷是不是函數
  if (typeof fn !== 'function') {
    var arg = fn;
    while (Array.isArray(arg) && arg.length !== 0) {
      arg = arg[0];
    }
    // 第一個參數是函數
    if (typeof arg !== 'function') {
      offset = 1;
      path = fn;
    }
  }

  //將arguments轉爲數組,而後扁平化多維數組
  var callbacks = flatten(slice.call(arguments, offset));

  //若是callbacks內沒有傳遞函數,拋錯
  if (callbacks.length === 0) {
    throw new TypeError('Router.use() requires a middleware function')
  }

  //循環callbacks數組
  for (var i = 0; i < callbacks.length; i++) {
    var fn = callbacks[i];
    
    if (typeof fn !== 'function') {
      throw new TypeError('Router.use() requires a middleware function but got a ' + gettype(fn))
    }

    //解析下query和expressInit的含義
    // 添加中間件
    //匿名 anonymous 函數
    debug('use %o %s', path, fn.name || '<anonymous>')

    var layer = new Layer(path, {
      sensitive: this.caseSensitive, //敏感區分大小寫 //默認爲false
      strict: false, //嚴格
      end: false //結束
    }, fn);

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

  return this;
}

複製代碼

router.use的主要做用就是將從app.use中傳遞過來的函數,經過Layer實例化的處理,添加一些處理錯誤處理請求的方法,以便後續調用處理。同時將傳遞過來的path,經過path-to-regexp模塊把路徑轉爲正則表達式(this.regexp),調用this.regexp.exec(path),將參數提取出來。

Layer代碼較多,這裏不貼代碼了,能夠參考express/lib/router/layer.js

處理中間件。

處理中間件就是將放入this,stacknew Layout([options],fn),拿出來依次執行。

proto.handle = function handle(req, res, out) {
  var self = this;

  debug('dispatching %s %s', req.method, req.url);

  var idx = 0;
  //獲取協議與URL地址
  var protohost = getProtohost(req.url) || ''
  var removed = '';
  //是否添加斜槓
  var slashAdded = false;
  var paramcalled = {};

  //存儲選項請求的選項
  //僅在選項請求時使用
  var options = [];

  // 中間件和路由
  var stack = self.stack;
  // 管理inter-router變量
  //req.params 請求參數
  var parentParams = req.params;
  var parentUrl = req.baseUrl || '';
  var done = restore(out, req, 'baseUrl', 'next', 'params');

  // 設置下一層
  req.next = next;

  // 對於選項請求,若是沒有其餘響應,則使用默認響應
  if (req.method === 'OPTIONS') {
    done = wrap(done, function(old, err) {
      if (err || options.length === 0) return old(err);
      sendOptionsResponse(res, options, old);
    });
  }

  // 設置基本的req值
  req.baseUrl = parentUrl;
  req.originalUrl = req.originalUrl || req.url;

  next();

  function next(err) {
    var layerError = err === 'route'
      ? null
      : err;

    //是否添加斜線 默認false
    if (slashAdded) {
      req.url = req.url.substr(1);
      slashAdded = false;
    }

    // 恢復改變req.url
    if (removed.length !== 0) {
      req.baseUrl = parentUrl;
      req.url = protohost + removed + req.url.substr(protohost.length);
      removed = '';
    }

    // 出口路由器信號
    if (layerError === 'router') {
      setImmediate(done, null)
      return
    }

    // 再也不匹配圖層
    if (idx >= stack.length) {
      setImmediate(done, layerError);
      return;
    }

    // 獲取路徑pathname
    var path = getPathname(req);

    if (path == null) {
      return done(layerError);
    }

    // 找到下一個匹配層
    var layer;
    var match;
    var route;

    while (match !== true && idx < stack.length) {
      layer = stack[idx++];
      //try layer.match(path) catch err
      //搜索 path matchLayer有兩種狀態一種是boolean,一種是string。
      match = matchLayer(layer, path);
      route = layer.route;

      if (typeof match !== 'boolean') {
        layerError = layerError || match;
      }

      if (match !== true) {
        continue;
      }

      if (!route) {
        //正常處理非路由處理程序
        continue;
      }

      if (layerError) {
        // routes do not match with a pending error
        match = false;
        continue;
      }

      var method = req.method;
      var has_method = route._handles_method(method);

      // build up automatic options response
      if (!has_method && method === 'OPTIONS') {
        appendMethods(options, route._options());
      }

      // don't even bother matching route
      if (!has_method && method !== 'HEAD') {
        match = false;
        continue;
      }
    }

    // no match
    if (match !== true) {
      return done(layerError);
    }

    //從新賦值router。
    if (route) {
      req.route = route;
    }

    // 合併參數
    req.params = self.mergeParams
      ? mergeParams(layer.params, parentParams)
      : layer.params;
      
    var layerPath = layer.path;

    // 處理參數
    self.process_params(layer, paramcalled, req, res, function (err) {
      if (err) {
        return next(layerError || err);
      }

      if (route) {
        return layer.handle_request(req, res, next);
      }

    // 處理req.url和layerPath,同時對layer中的請求error和handle_error加tryCatch處理。
      trim_prefix(layer, layerError, layerPath, path);
    });
  }

複製代碼

執行proto.handle中間件也就的while循環中的一些核心代碼,每次調用app.use中的回調函數中的next()都會讓idx加一,將stack[idx++];賦值給layer,調用一開始說到的layer.handle_request,而後調用trim_prefix(layer, layerError, layerPath, path),添加一些報錯處理。

trim_prefix函數以下:

function trim_prefix(layer, layerError, layerPath, path) {
    if (layerPath.length !== 0) {
      // Validate path breaks on a path separator
      var c = path[layerPath.length]
      if (c && c !== '/' && c !== '.') return next(layerError)

      // //刪除url中與路由匹配的部分
      // middleware (.use stuff) needs to have the path stripped
      debug('trim prefix (%s) from url %s', layerPath, req.url);
      removed = layerPath;
      req.url = protohost + req.url.substr(protohost.length + removed.length);

      // Ensure leading slash
      if (!protohost && req.url[0] !== '/') {
        req.url = '/' + req.url;
        slashAdded = true;
      }

      // 設置 base URL (no trailing slash)
      req.baseUrl = parentUrl + (removed[removed.length - 1] === '/'
        ? removed.substring(0, removed.length - 1)
        : removed);
    }

    debug('%s %s : %s', layer.name, layerPath, req.originalUrl);

    if (layerError) {
      layer.handle_error(layerError, req, res, next);
    } else {
      layer.handle_request(req, res, next);
    }
  }
};

複製代碼

總結

以上就是經過app.use調用以後,一步步執行中間件函數router.handle

next核心代碼很簡單,可是須要考慮的場景倒是不少,經過此次源碼閱讀,更能進一步的理解express的核心功能。

雖然說日常作項目用到express框架不多,或者能夠說基本不用,通常都是用Koa或者Egg,能夠說基本上些規模的場景的項目用的都是Egg

可是不能否認得是,express框架仍是一款很是經典的框架。

以上代碼純屬我的理解,若有不合適的地方,望在評論區留言。

相關文章
相關標籤/搜索