当前位置: 首页 > 文档资料 > 阅读 express 源码 >

2.6 路由

优质
小牛编辑
132浏览
2023-12-01

2.6.1 router/index.js

2.6.1.1 全局变量

声明了一些全局变量,objectRegExp 是一个正则表达式,toString 之后用来判断是否是对象。其他的就是保存了一下函数引用。

var objectRegExp = /^\[object (\S+)\]$/;
var slice = Array.prototype.slice;
var toString = Object.prototype.toString;
2.6.1.2 设置导出

默认导出的是一个函数,并且调用这个函数可以得到 router,且用 proto 保存一下这个函数。router 函数代理的 router.handle 方法,并且设置 router 的原型链为 proto,然后初始化 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 = [];

  return router;
};
2.6.1.3 param 参数方法

在阅读此方法之前,大家可以先看一下 param 的 API 文档,看一下它是如何工作的,这样便于我们理解函数的功能,进而理解函数的逻辑。

查看一个函数,我们以理解他的功能为优先。

proto.param = function param(name, fn) {
  // 假如第一个参数是一个函数,说明是自定义函数,直接放到 this._params 数组里面。
  if (typeof name === 'function') {
    deprecate('router.param(fn): Refactor to use path params');
    this._params.push(name);
    return;
  }

  // 函数的全局变量,用 params 保存一下 this._params
  var params = this._params;
  var len = params.length;
  var ret;

  if (name[0] === ':') { // 假如 name 以 :冒号开头,则去掉
    deprecate('router.param(' + JSON.stringify(name) + ', fn): Use router.param(' + JSON.stringify(name.substr(1)) + ', fn) instead');
    name = name.substr(1);
  }

  for (var i = 0; i < len; ++i) { // 执行 params(this._params) 里面的每一个函数,以 name 与 fn 传递进去,假如有返回值,则返回值做为新的 fn 作为参数传递。
    if (ret = params[i](name, fn)) {
      fn = ret;
    }
  }

  // ensure we end up with a
  // middleware function
  if ('function' !== typeof fn) { // 确保 fn 是一个 middleware 函数
    throw new Error('invalid param() call for ' + name + ', got ' + fn);
  }

  (this.params[name] = this.params[name] || []).push(fn); // 当 this.params[name] 不为空的时候,直接push fn 否则就等于 [fn]
  return this;
};
2.6.1.4 handle 方法

handle 方法是 router 里面的入口函数。下面我将解释注释在代码中。

proto.handle = function handle(req, res, out) {
  var self = this; // 保存 this

  debug('dispatching %s %s', req.method, req.url);

  var idx = 0;
  var protohost = getProtohost(req.url) || '' // 从 url 中拿到协议
  var removed = '';
  var slashAdded = false;
  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; // 保存一些 req 上面的参数
  var parentUrl = req.baseUrl || '';
  var done = restore(out, req, 'baseUrl', 'next', 'params'); // 保存一下变量,方法如下


function restore(fn, obj) {
  var props = new Array(arguments.length - 2); // 长度为3 的空数组
  var vals = new Array(arguments.length - 2); // 长度为3 的空数组

  for (var i = 0; i < props.length; i++) {
    props[i] = arguments[i + 2]; // 此时 props = ['baseUrl', 'next', 'params'];
    vals[i] = obj[props[i]]; // 此时 vals = [req['baseUrl'], req['next'], req['params']];
  }

  return function () { // 返回一个函数,调用即可恢复
    // restore vals
    for (var i = 0; i < props.length; i++) {
      obj[props[i]] = vals[i]; // 恢复之前保存的引用
    }

    return fn.apply(this, arguments); // 恢复之后再调用一下 fn (out),并且参数照原样传递。
  };
}

  // setup next layer
  req.next = next; // 保存一下 next 方法, next 方法在下面

  // for options requests, respond with a default if nothing else responds
  if (req.method === 'OPTIONS') {
    done = wrap(done, function(old, err) {
      if (err || options.length === 0) return old(err); // 假如有错,抛出err. old -> done -> out (next)
      sendOptionsResponse(res, options, old); // 返回一个 options 的响应,告诉前端,我支持 post 等等方法
    });

    function wrap(old, fn) { //  把 old 作为 fn 的第一个参数,调用 fn
    return function proxy() { // 返回一个函数
      var args = new Array(arguments.length + 1); // 新建一个数组,长度等于参数长度 + 1

      args[0] = old; // 把 index 为 0 的位置放置 old

      for (var i = 0, len = arguments.length; i < len; i++) {
        args[i + 1] = arguments[i]; // 从 1 开始报错参数传递过来的值
      }

      fn.apply(this, args); // 调用 fn 并且把所有的参数传给 fn
    };
  }

  }

  // setup basic req values
  req.baseUrl = parentUrl; // 保存一些引用到 req 上面去。
  req.originalUrl = req.originalUrl || req.url;

  next(); // 执行 next 方法

  function next(){.....}
  }
};
2.6.1.5 next 方法

