Express源码与中间件执行

本文详细解读了Express源码中实例化、添加中间件(app.use()和router.use())、路由实例化(Router)以及API请求处理流程。重点讲解了路由匹配、中间件执行顺序和响应结果返回机制。
摘要由CSDN通过智能技术生成

使用了三年多的express,今天坐下来读一读源码

实例化

function createApplication() {
  // 对应http.createServer(requestListener?: RequestListener)入参
  // (req: IncomingMessage, res: ServerResponse) => void
  // 也就是说所有的api请求,都从这里开始进入express
  var app = function(req, res, next) {
    app.handle(req, res, next);
  };
  // 继承EventEmitter
  mixin(app, EventEmitter.prototype, false);
  // 继承application 方法
  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
  // res 对应response.js, 常用的res.send(),res.json()等方法就在这里,后面会把这些方法加到请求的res中,参见lib/middleware/init.js expressInit 中间件
  app.response = Object.create(res, {
    app: { configurable: true, enumerable: true, writable: true, value: app }
  })

  app.init();
  return app;
}

添加中间件

app.use() 和 router.use()

// 删除了部分不那么重要的代码
app.use = function use(fn) {
  var offset = 0;
  var path = '/';

  if (typeof fn !== 'function') {
    var arg = fn;
    // 这行没懂,没遇到过这样use方式
    while (Array.isArray(arg) && arg.length !== 0) {
      arg = arg[0];
    }
    // first arg is the path
    if (typeof arg !== 'function') {
      offset = 1;
      path = fn;
    }
  }
  var fns = flatten(slice.call(arguments, offset));
  // 初始化路由,加了两个系统自带的路由 expressInit 和 query(解析url,获取query参数)
  this.lazyrouter();
  var router = this._router;

  fns.forEach(function (fn) {
    // non-express app
    if (!fn || !fn.handle || !fn.set) {
      // 最终调用这个方法添加中间件
      return router.use(path, fn);
    }
	// 以下什么时候会执行,不懂?
    debug('.use app under %s', path);
    fn.mountpath = path;
    fn.parent = this;

    // restore .app property on req and res
    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);
      });
    });

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

  return this;
};

疑惑点:1、为什么这样用slice.call(arguments, offset),

// router/index.js  router.use()
proto.use = function use(fn) {
  var offset = 0;
  var path = '/';
  // 删除一段代码,因为跟app.use()一样
  var callbacks = flatten(slice.call(arguments, offset));

  for (var i = 0; i < callbacks.length; i++) {
    var fn = callbacks[i];
	// 删除一段代码,判断fn是否是函数
    var layer = new Layer(path, {
      sensitive: this.caseSensitive,
      strict: false,
      end: false
    }, fn);
	// 关键代码,与router.route的区别所在
    layer.route = undefined;
	// 所有中间件都将加入到stack中
    this.stack.push(layer);
  }
  return this;
};

添加路由

实例化Router

var proto = module.exports = function(options) {
  var opts = options || {};

  function router(req, res, next) {
    router.handle(req, res, next); // 进入路由第一步执行
  }

  // mixin Router class functions
  setPrototypeOf(router, proto)

  router.params = {};
  router._params = [];
  router.caseSensitive = opts.caseSensitive;
  router.mergeParams = opts.mergeParams;
  router.strict = opts.strict;
  router.stack = []; // 初始化了个stack哟

  return router;
};

router.route()

有时候想知道router.get方法是怎么实现的时,你会发现源码中找不到get和post方法,囧

// create Router#VERB functions
// methods 中包含get、post等常用方法名,这就是为啥源码里面找不到post和get方法的原因,原来藏这了
// 由此可见,这些方法实现完全一模一样,只是名字不同而已,那get和post的区别是在哪里处理的呢?现在我也不知道,继续往下看
// router/index.js
methods.concat('all').forEach(function(method){
  proto[method] = function(path){
    var route = this.route(path)
    // 这行代码在干啥呢?调用Route类下面的[mehtod]方法
    route[method].apply(route, slice.call(arguments, 1));
    return this;
  };
});

/*
 * Create a new Route for the given path.
 * Each route contains a separate middleware stack and VERB handlers.
 */
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));
  // 与router.use()不同的地方,route绑定到layer
  layer.route = route;

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

// 实例化Route
function Route(path) {
  this.path = path;
  this.stack = []; // 这里又初始化了个 stack

  debug('new %o', path)

  // route handlers for various http methods
  this.methods = {};
}
// 综合Router 和 Route,每个Router 和 Route 都有个stack数组

