Skip to content

Express源码解读04 - 路由

1. 路由基本原理

Express里面,我们通常会定义一堆路由,然后,当请求进来的时候,会去我们定义好的路由里面通过methodpath匹配,匹配到后就执行里面的请求处理函数。

Express的路由里面有三个构造函数,分别是RouterRouteLayer,它们的实例分别是routerroutelayer其中:

  • Router

    • Router里面维护了一个路由数组stack,存放着一个个的layer
    • Express里面定义的每个路由,在routerstack里面都会有对应的一个layer
    • 在每个layer里面,存放着path(请求路径)和一个route处理函数dispatch,当路径匹配成功后,就会调用dispatch进入到自己的route里面。
    • 每个layer上面,都有一个自己的route
  • Route

    • Route里面也维护了一个stack,也是存放着一个个routelayer
    • Express一个路由的处理函数可以有很多个,每一个处理函数就会对应一个layer
    • route里面的layer里面,存放的有method(请求方法),和一个个事件处理函数handler,调用dispatch方法可以跳出route直接进入router里面的下一个layer.
  • Layer

    • RouterRoute里面都有Layer,他们是同一个类,只是作用不同,源码里面只是复用了代码而已。

image.png

2. 路由懒加载

为了性能考虑,Express的路由不会在创建应用的时候就加载,而是在需要使用的时候才会加载.比如,在自己开发的项目中定义了app.get(),app.use(),或者,直接使用路由模块express.Router()时候, 才会进行路由的加载.

js
app.lazyrouter = function lazyrouter() {
  if (!this._router) { // 若app下还没有路由示例,就实例化一个路由并挂载到app上
    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));
  }
};

new Router的过程先放一边,后面分析.

可以看到_routerRouter的实例.并挂载到了app上面.

3. 路由原理解析

Express里面内置了路由模块,当请求进来时,此时内部已经定义好了各种路由,源码lib/application.js文件里面定义了app.handle方法,请求会首先走进这个方法,方法如下:

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

  // 没有匹配到路由,会走done方法,返回 Connot GET /xxx
  var done =
    callback ||
    finalhandler(req, res, {
      env: this.get("env"),
      onerror: logerror.bind(this),
    });

  if (!router) {
    debug("no routes defined on app");
    done();
    return;
  }

  // 进入路由匹配和业务逻辑处理
  router.handle(req, res, done);
};

源码里面有两个概念:应用、路由,请求进来会先将请求交给路由处理,若是有对应的路由则由路由处理,但是若某个路由匹配不上,会返回Connot <method> /xxxx,这个处理逻辑是应用处理的。

那么现在先来看一下路由是如何进行匹配的。

1. 路由匹配原理

Express里面,有路由,中间件,其中路由的使用方式可以直接通过app.getapp.post等方式来使用也可以通过const Router = express.Router来通过router的方式单独使用路由。中间件app.use的使用方式和路由很相似,可以带上路径,当带路径时候,不区分请求方式,只要能匹配上路径的路由就能进入这个中间件。app.get等路由方法,处理路径外,可以有很多个参数,每个参数的回调可以有第三个参数next,那么这些API的使用在Express里面核心原理都是Router的原理。

例如:

js
// 1. 正常请求
app.get('/login',function(req,res) {
  // do something	
})

// 1.1 多个参数的路由
app.get('/login',function(req,res,next) {
  // do something	
  next()
},function(req,res) {
  // do something
})

// 2. 中间件
app.use(function(req,res,next) {
  // do something
})

// 3. 路由
const router = express.Router()
router.get('/user/login',function(req,res) {
  // do something
})

源码lib/application.js里面,通过methods模块,在app上循环定义了各种请求方法对应的匹配逻辑。

js
methods.forEach(function (method) {
  app[method] = function (path) {
    // 这里是用来获取app上面的一些属性值的逻辑,与路由匹配无关
    if (method === "get" && arguments.length === 1) {
      // app.get(setting)
      return this.set(path);
    }

    this.lazyrouter();

    var route = this._router.route(path); // 用path在内部实例化外层layer和route
    route[method].apply(route, slice.call(arguments, 1)); // 进入route匹配method
    return this;
  };
});

可以看到,首先会根据请求路径得到route,从上面可以知道,routeRoute的实例,因此可以知道。我们定义的每一个路由,其实都有于之对应的route,并且是根据path来进行区分的,所以路由定义时候,若多个路由相同的情况下,定义到上面的路由会覆盖下面的路由