我们可以忽略掉 process_params,(嵌套太深,不易阅读,了解其功能就行,处理一些参数的) 关于 setImmediate 更多请阅读这里

  function next(err) {
    var layerError = err === 'route'
      ? null
      : err; // 保存一下错误

    // 移除斜线 /
    if (slashAdded) {
      req.url = req.url.substr(1);
      slashAdded = false;
    }

    // 保存一些改变了的参数到 req
    if (removed.length !== 0) {
      req.baseUrl = parentUrl;
      req.url = protohost + removed + req.url.substr(protohost.length);
      removed = '';
    }

    // 假如出错就在下一次事件循环之前调用 done(next) 方法
    if (layerError === 'router') {
      setImmediate(done, null)
      return
    }

    // 没有匹配到响应的
    if (idx >= stack.length) {
      setImmediate(done, layerError); // 下一次事件循环之前调用
      return;
    }

    // 从 req 上面拿到 path
    var path = getPathname(req);

    if (path == null) {
      return done(layerError); // 拿不到,抛出错误
    }

    // 寻找路由匹配
    var layer;
    var match;
    var route;

    while (match !== true && idx < stack.length) {
      layer = stack[idx++]; // stack 是 this.stack
      match = matchLayer(layer, path); // 路径与该 layer 是否匹配
      route = layer.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; // 没有 route 对象,同样继续下一次循环
      }

      if (layerError) {
        // routes do not match with a pending error
        // 出错了,继续下一个循序
        match = false;
        continue;
      }

      var method = req.method; // 从 req 上拿到该请求所使用的方法
      var has_method = route._handles_method(method); // 判断 route 上面是否有该方法

      // build up automatic options response
      if (!has_method && method === 'OPTIONS') { // 假如不存在,且是 options 方法,则添加一个 options 方法
        appendMethods(options, route._options());
      }

      // don't even bother matching route
      if (!has_method && method !== 'HEAD') { // 假如没有匹配且方法不等于 head 继续下一次循环
        match = false;
        continue;
      }
    }

    // no match
    if (match !== true) {
      return done(layerError); // 没有匹配到
    }

    // store route for dispatch on change
    if (route) {
      req.route = route; // 假如拿到了 layer 上上面的 route,则保持到 req 对象上面去。
    }

    // Capture one-time layer values
    // 判断是否需要对 params 进行参数合并 self.mergeParams 是 router 构造器的一个选项参数 opts.mergeParams 传递进来的
    req.params = self.mergeParams
      ? mergeParams(layer.params, parentParams)
      : layer.params;
    var layerPath = layer.path;

    // 调用 process_params 函数,处理当前 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); // 假如有 route 直接调用该 layer 处理该路由的逻辑
      }

      trim_prefix(layer, layerError, layerPath, path); // 假如没有 route,说明还要通过 trim_prefix 做一些处理,处理完之后,再调用 layer.handle_request
    });

  }
2.6.1.6 为 router 代理 route 的请求(all、get、post、options等等)方法
methods.concat('all').forEach(function(method){
  proto[method] = function(path){
    var route = this.route(path)
    route[method].apply(route, slice.call(arguments, 1));
    return this;
  };
});
2.6.1.7 use 方法

我们再来看一下 router 暴露出来的公共方法 use,它返回 this,也就是 router 对象,而且它也是一个中间件。