// router/route.js
// 这里才是真正的router.get/post方法的实现
methods.forEach(function(method){
  Route.prototype[method] = function(){
    var handles = flatten(slice.call(arguments));
	// 一个路由有多个处理方法,依次加到当前route的stack里面去,也就意味着只能依次调用了
    for (var i = 0; i < handles.length; i++) {
      var handle = handles[i];
	  // 删除handle是否是函数的判断
      var layer = Layer('/', {}, handle);
      layer.method = method;

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

以上就是中间件和路由的添加过程了,常见的还有app.all() 等方法,其实都是router.use() 的封装

接下来,就是最重要API请求的处理流程了!!!

API请求处理流程

回到Express实例化的地方,express实例就是http.createServer()的参数,自然,API请求最先调用的就是app.handle()了

// application.js
app.handle = function handle(req, res, callback) {
  var router = this._router;

  // final handler
  // callback是undefined, 
  // finalhandler 就是匹配不到
  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);
};
// 最核心的代码,next的实现,也是中间件的执行过程
proto.handle = function handle(req, res, out) {
  var self = this;
  var idx = 0;
  var protohost = getProtohost(req.url) || ''
  var removed = '';
  var slashAdded = false;
  var paramcalled = {};
  // middleware and routes
  var stack = self.stack;
  // manage inter-router variables
  var parentParams = req.params;
  var parentUrl = req.baseUrl || '';
  var done = restore(out, req, 'baseUrl', 'next', 'params');

  // setup next layer
  req.next = next;
  // 删除一段options请求的处理
  // setup basic req values
  req.baseUrl = parentUrl;
  req.originalUrl = req.originalUrl || req.url;

  next();
  // next方法原来在这,害我找半天
  // next() 可以传入报错信息
  function next(err) {
    var layerError = err === 'route'
      ? null
      : err;

    // remove added slash
    if (slashAdded) {
      req.url = req.url.substr(1);
      slashAdded = false;
    }

    // restore altered req.url
    if (removed.length !== 0) {
      req.baseUrl = parentUrl;
      req.url = protohost + removed + req.url.substr(protohost.length);
      removed = '';
    }

    // signal to exit router
    // 这个信号从哪来?这个是开发者调用next方法的时候传进来的,意味着告诉express直接退出这级router,不再往下匹配了
    if (layerError === 'router') {
      setImmediate(done, null)
      return
    }

    // no more matching layers
    if (idx >= stack.length) {
      setImmediate(done, layerError);
      return;
    }

    // get pathname of request
    var path = getPathname(req);

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

    // find next matching layer
    var layer;
    var match;
    var route;
	// 查找匹配的中间件
    while (match !== true && idx < stack.length) {
      layer = stack[idx++];
      match = matchLayer(layer, path); // 路由匹配方法,待下面详解,先略过
      route = layer.route;

      if (typeof match !== 'boolean') {
        // hold on to layerError
        layerError = layerError || match;
      }

      if (match !== true) {
        continue;
      }
	  
      if (!route) {
        // process non-route handlers normally
        continue;
      }

      if (layerError) {
        // routes do not match with a pending error
        match = false;
        continue;
      }
      // 删除method === options和head时的处理
    }

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

    // store route for dispatch on change
    if (route) {
      req.route = route;
    }

    // Capture one-time layer values
    req.params = self.mergeParams
      ? mergeParams(layer.params, parentParams)
      : layer.params;
    var layerPath = layer.path;

    // this should be done for the layer
    // 读取url里面的params
    self.process_params(layer, paramcalled, req, res, function (err) {
      if (err) {
        return next(layerError || err);
      }
	  // route不为空,说明是单个route,直接将该route下面的handles执行完
      if (route) {
        return layer.handle_request(req, res, next);
      }
	  // route为null,说明下面可能还有子路由,所以得剔除掉这一级匹配到的路径,继续往下匹配
      trim_prefix(layer, layerError, layerPath, path);
    });
  }
};

// 然后就到了 layer.handle_request方法了,继续挖
// router/layer.js

Layer.prototype.handle_request = function handle(req, res, next) {
  // 这个handle 是啥呢,就是实例化Layer的回调函数,往上面看中间件和路由的添加代码
  // handle函数 或者 dispatch函数
  var fn = this.handle;

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

  try {
    fn(req, res, next);
  } catch (err) {
  	// 处理出错了,就抛出去了,但是因为fn里面可能有异步,这里可能捕获不到
    next(err);
  }
};

/**
 * dispatch req, res into this route
 * @private
 */
// router/route.js
Route.prototype.dispatch = function dispatch(req, res, done) {
  var idx = 0;
  var stack = this.stack;
  if (stack.length === 0) {
    return done();
  }
  var method = req.method.toLowerCase();
  req.route = this;

  next();

  function next(err) {
    // signal to exit route
    // 这个是express 埋的一个点,可以提前终止后续handles的执行
    if (err && err === 'route') {
      return done();
    }

    // signal to exit router
    if (err && err === 'router') {
      return done(err)
    }

    var layer = stack[idx++];
    if (!layer) {
      return done(err);
    }

    if (layer.method && layer.method !== method) {
      return next(err);
    }

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

以上就是中间件和路由的匹配和执行过程,一直往下匹配(前提是一直next了,如果那级没next那就停了),知道中间件匹配完了,或者res.send/json等方法返回结果

API请求结果返回

常见的send、json等方法都在 response.js文件中,在expressInit中间件中会挂载到res中

res.send = function send(body) {
  var chunk = body;
  var encoding;
  var req = this.req;
  var type;

  // settings
  var app = this.app;
  // 删除一段deprecate代码
  switch (typeof chunk) {
    // string defaulting to html
    case 'string':
      if (!this.get('Content-Type')) {
        this.type('html');
      }
      break;
    case 'boolean':
    case 'number':
    case 'object':
      if (chunk === null) {
        chunk = '';
      } else if (Buffer.isBuffer(chunk)) {
        if (!this.get('Content-Type')) {
          this.type('bin');
        }
      } else {
        return this.json(chunk);
      }
      break;
  }
  // write strings in utf-8
  // 略

  // determine if ETag should be generated
  var etagFn = app.get('etag fn')
  var generateETag = !this.get('ETag') && typeof etagFn === 'function'

  // populate Content-Length
  var len
  if (chunk !== undefined) {
    if (Buffer.isBuffer(chunk)) {
      // get length of Buffer
      len = chunk.length
    } else if (!generateETag && chunk.length < 1000) {
      // just calculate length when no ETag + small chunk
      len = Buffer.byteLength(chunk, encoding)
    } else {
      // convert chunk to Buffer and calculate
      chunk = Buffer.from(chunk, encoding)
      encoding = undefined;
      len = chunk.length
    }

    this.set('Content-Length', len);
  }

  // populate ETag
  var etag;
  if (generateETag && len !== undefined) {
    if ((etag = etagFn(chunk, encoding))) {
      this.set('ETag', etag);
    }
  }

  // freshness
  if (req.fresh) this.statusCode = 304;

  // strip irrelevant headers
  if (204 === this.statusCode || 304 === this.statusCode) {
    this.removeHeader('Content-Type');
    this.removeHeader('Content-Length');
    this.removeHeader('Transfer-Encoding');
    chunk = '';
  }

  if (req.method === 'HEAD') {
    // skip body for HEAD
    this.end();
  } else {
    // respond
    // 返回数据,这个方法是 来自于ServerResponse-> OutgoingMessage -> stream.Writable
    this.end(chunk, encoding);
  }

  return this;
};

至此,一个API请求就结束了!!!

但是如果是返回文件呢?res.sendFile方法什么实现的呢?这个还是比较复杂的,等看完stream相关内容之后再看。

最后在看下中间件handles执行顺序的示例

router.get('/test1', 
    (req, res, next) => {
        console.log('handle 1 start');
        next('route'); // handle 2 不会执行了
        console.log('handles 1 finished');
    }, 
    async (req, res, next) => {
        try {
            console.log('handle 2 start');
            next();
            console.log('handle 2 finished');
        } catch (error) {
            return res.json({ error: 'Internal Server Error' });
        }
    }
);

router.get('/test1', 
    (req, res, next) => {
        console.log('handle 3 start');
        next('route'); // handle 4 不会执行了
        console.log('handle 3 finished');
    }, 
    async (req, res, next) => {
        try {
            console.log('handle 4 start');
            console.log('handle 4 finished');
            res.json({ data: '' });
        } catch (error) {
            return res.json({ error: 'Internal Server Error' });
        }
    }
);

router.get('/test1', (req, res, next) => {
    res.json('结束了');
});

GET /test1时,输出结果如下

handle 1 start
handle 3 start
handles 3 finished
handles 1 finished

next(‘route’) 阻止了handle 2、4 的调用,但是并不会阻止3的调用

再看一个next(‘router’)的示例

let router2 = express.Router();
router2.get('/demo', 
    (req, res, next) => {
        console.log('handle 5 start');
        next();
        console.log('handles 5 finished');
    }, 
    async (req, res, next) => {
        try {
            console.log('handle 6 start');
            next('router'); // handle 7 不会执行了
            console.log('handles 6 finished');
        } catch (error) {
            return res.json({ error: 'Internal Server Error' });
        }
    }
);
router2.get('/demo',(req, res, next) => {
        console.log('handle 7 start');
        next();
        console.log('handles 7 finished');
});
router.use('/test2', router2);

router.use('/test2', (req, res, next) => {
    console.log('handle 9 start');
    next();
    console.log('handles 9 finished');
});

GET /test2/demo时,输出结果如下

handle 5 start
handle 6 start
handles 6 finished
handles 5 finished
handle 9 start
handles 9 finished

next(‘router’) 阻止了handle 7 以及router2 下面所有的route调用,相当于跳出了router2,但不影响handle 9的执行。

express的问题

由于中间件串联执行的关键方法next(),没有封装成了promise,也就意味着,一旦handles里面有异步方法,next() 之后的方法并不会等待这些异步方法执行完毕再执行!!!
这就破坏了“洋葱圈”模型了,如果你想等next()执行完再做一些操作就没办法搞了。。。
最常用的打印api log的中间件morgan就只能采用监听scoket断开的方式打印log。

这时候,Koa 就该登场了,简单来说,koa把中间件执行过程全部promise化了,只要await一下next(),就完美了。

好了,express的源码解析就到这了,算是理清流程了。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值