Skip to content
This repository has been archived by the owner on Oct 31, 2022. It is now read-only.

Latest commit

 

History

History
693 lines (554 loc) · 19.1 KB

File metadata and controls

693 lines (554 loc) · 19.1 KB

index.js

1. 模块依赖及变量定义

/**
 * Module dependencies.
 */

var Route = require('./route');
var Layer = require('./layer');
var methods = require('methods');
var mixin = require('utils-merge');
var debug = require('debug')('express:router');
var deprecate = require('depd')('express');
var parseUrl = require('parseurl');
var utils = require('../utils');

/**
 * Module variables.
 */

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

2. 导出对象

该模块为Router的代码,导出对象源码如下:

/**
 * Initialize a new `Router` with the given `options`.
 *
 * @param {Object} options
 * @return {Router} which is an callable function
 * @api public
 */

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

  function router(req, res, next) {
    router.handle(req, res, next);
  }

  // mixin Router class functions
  router.__proto__ = proto;

  router.params = {};
  router._params = [];
  router.caseSensitive = options.caseSensitive;
  router.mergeParams = options.mergeParams;
  router.strict = options.strict;
  router.stack = [];

  return router;
};

该代码的简化版本如下:

var Router = function() {
	function router() {
		router.doSomething();
	}
	router.__proto__ = Router;
	router.prop = 'some prop';
	return router;
};

Router.doSomething = function() {
	console.log('do something');
};

var router1 = Router();
var router2 = new Router();

在该例子中,Router事实上只是一个工厂函数,用来生成router,并且作为router的原型。在调用Router的时候,有没有new差别不大,因为最终都是返回了router

回归到源码,该段代码主要做了如下几件事:

  1. 生成一个router对象。事实上,router是一个函数,该函数有一些其它属性。
  2. router__proto__属性设置为proto,即让router继承自proto
  3. 设置router的一些属性值,主要包括:
    • params
    • _params
    • caseSensitive
    • mergeParam
    • strict
    • stack

application.js中的lazyrouter中,正是通过new Router({...})来生成了app._router

3. proto.use

该方法代码如下:

/**
 * Use the given middleware function, with optional path, defaulting to "/".
 *
 * Use (like `.all`) will run for any http METHOD, but it will not add
 * handlers for those methods so OPTIONS requests will not consider `.use`
 * functions even if they could respond.
 *
 * The other difference is that _route_ path is stripped and not visible
 * to the handler function. The main effect of this feature is that mounted
 * handlers can operate without any code changes regardless of the "prefix"
 * pathname.
 *
 * @api public
 */

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

  // default path to '/'
  // disambiguate router.use([fn])
  if (typeof fn !== 'function') {
    var arg = fn;

    while (Array.isArray(arg) && arg.length !== 0) {
      arg = arg[0];
    }

    // first arg is the path
    if (typeof arg !== 'function') {
      offset = 1;
      path = fn;
    }
  }

  var callbacks = utils.flatten(slice.call(arguments, offset));

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

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

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

    var layer = new Layer(path, {
      sensitive: this.caseSensitive,
      strict: false,
      end: false
    }, fn);

    layer.route = undefined;

    this.stack.push(layer);
  }, this);

  return this;
};

该方法的思路如下:

  • 首先定义局部变量offsetpath
    • offset:表示从第几个参数开始为中间件函数,默认为0
    • path:路径,默认为/
  • 当第一个参数不为函数的时候:
    • while循环中是针对第一个参数为数组的情况,即[fn][[fn]]等情况,此时让局部变量arg为数组或多层数组的第一项
    • 如果arg不为函数,则认为第一个参数为字符串,并将其赋值给path,同时设置offset1
  • 获取参数中的offset下标开始的所有参数,并进行扁平化操作,赋值给callbacks,即代表所有的中间件函数
  • 如果没有中间件函数,则抛出错误
  • callbacks进行forEach操作,即对于每个中间件函数:
    • 如果它不是一个函数,则报错
    • 如果是一个函数,则使用path和该函数构建一个layer对象,并加到this.stack
  • 返回该对象,从而可以进行链式操作

