笔记:解读express 4.x源码

此为裁剪过的笔记版本。

原文在此:https://segmentfault.com/a/11…
原文在此: https://cnodejs.org/topic/574…

谢谢@YiQi ,@leijianning 带来的好文章。我稍作修正和兼并,只是为了越发清楚一点点。

基于的版本

tags:4.4.2。

把express代码跑起来

从一个官方示例最先:

var express = require('express');
var app = express();
app.get('/', function(req, res){
  res.send('Hello World');
});
app.listen(3000);

代码运转后,接见localhost:3000显现Hello World。

逐行剖析

起首第一行,典范的Node.js模块载入代码。载入了express框架,我们来看express源代码中的index.js。

module.exports = require('./lib/express');

只是简朴的导入了./lib/express.js,所以继续深挖看此代码。

exports = module.exports = createApplication;

从这里我们能够看出,实例递次的第一行导入了函数createApplication函数。第二行则是运转了这个函数,然后返回值赋给了app。

函数createApplication

函数createApplication代码以下

var EventEmitter = require('events').EventEmitter;
var mixin = require('utils-merge');
var proto = require('./application');
var req = require('./request');
var res = require('./response');
function createApplication() {
  var app = function(req, res, next) {
    app.handle(req, res, next);
  };
  mixin(app, proto);
  mixin(app, EventEmitter.prototype);
  app.request = { __proto__: req, app: app };
  app.response = { __proto__: res, app: app };
  app.init();
  return app;
}

代码的最先定义了一个函数,函数有形参req,res,next为回调函数。函数体只要一条语句,实行app.handle,此函数在application.js文件中定义,此处是经由过程mixin导入,它的作用就是将每对[req,res]举行逐级分发,作用在每一个定义好的路由及中心件上,直到末了完成。接下来会对此函数剖析。

然后来看看中心的两行:

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

函数mixin,从功用上来讲,就是实在就是让app拷贝proto的一切属性,等同于app继续自proto。是的,JavaScript如许的动态言语,能够动态的指定继续的基础类。proto在头部的require处载入的是./lib/application.js文件,个中定义了大部分express的public api,如app.set,app.get,app.use等。响应的,mixin(app, EventEmitter.prototype)等同于继续EventEmitter,从而让app有了事宜处置惩罚的才能。

想要详细相识mixin的同砚,能够看到,此函数为在头部的require处载入的utils-merge模块,它的代码以下

exports = module.exports = function(a, b){
  if (a && b) {
    for (var key in b) {
      a[key] = b[key];
    }
  }
  return a;
};

再来看接下来的两行:


app.request = { __proto__: req, app: app };
app.response = { __proto__: res, app: app };

这里定义了app的request和response对象,运用了对象的字面量示意法,使其离别继续自req(顶部导入的request.js)和res(顶部导入的response.js),并反向引用了app本身。

比云云官方实例中挪用了res.send,此要领就在response.js内定义,由于指定了app.response的原型为response.js,就即是res也有了response.js的悉数属性和要领,天然也就有了send要领。

接下来是app.init();。明显,作用是初始化,做哪些事情呢?

app.init = function(){
  this.cache = {};
  this.settings = {};
  this.engines = {};
  this.defaultConfiguration();
};

设定了cache对象(render的时刻用到),种种setting的存储对象,engines对象(模板引擎),末了举行默许的设置。

好了,createApplication函数就是这些

函数get

实例递次第三行中挪用了从app.get()要领。才函数是动态定义的。定义在此:

methods.forEach(function(method){
  app[method] = function(path){
    if ('get' == method && 1 == arguments.length) return this.set(path);
    this.lazyrouter();    
    var route = this._router.route(path);
    route[method].apply(route, [].slice.call(arguments, 1));
    return this;
  };
});

methods在顶部模块引入中定义,实际上是一个包含各个HTTP要求要领的数组,代码在此https://github.com/jshttp/met… 。数组内包含get,put等元素。

