深刻理解connect/express

其實關於這個話題以前已經提到過了, 也寫過一篇關於express和koa對比的文章, 可是如今回過頭看, 其實仍是挺多錯誤的地方, 好比關於express和koa中間件原理的部分陷入了一個陷阱, 當時也研究了挺久可是沒怎麼理解. 關於這部分其實就是對於設計模式的欠缺了. 關於中間件模式咱們不說那麼多概念或者實現了, 針對代碼說話.express

柿子固然挑軟的捏, express的代碼量不算大, 可是有個更加簡單的connect, 咱們就從connect入手吧.設計模式

花了點時間畫了個示意圖, 可是以前沒怎麼畫過代碼流程圖, 意思一下而已:數組

image

代碼分析

首先咱們看看connect是怎麼使用的:服務器

const connect = require('connect')

const app = connect()

app.use('/', function (req, res, next) {
  console.log('全局中間件')
  next()
  console.log('執行完了')
})

app.use('/bar', function (req, res) {
  console.log('第二個中間件')
  res.end('end')
})

app.listen(8001)
複製代碼

跟express相似, 新建實例, 匹配路由, 很簡潔也頗有效. 上面代碼執行訪問後咱們發現其實next後仍是會回來執行下面的代碼的, 彷佛跟koa的中間件有點相似, 號稱洋蔥型中間件嘛. 結論是否認的, 反正這裏不是與koa進行對比.app

梳理一下代碼結構吧:koa

var proto = {}

var createServer = function () {}

proto.use = function () {}

proto.handle = function () {}

proto.listen = function () {}
複製代碼

主要就是上面這幾個函數, 其餘輔助函數咱們砍掉. 能夠看到咱們用connect主要就是在proto這塊, 讓咱們根據代碼來看咱們啓動一個connect服務器到底發生了哪些事情.函數

首先咱們是新建一個connect實例:ui

var app = connect()
複製代碼

毫無疑問調用的是createServer, 由於這個模塊最終導出的就是它嘛, createServer部分的代碼也很簡單:this

function createServer() {
  function app(req, res, next){ app.handle(req, res, next); }
  merge(app, proto); // 繼承了proto
  merge(app, EventEmitter.prototype); // 繼承了EventEmitter
  app.route = '/';
  app.stack = []; // 暫存路由和方法的地方
  return app;
}
複製代碼

上面有用的部分我已經標出來了, 能夠看出來其實咱們那些經常使用的connect方法都來自proto, 那麼咱們下面主要工做就圍繞着proto來.url

app.use

當咱們想設置某個路由的時候就是調用app.use, 可是可能你們並不太清楚他具體作了什麼事情, 好比下面的代碼:

app.use('/bar', function (req, res) {
  res.end('end')
})
複製代碼

上面已經講了, 有個stack數組是專門用來存放路由和他的方法的, 很容易的就能想到: app.use就是將咱們想的路由和方法推動去等待執行, 實際上也是這樣的:

proto.use = function use(route, fn) {
  var handle = fn;
  var path = route;

  // default route to '/'
  if (typeof route !== 'string') {
    handle = route;
    path = '/';
  }

  // wrap sub-apps
  if (typeof handle.handle === 'function') {
    var server = handle;
    server.route = path;
    handle = function (req, res, next) {
      server.handle(req, res, next);
    };
  }

  // wrap vanilla http.Servers
  if (handle instanceof http.Server) {
    handle = handle.listeners('request')[0];
  }

  // strip trailing slash
  if (path[path.length - 1] === '/') {
    path = path.slice(0, -1);
  }

  // add the middleware
  debug('use %s %s', path || '/', handle.name || 'anonymous');
  this.stack.push({ route: path, handle: handle });

  return this;
};
複製代碼

看上去蠻複雜的, 咱們簡化一下, 不考慮各類異常以及兼容, 默認只能app.use(route, handle)調用:

// 很好嘛 把if都給去掉了就是簡化2333
proto.use = function use(route, fn) {
  var handle = fn;
  var path = route;
  this.stack.push({ route: path, handle: handle });
  return this;
};
複製代碼