归结起来,该方法其实就是向this.stack数组中添加layer对象,layerrouter/layer.js的导出对象的实例。

看如下例子:

var express = require('express');
var app = express();

app.use(function foo() {});
app.use('/users', function bar() {}, function test() {});

console.log(app._router.stack);

输出结果为:

[{
	handle: [Function: query],
	name: 'query',
	params: undefined,
	path: undefined,
	keys: [],
	regexp: {
		/^\/?(?=\/|$)/i
		fast_slash: true
	},
	route: undefined
}, {
	handle: [Function: expressInit],
	name: 'expressInit',
	params: undefined,
	path: undefined,
	keys: [],
	regexp: {
		/^\/?(?=\/|$)/i
		fast_slash: true
	},
	route: undefined
}, {
	handle: [Function: foo],
	name: 'foo',
	params: undefined,
	path: undefined,
	keys: [],
	regexp: {
		/^\/?(?=\/|$)/i
		fast_slash: true
	},
	route: undefined
}, {
	handle: [Function: bar],
	name: 'bar',
	params: undefined,
	path: undefined,
	keys: [],
	regexp: /^\/users\/?(?=\/|$)/i,
	route: undefined
}, {
	handle: [Function: test],
	name: 'test',
	params: undefined,
	path: undefined,
	keys: [],
	regexp: /^\/users\/?(?=\/|$)/i,
	route: undefined
}]

其中数组中的前两项,是因为在application.jslazyrouter方法中,初始化app._router的时候,有如下代码:

this._router.use(query(this.get('query parser fn')));
this._router.use(middleware.init(this));

4. proto.param

该方法源码如下:

/**
 * Map the given param placeholder `name`(s) to the given callback.
 *
 * Parameter mapping is used to provide pre-conditions to routes
 * which use normalized placeholders. For example a _:user_id_ parameter
 * could automatically load a user's information from the database without
 * any additional code,
 *
 * The callback uses the same signature as middleware, the only difference
 * being that the value of the placeholder is passed, in this case the _id_
 * of the user. Once the `next()` function is invoked, just like middleware
 * it will continue on to execute the route, or subsequent parameter functions.
 *
 * Just like in middleware, you must either respond to the request or call next
 * to avoid stalling the request.
 *
 *  app.param('user_id', function(req, res, next, id){
 *    User.find(id, function(err, user){
 *      if (err) {
 *        return next(err);
 *      } else if (!user) {
 *        return next(new Error('failed to load user'));
 *      }
 *      req.user = user;
 *      next();
 *    });
 *  });
 *
 * @param {String} name
 * @param {Function} fn
 * @return {app} for chaining
 * @api public
 */

proto.param = function param(name, fn) {
  // param logic
  if (typeof name === 'function') {
    deprecate('router.param(fn): Refactor to use path params');
    this._params.push(name);
    return;
  }

  // apply param functions
  var params = this._params;
  var len = params.length;
  var ret;

  if (name[0] === ':') {
    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) {
    if (ret = params[i](name, fn)) {
      fn = ret;
    }
  }

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

  (this.params[name] = this.params[name] || []).push(fn);
  return this;
};

该方法的主要思路如下:

  • 如果第一个参数为函数,即调用方式为param(fn)。由于app.param(callback)已经在4.11.0版本中废弃,因此处理方式是将参数中的函数放在this._params中,然后返回
  • 如果name以冒号开头,则去掉冒号
  • 对于name,使用this._params中的每个函数对其进行依次处理。加入之前没有调用过param(fn)的话,相当于跳过此步
  • 如果fn参数不是函数,报错
  • this.params[name]数组中添加fn
  • 返回this从而进行链式调用

总的来说,该方法其实是对this.params进行设置,而this.params则是如下的数据结构:

{
	foo: [],
	bar: []
}