而且get要领是被’重载’的,即当app.get();的参数只要一个时刻,实行的是猎取变量的功用,不然,实行route组件中的route.get要领,将该路由和回调函数(即第二个参数)存储进一个栈中(后续会进一步剖析)。回到本来的题目,在这里,症结是看中心的

this.lazyrouter();

我们看它的详细代码


app.lazyrouter = function() {
  if (!this._router) {
    this._router = new Router({
      caseSensitive: this.enabled('case sensitive routing'),
      strict: this.enabled('strict routing')
    }); 
    this._router.use(query());
    this._router.use(middleware.init(this));
  }
};

此代码在第一次实行时,如果this._route没有定义的话,就定义它,并增加基础的路由。

注重末了一句用到了middleware模块的init要领,继续上代码:

exports.init = function(app){
  return function expressInit(req, res, next){
    if (app.enabled('x-powered-by')) res.setHeader('X-Powered-By', 'Express');
    req.res = res;
    res.req = req;
    req.next = next;
    req.__proto__ = app.request;
    res.__proto__ = app.response;
    res.locals = res.locals || Object.create(null);
    next();
  };
};

expressInit函数是一个中心件,能够给req设置X-Powered-By的值,也会初始化request和response,经由过程设置属性__proto__,把app.request和app.respone继续到request.js和response.js上。

函数listen

最开首的官方示例中另有末了一句app.listen(3000),完成代码以下:

app.listen = function(){
  var server = http.createServer(this);
  return server.listen.apply(server, arguments);
};

实际上是挪用了Node.js原生的http模块的CreatServer要领,代码:

http.createServer(this);

中的this,就是app,也就是createApplication返回的函数,这个函数指向到app.handle(),因而,app.handle()就是一切要求的主进口。

路由模块进场,谈及Router,Route的关联

从新再看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')));
    this._router.use(middleware.init(this));
  }
};

果然是,如果_router不存在,就new一个Router出来,而这个Router就是我们刚才在目次组织中看到的router目次,也就是本日的主角Router模块。

Router.route

继续上边的代码,加载完_router以后,实行了this._router.route(path)如许一行代码,那这行代码做了什么呢?我们在router目次下的index.js中找到了它的完成:

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));
  layer.route = route;
  this.stack.push(layer);
  return route;
};

我们能够看到,这里new了一个Route对象,而且new了一个Layer对象,然后将Route对象赋值给layer.route,末了将这个Layer增加到stack数组中。那这个Route又是什么呢,它和Router模块有什么关联呢,我来讲下我的明白:

  1. Route模块对应的是route.js,主如果来处置惩罚路由信息的,每条路由都邑天生一个Route实例。
  2. Router模块对应的是index.js,Router是一个route的鸠合,在Router模块下能够定义多个route
  3. 每一个express建立的实例都邑懒加载一个_router来举行路由处置惩罚,这个_router就是一个Router范例。

这就是Route和Router的关联。

好了,我们接着看函数get()的代码,拿到route对象以后,经由过程apply的体式格局挪用了route的对应method函数,如果我们如今运用的是get函数,那如今method就即是get。看到这里人人就会发明,express实例在处置惩罚路由的步骤是如许的:

  1. 先建立一个Router对象
  2. 然后用Router对象和对应的path来天生一个Route对象
  3. 末了由Route对象来处置惩罚详细的路由完成

route.method

好了,那接下来我们继续深入研讨,看看route.method终究做了什么,我们找到route.js文件,发明以下的代码:

methods.forEach(function(method){
  Route.prototype[method] = function(){
    var handles = flatten(slice.call(arguments));
    for (var i = 0; i < handles.length; i++) {
      var handle = handles[i];
      if (typeof handle !== 'function') {
        var type = toString.call(handle);
        var msg = 'Route.' + method + '() requires callback functions but got a ' + type;
        throw new Error(msg);
      }
      debug('%s %s', method, this.path);
      var layer = Layer('/', {}, handle);
      layer.method = method;
      this.methods[method] = true;
      this.stack.push(layer);
    }
    return this;
  };
});