proto.use = function use(fn) {
  var offset = 0;
  var path = '/';  // 默认值

  // default path to '/'
  // disambiguate router.use([fn])
  if (typeof fn !== 'function') { // 假如 fn 不是一个函数,说明 fn 传递的是 path 参数
    var arg = fn;

    while (Array.isArray(arg) && arg.length !== 0) { // 是一个非空数组
      arg = arg[0]; // arg 保存数组第一项
    }

    // first arg is the path
    if (typeof arg !== 'function') { // 假如 arg 还不是函数, 说明是路径
      offset = 1; // use(['get','post'], function cal(){});
      path = fn; // 取到 path = ['get','post']
    }
  }

  var callbacks = flatten(slice.call(arguments, offset)); // [cal()]

  if (callbacks.length === 0) {
    throw new TypeError('Router.use() requires middleware functions'); // 没有回调
  }

  for (var i = 0; i < callbacks.length; i++) { // 循序处理 回调
    var fn = callbacks[i];

    if (typeof fn !== 'function') {
      throw new TypeError('Router.use() requires middleware function but got a ' + gettype(fn));
    }

    // add the middleware
    debug('use %o %s', path, fn.name || '<anonymous>')

    var layer = new Layer(path, { // 为该 path 新建一个 layer 层,并且传入 fn
      sensitive: this.caseSensitive,
      strict: false,
      end: false
    }, fn);

    layer.route = undefined; // 设置一个 route 默认值

    this.stack.push(layer); // 放到 this.stack 里面去
  }

  return this;
};
2.6.1.8 route 方法

同样也是新建 layer 的一种方式,不过它返回的是 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;
};
2.6.1.9 从 API 来看一看两种路由区别。

app.route 代理的 router.route ,以这样的形式返回 route,这样可以以链式调用的方式指定相同 path 处理不同的方法,这样我们就非常容易实现 RESTful API 的路由模式。

app.route('/book')
  .get(function(req, res) {
    res.send('Get a random book');
  })
  .post(function(req, res) {
    res.send('Add a book');
  })
  .put(function(req, res) {
    res.send('Update the book');
  });

express.Router() 的返回值是 this,也是一个中间件,所以,我们可以通过 app.use 指定路由中间件,并且指定在何种 path 下才执行这些中间件。这样我们就可以把路由文件给分开。

var express = require('express');
var router = express.Router();

// 该路由使用的中间件
router.use(function timeLog(req, res, next) {
  console.log('Time: ', Date.now());
  next();
});
// 定义网站主页的路由
router.get('/', function(req, res) {
  res.send('Birds home page');
});
// 定义 about 页面的路由
router.get('/about', function(req, res) {
  res.send('About birds');
});

module.exports = router;
var birds = require('./birds');
...
app.use('/birds', birds);

2.6.2 router/layer.js

解析 path之后,调用 fn

2.6.2.2 path-to-regexp 库

这个文件依赖了一个外置库 path-to-regexp,它的简单使用如下所示。通过这个模块可以非常方便的实现路由参数匹配的功能。

var re = pathToRegexp('/:foo/:bar', keys)
// keys = [{ name: 'foo', prefix: '/', ... }, { name: 'bar', prefix: '/', ... }]

re.exec('/test/route')
//=> ['/test/route', 'test', 'route']
2.6.2.2 构造器
function Layer(path, options, fn) {
  if (!(this instanceof Layer)) {
    return new Layer(path, options, fn);
  }

  debug('new %o', 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);

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

把传入的路由处理函数 fn 保存到 handle 上面,顺便保存一下 fn 的名字。声明一个 params 和一个 path,通过 pathRegexp 解析 path, 存储 keys 到 this 上面去。

2.6.2.3 handle_request

handle_error 就是处理错误的方法,这个忽略掉,先看 handle_request,非常简单调用 fn 即可

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);
  }
};
2.6.2.4 match

解析 path,将得到的 params 保持到 this.params 上面去。

Layer.prototype.match = function match(path) {
  var match

  match = this.regexp.exec(path) // 匹配路径

  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]) // 对参数进行解码 (decodeURIComponent)

    if (val !== undefined || !(hasOwnProperty.call(params, prop))) {
      params[prop] = val; // this.params 上保存 match 匹配到的变量。
    }
  }

  return true;
};

2.6.3 router/route.js

2.6.3.1 导出了什么

导出了 Route,Route 上面有3个实例属性,path 路径,stack 数组(用来保存 layer),methods 方法对象。

module.exports = Route;

function Route(path) {
  this.path = path;
  this.stack = [];

  debug('new %o', path)

  // route handlers for various http methods
  this.methods = {};
}
2.6.3.2 dispatch 方法
  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 为 head 请求,但系统不支持 head,则转换为 get 请求
    method = 'get';
  }

  req.route = this;

  next();