例如,我们执行如下代码:

var express = require('express');
var app = express();

app.param('user_id', function foo() {});
app.param('user_id', function bar() {});
app.param('user_name', function test() {});

其中app.paramproto.param的代理函数。输出结果为:

{
	user_id: [
		[Function: foo],
		[Function: bar]
	],
	user_name: [
		[Function: test]
	]
}

5. proto.route

该方法的作用是创建一条新的路由,源码如下:

/**
 * Create a new Route for the given path.
 *
 * Each route contains a separate middleware stack and VERB handlers.
 *
 * See the Route api documentation for details on adding handlers
 * and middleware to routes.
 *
 * @param {String} path
 * @return {Route}
 * @api public
 */

proto.route = function(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;
};

首先使用path创建一个route对象,然后使用pathroute.dispatch创建一个layer对象,将layer.route属性值设置为route,并把layer放在this.stack数组中。

6. VERB方法

源码如下:

// create Router#VERB functions
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;
  };
});

其实就是使用参数中的path进行this.route(path)调用,创建一个route,然后调用route的VERB方法。

7. proto.handle

该方法主要用来处理接收到的HTTP请求,会被app.handle调用。该方法的源码较长,下面是源码的主要结构:

proto.handle = function(req, res, done) {
  // ... ...

  next();

  function next(err) {
    // ... ...
  }

  function trim_prefix(layer, layerError, layerPath, path) {
    // ... ...
  }
}

因此,该方法主要是调用了next(),下面来看next函数。由于next的源码依然很长且比较繁琐,因此主要看比较关键的代码。在调用next之前,首先定义了idxstack这两个变量,分别表示当前中间件的下表和中间件列表。

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

这段代码表明当遍历完了所有的中间件时,执行done()。如果遍历未结束,执行while循环,代码如下:

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

  var method = req.method;
  var has_method = route._handles_method(method);

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

在这里,layer表示当前的中间件,match表示当前的中间件与当前路径是否匹配。其中matchLayer(layer, path)其实是调用了layer.match(path)

while循环的逻辑如下:

  • 如果match不为true,即该中间件与路径不匹配,则执行continue,对下一个中间件进行判断
  • 如果matchtrue但是route不存在,则说明是一个非路由中间件,执行continue,但此时由于match不再满足循环条件,因此会跳出循环
  • 如果matchtrueroute存在,则说明是一个路由中间件,则将继续对HTTP请求方法做一些处理,首先判断该路由是否能够处理该HTTP方法,即has_method
    • 如果has_methodfalse且HTTP方法为OPTIONS,则执行appendMethods(options, route._options())
    • 如果has_methodfalse且HTTP方法不为HEAD,则设置matchfalse,也就是说,该路由无法处理该请求,此时由于match依然满足循环条件,因此会对下一个中间件进行判断
    • 如果has_methodtrue,则由于match不再满足循环条件,因此会跳出循环

总的来说,该循环的主要作用就是从当前下标开始找出第一个能够处理该HTTP请求的中间件。如果是非路由中间件,则只要匹配路径即可;如果是路由中间件,则需要同时匹配路径和HTTP请求方法。

while之后,如果match不为true,则说明已经遍历完了所有的中间件,因此直接执行done();否则调用self.process_params,即进行参数的预处理,并调用回调函数。代码如下:

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

在回调函数中,如果route存在,即对于路由中间件,调用layer.handle_request(req, res, next),如果不是路由中间件,则会调用trim_prefix(layer, layerError, layerPath, path),对路径进行处理后,才调用layer.handle_request(req, res, next)

8. proto.process_params

该方法主要是对参数进行预处理,即在接收到HTTP请求后,在对请求处理之前,对请求路径中的参数进行一定的预处理。该方法会在proto.handle中被调用。该方法源码主要结构如下:

/**
 * Process any parameters for the layer.
 *
 * @api private
 */