本来route和application运用了一样的技能,经由过程轮回methods来动态增加method函数。

我们直接看函数内部完成,起首经由过程入参猎取到handles,这里的handles就是我们定义的路由中心件函数,这里我们能够看到是一个数组,所以我们能够给一个路由增加多个中心件函数。经常使用的设置路由函数是如许的,

route.get(“/hello”,function(){})

然则,为了轻易,设置多个也是能够的:

route.get(“/hello”,function(){},function(){},function(){}))

因而,handles是一个数组。接下来轮回handles,在每一个轮回中应用handle来建立一个Layer对象,然后将Layer对象push到stack中去,这个stack实际上是Route内部保护的一个数组,用来寄存一切的Layer对象。那末,对象Layer是什么东西呢?

我们能够route对象设置Layer的代码:

route.get(“/hello”,function(){})

我们能够app对象设置Layer的代码:

app.get(“/hello”,function(){})

也就是说,在route层面,也能够犹如app一样的设置Layer,因而官方文档中提到了,route被认为是mini app,就是如许来的。

Layer对象

那我们继续往下看,看看layer.js的源代码:

function Layer(path, options, fn) {
  if (!(this instanceof Layer)) {
    return new Layer(path, options, fn);
  }

  debug('new %s', path);
  var opts = options || {};

  this.handle = fn;
  this.name = fn.name || '<anonymous>';
  this.params = undefined;
  this.path = undefined;
  this.regexp = pathRegexp(path, this.keys = [], opts);

  if (path === '/' && opts.end === false) {
    this.regexp.fast_slash = true;
  }
}

上边是Layer的组织函数,我们能够看到这里定义handle,params,path和regexp等几个主要的属性:

  1. handle,它就是我们刚刚在route中建立Layer对象传入的中心件函数
  2. params实在就是req.params
  3. path就是我们定义路由时传入的path。
  4. regexp举行路由婚配的时刻就是靠它来搞定的,而它的值是由pathRegexp得来的,实在这个pathRegexp对应的是一个第三方模块path-to-regexp,它的功用是将path转换成regexp,详细用法人人能够自行检察。

Layer.match()

看完属性,我们再来看看Layer有什么要领:

Layer.prototype.match = function match(path) {
  if (path == null) {
    // no path, nothing matches
    this.params = undefined;
    this.path = undefined;
    return false;
  }
  if (this.regexp.fast_slash) {
    // fast path non-ending match for / (everything matches)
    this.params = {};
    this.path = '';
    return true;
  }
  var m = this.regexp.exec(path);
  if (!m) {
    this.params = undefined;
    this.path = undefined;
    return false;
  }
  // store values
  this.params = {};
  this.path = m[0];
  var keys = this.keys;
  var params = this.params;
  for (var i = 1; i < m.length; i++) {
    var key = keys[i - 1];
    var prop = key.name;
    var val = decode_param(m[i]);
    if (val !== undefined || !(hasOwnProperty.call(params, prop))) {
      params[prop] = val;
    }
  }
  return true;
};

match函数主要用来婚配path的,当我们向express发送一个http要求时,当前要求对应的是哪一个路由,就是经由过程这个match函数来推断的,如果path中带有参数,match还会把参数提取出来赋值给params,所以说match是全部路由中很主要的一点。

Layer.prototype.handle_error = function handle_error(error, req, res, next) {
  var fn = this.handle;

  if (fn.length !== 4) {
    // not a standard error handler
    return next(error);
  }

  try {
    fn(error, req, res, next);
  } catch (err) {
    next(err);
  }
};

这个是毛病处置惩罚函数,特地用来处置惩罚毛病的。

