深刻理解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
相关文章
相关标签/搜索