proto.process_params = function(layer, called, req, res, done) {
  var params = this.params;

  // captured parameters from the layer, keys and values
  var keys = layer.keys;

  // fast track
  if (!keys || keys.length === 0) {
    return done();
  }

  var i = 0;
  var name;
  var paramIndex = 0;
  var key;
  var paramVal;
  var paramCallbacks;
  var paramCalled;

  // process params in order
  // param callbacks can be async
  function param(err) {
    // ... ...
  }

  // single param callbacks
  function paramCallback(err) {
    // ... ...
  }

  param();
};

其中,变量params形式示例如下:

{
  user_id: [
    [Function: foo],
    [Function: bar]
  ],
  user_name: [
    [Function: test]
  ]
}

变量keys形式示例如下:

[{
  name: 'user_id',
  // ... ...
}, {
  name: 'user_name',
  // ... ...
}]

也就是所,keys表示的是路径中应当接收的参数,params存放的是所有的参数预处理函数,而路径中实际接收到的参数保存在req.params中。

在该函数的开始,先对keys进行判断,如果它不存在或者为空数组,则直接执行done(),否则,会执行param()param函数的源码如下:

// process params in order
// param callbacks can be async
function param(err) {
  if (err) {
    return done(err);
  }

  if (i >= keys.length) {
    return done();
  }

  paramIndex = 0;
  key = keys[i++];

  if (!key) {
    return done();
  }

  name = key.name;
  paramVal = req.params[name];
  paramCallbacks = params[name];
  paramCalled = called[name];

  if (paramVal === undefined || !paramCallbacks) {
    return param();
  }

  // param previously called with same value or error occurred
  if (paramCalled && (paramCalled.error || paramCalled.match === paramVal)) {
    // restore value
    req.params[name] = paramCalled.value;

    // next param
    return param(paramCalled.error);
  }

  called[name] = paramCalled = {
    error: null,
    match: paramVal,
    value: paramVal
  };

  paramCallback();
}

其中,i表示的是当前处理的参数在keys中的下标,因此当i >= keys.length的时候,说明所有的参数已经被处理,因此执行done()paramIndex表示的是当前参数对应的预处理函数的在处理函数列表中的下标,每次执行param()的时候,都将paramIndex重置为0paramVal表示的是参数的实际值,例如路径为/user/:user_name,则当访问/user/alex的时候,paramVal即为alexparamCallbacks为预处理函数列表。paramCalled为结果缓存,由于函数的预处理是在每个Layer处理前执行,而不是在整个请求的处理前,因此可能两个或多个Layer会有同样的参数,此时只需要对参数进行一次预处理就可以了,并将结果缓存起来,这样,当再次需要对相同的参数进行预处理的话只需要直接读取结果就可以了。

如果paramCalled不为空,则说明已经处理了,因为在预处理的过程中可能会更改参数的值,因此需要执行req.params[name] = paramCalled.value更新参数值,然后对下一个参数进行处理。否则的话对缓存called进行设置,并执行paramCallback(),该函数用来依次执行当前参数所对应的预处理函数。paramCallback函数的源码如下:

// single param callbacks
function paramCallback(err) {
  var fn = paramCallbacks[paramIndex++];

  // store updated value
  paramCalled.value = req.params[key.name];

  if (err) {
    // store error
    paramCalled.error = err;
    param(err);
    return;
  }

  if (!fn) return param();

  try {
    fn(req, res, paramCallback, paramVal, key.name);
  } catch (e) {
    paramCallback(e);
  }
}

其中,fn表示的是当前的预处理函数。首先是更新paramCalled.value,因为在之前的预处理函数执行过程中可能会更改参数的值。然后如果有错误,则执行param(err);如果fn不存在,则执行param(),对下一个参数进行预处理。否则,执行fn(req, res, paramCallback, paramVal, key.name),即执行当前的预处理函数,并将paramCallback作为其第三个参数。例如如下的例子:

app.param('user_name', function foo(req, res, next){
  // ... ...
});

在执行的时候,fn表示的就是foo,而next事实上则是paramCallback