Layer.prototype.handle_request = function handle(req, res, next) {

var fn = this.handle;

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

try {
  fn(req, res, next);
} catch (err) {
  next(err);
}

};

从上边的代码我们能够看到挪用了fn,而这个fn就是layer的handle属性,就是我们定义路由时传入的路由中心件,如今总结下,Layer终究是做什么的呢,它和Route之间的关联怎样。说说我的明白:

  1. 能够发明Route和Layer是一对多的关联,每一个Route都邑保护一个Layer数组
  2. 每一个Route代表一个路由
  3. 每一个Layer对应的是路由的每一个中心件函数。Layer存储了每一个路由的path和handle等信息,而且完成了match和handle的功用。

讲完了Route和Layer的关联,我们再来转头看看Router和Layer的关联,我们再来看看index.js中prop.route的代码:

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));
  layer.route = route;
  this.stack.push(layer);
  return route;
};

从代码我们能够看出来Router每次增加一个route,都邑把route包装到layer中,而且将layer增加到本身的stack中。

那为何要把route包装到layer中呢,前边我们已细致研讨了Layer模块的代码,我们发明Layer具有match和handle的功用,如许我们就能够经由过程Layer的match来举行route的婚配了。

route.dispatch()

这里有一个症结点我们须要迥殊讲解下,上边的代码中在建立Layer对象的时刻传入的handle函数为route.dispatch.bind(route),我们来看看route.js中的route.dispatch:

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();
  if (method === 'head' && !this.methods['head']) {
    method = 'get';
  }
  req.route = this;
  next();
  function next(err) {
    if (err && err === 'route') {
      return done();
    }
    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);
    }
  }
};

我们发明dispatch中经由过程next()猎取stack中的每一个layer来实行响应的路由中心件,如许就保证了我们定义在路由上的多个中心件函数被根据定义的递次顺次实行。到这里我们已知道了单个路由是被怎样实行的,那我们定义的多个路由之间又是怎样被顺次实行的呢,如今我们来看看index.js中的handle函数(有删减):

proto.handle = function handle(req, res, out) {
  // middleware and routes
  var stack = self.stack;
  next();
  function next(err) {
    // 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 (match !== true) {
        continue;
      }
      if (!route) {
        // process non-route handlers normally
        continue;
      }
    }
    // no match
    if (match !== true) {
      return done(layerError);
    }
    // this should be done for the layer
    self.process_params(layer, paramcalled, req, res, function (err) {
      if (err) {
        return next(layerError || err);
      }
      if (route) {
        return layer.handle_request(req, res, next);
      }
      trim_prefix(layer, layerError, layerPath, path);
    });
  }
};

此处代码也是应用next(),来处置惩罚stack中的每一个Layer,这里的stack是Router.stack,stack中存贮了多个route对应的layer

  1. 猎取到每一个layer对象
  2. 用要求的path与layer举行婚配,此处婚配用的是layer.match

3.1 如果能婚配到对应的layer,则取得layer.route
3.2 如果route不为空则实行对应的layer.handle_request()
3.3 如果route为空申明这个layer是经由过程use()增加的非路由中心件

须要迥殊申明的是,如果经由过程use()增加的非路由中心件没有指定path,则会在layer.match中默许返回true,也就是说,没有指定path的非路由中心件会婚配一切的http要求。

总结

我们接下来来从新梳理一下。看看express终究是怎样对http要求举行路由的。

  1. 当客户端发送一个http要求后,会先进入express实例对象对应的router.handle函数中
  2. router.handle函数会经由过程next()遍历stack中的每一个layer举行match
  3. 如果match返回true,则猎取layer.route,实行route.dispatch函数
  4. route.dispatch一样是经由过程next()遍历stack中的每一个layer,然后实行layer.handle_request,也就是挪用中心件函数。
  5. 直到一切的中心件函数被实行终了,全部路由处置惩罚完毕。
    原文作者:1000copy
    原文地址: https://segmentfault.com/a/1190000013462633
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