1. 程式人生 > >express中介軟體,瞭解一下

express中介軟體,瞭解一下

本篇文章從express原始碼上來理解中介軟體,同時可以對express有更深層的理解

前言

中介軟體函式可以執行哪些任務?

  • 執行任何程式碼。
  • 對請求和響應物件進行更改。
  • 結束請求/響應迴圈。
  • 呼叫堆疊中的下一個中介軟體函式。

我們從一個app.use開始,逐步分析到下一個中介軟體函式的執行。

初始化伺服器

首先從github上下載express原始碼。

建立一個檔案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服務,開啟終端,看看終端日誌執行順序。

從日誌我們可以看出,每次next()之後,都會按照順序依次呼叫下中介軟體函式,然後按照執行順序依次列印A1,B1,C1,此時中介軟體已經呼叫完成,再依次列印C2,B2,A2

目錄結構

--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的檔案。

例項化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.

開啟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框架還是一款非常經典的框架。

以上程式碼純屬個人理解,如有不合適的地方,望在評論區留言。