在 req.route 上面挂载了 this, 紧接着调用 next 方法。next 方法里面除去一些判断的逻辑,其主要逻辑是从 stack 拿出 layer 然后调用了 layer.handle_request 方法。

var layer = stack[idx++];
if (err) {
 layer.handle_error(err, req, res, next);
} else {
 layer.handle_request(req, res, next);
}
2.6.3.3 all 方法

把一些无关紧要的代码删除掉之后,看起来也非常简单,handles 保存了所有回调,把这些回调包装成 layer 推进 stack。

Route.prototype.all = function all() {
  var handles = flatten(slice.call(arguments));
  for (var i = 0; i < handles.length; i++) {
    var handle = handles[i];
    var layer = Layer('/', {}, handle);
    this.stack.push(layer);
  }
  return this;
};
2.6.3.4 其他方法(get、post、delete 等等)

功能与上面的 all 方法一样,只不过是给 Route 的实例上添加的 get、 post、delete 等等方法而已。

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

      var layer = Layer('/', {}, handle);
      layer.method = method;
      this.methods[method] = true;
      this.stack.push(layer);
    }
    return this;
  };
});

2.6.4 总结

我们来总结一下路由的逻辑。

2.6.4.1 功能汇总
  • layer.js 用于解析 path 对应的 params 参数,与保存处理请求的回调 fn。

  • route.js 用于绑定 Http 方法与对应的 path 路径,route 里面有一个 stack 数组,保存了许多 layer 实例。通过 dispatch 可以调用 layer 的处理请求 handle_request 方法。

2.6.4.2 路由添加的区别

Router 添加路由有2种方式

  • use
  • route

当使用 use 的时候,会创建一个 layer,解析 path 参数与 保存 fn,并放到 router.stack 数组里面,返回一个中间件其实就是 router 本身。

当使用 route 的时候,会新建一个 route,与 layer 。这个 route 不是一个中间件。但是可以通过它的 get/post/options 等等方法,给 route.stack (这里的 stack 是 route 里面的)添加 layer。

也就是说 router.stack 是保存 layer 的,route.stack 也是保存 layer 的,那么有什么区别呢?

我们拿以下这个例子来说, app.route('/book') 会放到 router 里面去,后面的 .get / .post 会放到 route 里面去。

app.route('/book')
  .get(function(req, res) {
    res.send('Get a random book');
  })
  .post(function(req, res) {
    res.send('Add a book');
  })
  .put(function(req, res) {
    res.send('Update the book');
  });

而在代码上的区别就是,router.js 里面的 layer 有一个 route 属性。

// router.js - route 方法
var layer = new Layer(path, {
sensitive: this.caseSensitive,
strict: this.strict,
end: true
}, route.dispatch.bind(route));

layer.route = route;

route.js 里面的 layer 则有一个 method 属性。

// route.js
var layer = Layer('/', {}, handle);
layer.method = method;

this.methods[method] = true;
this.stack.push(layer);
2.6.4.4 Router 以中间件的形式是如何被用起来的呢?

可以回顾一下 2.6.1.4 小节的内容。

其实是在构造器里面调用了 router.handle,而又在 handle 方法中通过 path 去匹配 router 的 stack 里面所有的 layer(一个请求是可以匹配多个layer),命中之后再调用 process_params 处理参数,把参数添加到 req.params 上面去。只有当处理完参数之后,才在 process_params 回调里面调用 layer 的 handle_request 方法继而执行用户自定义的逻辑。

     ┌────────────────┐               ┌──────────────────────────────┐
     │    Request     │               │  Layer                       │
     └────────────────┘               │                              │
              │                       │                              │
              │                       │                              │
              ▼                       │     ┌───────────────────┐    │
    ┌──────────────────┐              │     │fn(req, res, next) │    │
    │  Router handle   │              │     └───────────────────┘    │
    └──────────────────┘              │               ▲              │
              │                       │               │              │
              │                       │               │              │
              ▼                       │               │              │
    ┌──────────────────┐   callback   │     ┌──────────────────┐     │
    │  process_params  │──────────────┼────▶│  handle_request  │     │
    └──────────────────┘              │     └──────────────────┘     │
                                      └──────────────────────────────┘

图由 monodraw 软件所画。