簡化後是否是順眼多了, 其實就是維護數組, 固然這樣確定有問題的, 重複路由什麼的就無論了.

中間件的實現

那use實現後其實咱們就有點數了, 中間件如今都在stack裏, 那咱們執行中間件就是針對具體路由來遍歷這個stack嘛, 對的, 就是遍歷stack, 可是connect的中間件事順序執行的, 若是一個個排下來就是全部中間件都會執行一遍, 可能的狀況就是好比一個異常處理的中間件, 我只要在出現異常的時候才須要調用這個中間件.這時候next就上場了, 首先來看看proto.handle實現的幾十行代碼吧:

proto.handle = function handle(req, res, out) {
  var index = 0;
  var protohost = getProtohost(req.url) || '';
  var removed = '';
  var slashAdded = false;
  var stack = this.stack;

  // final function handler
  var done = out || finalhandler(req, res, {
    env: env,
    onerror: logerror
  });

  // store the original URL
  req.originalUrl = req.originalUrl || req.url;

  function next(err) {
    if (slashAdded) {
      req.url = req.url.substr(1);
      slashAdded = false;
    }

    if (removed.length !== 0) {
      req.url = protohost + removed + req.url.substr(protohost.length);
      removed = '';
    }

    // next callback
    var layer = stack[index++];

    // all done
    if (!layer) {
      defer(done, err);
      return;
    }

    // route data
    var path = parseUrl(req).pathname || '/';
    var route = layer.route;

    // skip this layer if the route doesn't match
    if (path.toLowerCase().substr(0, route.length) !== route.toLowerCase()) {
      return next(err);
    }

    // skip if route match does not border "/", ".", or end
    var c = path.length > route.length && path[route.length];
    if (c && c !== '/' && c !== '.') {
      return next(err);
    }

    // trim off the part of the url that matches the route
    if (route.length !== 0 && route !== '/') {
      removed = route;
      req.url = protohost + req.url.substr(protohost.length + removed.length);

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

    // call the layer handle
    call(layer.handle, route, err, req, res, next);
  }

  next();
};
複製代碼

仍是挺長的, 須要簡化, 同理咱們把if都給去掉簡化代碼:

proto.handle = function handle(req, res, out) {
  var index = 0;
  var protohost = getProtohost(req.url) || '';
  var removed = '';
  var slashAdded = false;
  var stack = this.stack;

  // final function handler
  var done = out || finalhandler(req, res, {
    env: env,
    onerror: logerror
  });

  // store the original URL
  req.originalUrl = req.originalUrl || req.url;

  function next(err) {

    // next callback
    var layer = stack[index++];

    // all done 這個不能去
    if (!layer) {
      defer(done, err);
      return;
    }

    // route data
    var path = parseUrl(req).pathname || '/';
    var route = layer.route;

    // skip this layer if the route doesn't match
    if (path.toLowerCase().substr(0, route.length) !== route.toLowerCase()) {
      return next(err);
    }


    // call the layer handle
    call(layer.handle, route, err, req, res, next);
  }

  next();
};
複製代碼

簡化後咱們能夠看到, 其實next是個遞歸, 只要符合條件它會不停地調用自身, 也就是說只要你在中間件裏調用了next它會遍歷stack尋找中間件若是找到了就執行, 若是沒找到就defer(done), 注意proto.handle定義了一個index, 這是尋找中間件的一個索引, next一直須要用到. 這裏可有可無的函數就不提了, 好比getProtohost, 好比call.

app.listen

app.listen其實也很簡單了, 沒法是新建一個http.Server而已, 代碼以下:

proto.listen = function listen() {
  var server = http.createServer(this);
  return server.listen.apply(server, arguments);
};
複製代碼

結束

說到這裏差很少快結束了, 咱們其實能夠知道, connect/express的中間件模型是這樣的:

http.createServer(function (req, res) {
  m1 (req, res) {
    m2 (req, res) {
      m3 (req, res) {}
    }
  }
})
複製代碼

當咱們調用next的時候纔會繼續尋找中間件並調用. 這樣寫出來我本身好像也清楚了不少(逃

image
相關文章
相關標籤/搜索