下面来看一下_router里面的逻辑

源码lib/router/index.js里面,是定义路由router入口的文件:

js
// proto是Router.prototype 项目内定义的每一个路由都会走一次这个方法
proto.route = function route(path) {
  var route = new Route(path); // 每一个真是的路由都会有一个自己的 route

  var layer = new Layer(path, {  // 每一个路由都会有一个layer
    sensitive: this.caseSensitive,
    strict: this.strict,
    end: true
  }, route.dispatch.bind(route));

  layer.route = route; // layer上面有route
//  this 代表 router
  this.stack.push(layer); // Express路由模块维护一个stack,也就是路由数组,数组内存放的是一个个的layer
  return route; // 最终返回得到route
};

上面的代码是var route = this._router.route(path)这行代码的内部逻辑。可以看到,面在Router原型上面有一个route方法,这个方法会得到一个Route的实例route,并将其返回,另外,还实例化了一个Layer得到layer,并且每个路由都会对应一个layerExpress路由是放到一个数组stack内进行维护的,数组内的每一项是一个个的layer

既然数组内的每一项是一个个的layer,那么我们来看一下router数组里面的layer里面到底有什么,还有route.dispatch这个方法具体逻辑。

1. layer里面有什么?

看一下layer实例化时候传入的参数,有path{sensitive: this.caseSensitive, strict: this.strict, end: true} route.dispatch.bind(route)这是哪个参数。

看一下layer内部,位置lib/router/layer.js

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

  var opts = options || {};

  this.handle = fn;
  this.name = fn.name || '<anonymous>';
  this.params = undefined;
  this.path = undefined;
  this.regexp = pathRegexp(path, this.keys = [], opts); // 第三方模块, 用于返回路径对应的正则

  // set fast path flags
  this.regexp.fast_star = path === '*'
  this.regexp.fast_slash = path === '/' && opts.end === false
}

layer内部的逻辑其实就是在layer上有path属性,layer原型上还有一个match方法,用来匹配路径的,里面处理了路径上带参数的情况,但是你最终逻辑最后都是返回truefalse,也就是路径是否匹配成功;

js
Layer.prototype.match = function match(path) {
  var match
  if (path != null) {
    // fast path non-ending match for / (any path matches)
    if (this.regexp.fast_slash) {
      this.params = {}
      this.path = ''
      return true
    }

    // fast path for * (everything matched in a param)
    if (this.regexp.fast_star) {
      this.params = {'0': decode_param(path)}
      this.path = path
      return true
    }

    // match the path
    match = this.regexp.exec(path)
  }

  if (!match) {
    this.params = undefined;
    this.path = undefined;
    return false;
  }

  // store values
  this.params = {};
  this.path = match[0]

  var keys = this.keys;
  var params = this.params;

  for (var i = 1; i < match.length; i++) {
    var key = keys[i - 1];
    var prop = key.name;
    var val = decode_param(match[i])

    if (val !== undefined || !(hasOwnProperty.call(params, prop))) {
      params[prop] = val;
    }
  }
  return true;
};

当然,只有外层的layer,也就是router数组里面的layer才会匹配路径。内层的layer是不会匹配路径的,因此,match方法只有外层会用到。

2. route.dispatch具体逻辑

layer里面,只有当请求处理函数的参数不等于4个或者小于3个的时候,才会调用route.dispatch方法,并且传入的参数会根据当前的处理函数类型而定。那么可以知道,layer里面的这两个方法的逻辑定是为内层layer做准备的,因为,外层layer是没有存请求处理函数的,存的只是pathroute

js
// 错误处理函数
Layer.prototype.handle_error = function handle_error(error, req, res, next) {
  var fn = this.handle; // route.dispatch
  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;  // route.dispatch
  if (fn.length > 3) {
    // not a standard request handler
    return next();
  }
  try {
    fn(req, res, next); // route.dispatch
  } catch (err) {
    next(err);
  }
};

route.dispatch逻辑在源码lib/router/route.js里面:

js
Route.prototype.dispatch = function dispatch(req, res, done) {
  var idx = 0; // 用于标识当前是第几个layer
  var stack = this.stack;
  var sync = 0

  if (stack.length === 0) { // 没有定义路由,直接跳出router,由应用返回 Cannot GET /
    return done();
  }

  var method = req.method.toLowerCase(); // http请求方式默认是大写的,先转为小写,方便后续比较
  if (method === 'head' && !this.methods['head']) {
    method = 'get';
  }

  req.route = this;

  next();

  function next(err) { 
    // signal to exit route
    if (err && err === 'route') return done();
    // signal to exit router
    if (err && err === 'router')  return done(err)
    // max sync stack
    if (++sync > 100) return setImmediate(next, err) // 递归的数量大于100此都没匹配上

    var layer = stack[idx++] // 当前的layer

    // end of layers
    if (!layer) return done(err) // 匹配完都没匹配到,就跳出路由

    if (layer.method && layer.method !== method) { // 根据每个路由的请求方法匹配,
      next(err) // 方法没匹配到,就继续递归匹配下一个layer
    } else if (err) {  
      layer.handle_error(err, req, res, next);// 匹配到了但属于错误处理的
    } else {
      layer.handle_request(req, res, next); // 匹配成功,且属于正常请求
    }

    sync = 0 // 匹配结束后,将sync置为初始值
  }
};

3. 问:什么时候调用的route.dispatch?

我们来捋一捋,请求进来时候,先走的lib/application.js里面的app.handle方法:

js
app.handle = function handle(req, res, callback) {
  var router = this._router;
  // 用于返回 Connot GET /xxx
  var done = callback || finalhandler(req, res, { 
      env: this.get("env"),
      onerror: logerror.bind(this),
    });
  if (!router) return done(); // 没有定义路由,返回找不到路径 Connot GET /xxx

  router.handle(req, res, done); // Router.prototype.handler
};

然后调用router.handle方法,router.handle方法内核心逻辑,就是匹配routerstack内的每一个layerpath,匹配上后,就会调用这个layer对应routedispatch方法进入到route内部,router.handle源码如下:

js
proto.handle = function handle(req, res, out) { // 这里的ourt其实就是application里面的done
  var self = this;
  var idx = 0; // 用于记录当前是第几个layer(外层)
  var protohost = getProtohost(req.url) || ''
  var removed = '';
  var slashAdded = false;
  var sync = 0
  var paramcalled = {};

  // store options for OPTIONS request
  // only used if OPTIONS request
  var options = [];

  // 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;

  // for options requests, respond with a default if nothing else responds
  if (req.method === 'OPTIONS') { // 处理 options请求
    done = wrap(done, function(old, err) {
      if (err || options.length === 0) return old(err);
      sendOptionsResponse(res, options, old);
    });
  }

  // setup basic req values
  req.baseUrl = parentUrl;
  req.originalUrl = req.originalUrl || req.url;

  next();

  function next(err) {
    var layerError = err === 'route'
      ? null
      : err;

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

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

    // signal to exit router
    if (layerError === 'router') return  setImmediate(done, null);
    // no more matching layers
    if (idx >= stack.length)  return  setImmediate(done, layerError);
    // max sync stack
    if (++sync > 100) return setImmediate(next, err);

    // 拿到请求路径
    var path = getPathname(req);

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

    // 找到下一个匹配的layer
    var layer, match, route;

    // 当没匹配到路径并且还没到最后一个layer,则继续匹配
    while (match !== true && idx < stack.length) {
      layer = stack[idx++];
      match = matchLayer(layer, path); // 用路径匹配layer 要么得到boolean,要么得到error
      route = layer.route;

      if (typeof match !== 'boolean') { // 得到的路径匹配结果不是true false,就是用路径匹配layer时候抛错了,此时match是错误
        layerError = layerError || match;
      }
       // 用path匹配layer出错时,开始下一轮循环匹配
      if (match !== true) continue; 
      // layer上没有route,开始下一轮循环(正常处理非路由处理程序)
      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); // 判断route里面是否有对应请求方式,返回boolean

      // 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;
      }
    }

    // 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
    self.process_params(layer, paramcalled, req, res, function (err) {
      if (err) {
        next(layerError || err)
      } else if (route) {
        layer.handle_request(req, res, next)
      } else {
        trim_prefix(layer, layerError, layerPath, path)
      }

      sync = 0
    });
  }

  function trim_prefix(layer, layerError, layerPath, path) {
    if (layerPath.length !== 0) {
      // Validate path is a prefix match
      if (layerPath !== path.slice(0, layerPath.length)) {
        next(layerError)
        return
      }

      // Validate path breaks on a path separator
      var c = path[layerPath.length]
      if (c && c !== '/' && c !== '.') return next(layerError)

      // Trim off the part of the url that matches the route
      // 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.slice(protohost.length + removed.length)

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

      // Setup 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);
    }
  }
};