koa源码部分解析(2)koa-router

何超英
2023-12-01

版本7.4.0

比较长,建议一边看一边打断点,光看一遍估计是不知道在说什么的,因为它的思路不是面向过程的那种一条线,而是互相穿插,绕来绕去的。

koa-router的核心其实是path-to-regexp这个库,不过koa-router做了非常多的参数处理和封装,主要提供的功能有:基础的rest方法的注册、嵌套路由、路由中间层、参数处理等,我们一个一个来看。

先来看看koa-router的基本使用

var Koa = require('koa');
var Router = require('koa-router');
var app = new Koa();
var router = new Router();

router.get('/a', (ctx, next) => {
  console.log(ctx.router)
  next();
  ctx.body = 'aaa';
}, (ctx, next) => {
  console.log('a2')
});

// 命名路由
router.get('b', '/b', (ctx, next) => {
  console.log(ctx.router)
  ctx.body = 'bbb';
});

app
  .use(router.routes());

app.listen(3000);

这里比较关键的步骤如下:

1.实例化了Router

2.调用实例的get方法,定义了两个路由a和b

3.调用实例的routes方法,将返回注册为应用的中间件

我们要说的,除了上述几个点以外,还有register、use、param函数,嵌套的路由如何实现,路由的前缀形式等

构造和verb的注册

我们按顺序来看,首先是构造函数,构造函数没干啥,初始化了opts、methods、params、stack几个变量,紧跟着通过methods.forEach在prototype上定义了get、post等方法,methods的列表可以自己看一下,在这里https://github.com/jshttp/methods/blob/master/index.js。这些方法进行了参数处理后直接调用了register函数

function Router(opts) {
  if (!(this instanceof Router)) {
    return new Router(opts);
  }

  this.opts = opts || {};
  // this.methods只在allowedMethods里面用到了,如果不提供methods参数,下面这些就是默认实现的方法
  this.methods = this.opts.methods || [
    'HEAD',
    'OPTIONS',
    'GET',
    'PUT',
    'PATCH',
    'POST',
    'DELETE'
  ];
	// params保存参数处理相关内容,stack保存Layer实例
  this.params = {};
  this.stack = [];
};
// 这个methods和上面的this.methods要区分一下
methods.forEach(function (method) {
  Router.prototype[method] = function (name, path, middleware) {
    var middleware;

    if (typeof path === 'string' || path instanceof RegExp) {
    	// 注意这两个slice将middleware变成了数组,注册的时候可以传多个方法
    	// 可以看看上面第一个例子里的/a路径注册的处理函数
      middleware = Array.prototype.slice.call(arguments, 2);
    } else {
      middleware = Array.prototype.slice.call(arguments, 1);
      path = name;
      name = null;
    }
		// 调用了register方法
    this.register(path, [method], middleware, {
      name: name
    });
		// 这一句保证了链式调用
    return this;
  };
});

register

在看register方法的代码前我们来看一看register方法的简单使用,注意register支持传入数组形式的path,而第二个参数methods只能是数组形式,

router.register(['/c', '/cc'], ['GET', 'POST'], (ctx, next) => {
  console.log(ctx.router)
  ctx.body = 'register';
});

register方法代码如下,比较重要的几个地方,一是通过path.forEach将传入的数组形式的路径,分别单独调用register,二是通过Layer构造了一个route对象,并且push到stack里,三是对param的处理

Router.prototype.register = function (path, methods, middleware, opts) {
  opts = opts || {};

  var router = this;
  var stack = this.stack;

  // support array of paths
  if (Array.isArray(path)) {
  	// 如果path是数组,每个路径单独调用register方法
    path.forEach(function (p) {
      router.register.call(router, p, methods, middleware, opts);
    });

    return this;
  }

	// 调用Layer的构造函数
  // create route
  var route = new Layer(path, methods, middleware, {
    end: opts.end === false ? opts.end : true,
    name: opts.name,
    sensitive: opts.sensitive || this.opts.sensitive || false,
    strict: opts.strict || this.opts.strict || false,
    prefix: opts.prefix || this.opts.prefix || "",
    ignoreCaptures: opts.ignoreCaptures
  });

  if (this.opts.prefix) {
  	// 处理实例化的时候传入的prefix选项,参见下文的嵌套路由部分,setPrefix是Layer的函数
    route.setPrefix(this.opts.prefix);
  }
	
  // add parameter middleware
  // 对param的处理
  Object.keys(this.params).forEach(function (param) {
    route.param(param, this.params[param]);
  }, this);
	// 所有注册的路由全都存在stack里面,每个path作为一个元素,一个path可能有多个methods和多个处理函数
  stack.push(route);

  return route;
};

因为register里面有用到,所以我们来看一看Layer的构造函数,注意这里的this.methods和this.stack和前面不一样了,是Layer自己定义的

function Layer(path, methods, middleware, opts) {
  this.opts = opts || {};
  this.name = this.opts.name || null;
  this.methods = [];
  this.paramNames = [];
  this.stack = Array.isArray(middleware) ? middleware : [middleware];

	// register的methods参数必须是数组是因为这里直接拿来就forEach
  methods.forEach(function(method) {
    var m = this.methods.push(method.toUpperCase());
    // 如果注册了一个get请求,那么添加一个对应的head请求的处理
    if (this.methods[m-1] === 'GET') {
      this.methods.unshift('HEAD');
    }
  }, this);

  // ensure middleware is a function
  this.stack.forEach(function(fn) {
    var type = (typeof fn);
    if (type !== 'function') {
      throw new Error(
        methods.toString() + " `" + (this.opts.name || path) +"`: `middleware` "
        + "must be a function, not `" + type + "`"
      );
    }
  }, this);

  this.path = path;
  // 生成路径匹配用的正则表达式
  // paramNames是pathToRegExp来赋值的,如果路径是/foo/:bar这样的,paramNames里面就会有值
  // 如果是/foo/bar,paramNames就是空数组
  this.regexp = pathToRegExp(path, this.paramNames, this.opts);

  debug('defined route %s %s', this.methods, this.opts.prefix + this.path);
};

实际上Layer返回的route是这样么一个对象(下面是以本文第一个例子里的b的那个路由为例得到的值),其中opts是一些pathToRegExp接受的选项,regex是pathToRegExp的返回值,stack是我们注册路由的处理函数,建议自己打断点看一下

{
  methods: ['HEAD', 'GET'],
  name: null,
  opts: Object
  paramNames: [],
  path: '/b',
  regex: Object,
  stack: [middleware]
}

到这里为止关于register的函数我们看完了,可能贴的代码有点多不方便看,但是最核心的部分其实只有一个pathToRegExp,另外都是一些参数的处理。

routes

回看一眼本文开头的例子,所有路由注册完毕以后,我们会调用routes方法,除去allowedMethods,实际上看完routes方法我们就算完成了一个简单的流程。

在看routes方法之前,我们可以确定的是,routes方法执行后返回的一定是koa中间件的格式,也就是一个函数,并且有ctx和next两个参数。

routes方法的代码如下,核心就是dispatch函数,我们的所有路由都会经过这个函数,里面调用了Router.prototype.match,用我们stack里面的全部middleware的regexp去匹配请求路径,匹配成功的middleware的处理函数先转成数组,再通过compose转换成单个函数执行。这里还是比较绕的,建议自己打断点看一下每一步的结果。

// middleware和routes是一样的
Router.prototype.routes = Router.prototype.middleware = function () {
  var router = this;

	// 最终的处理路由的地方就是这个函数
  var dispatch = function dispatch(ctx, next) {
    debug('%s %s', ctx.method, ctx.path);
    
		// ctx.path是koa赋值的,ctx.routerPath不知道是哪里来的
    var path = router.opts.routerPath || ctx.routerPath || ctx.path;
    // 调用了match方法,根据this.stack里面注册的路由生成的正则,一个一个匹配,
    // 返回是否匹配成功和匹配到的Layer实例
    var matched = router.match(path, ctx.method);
    var layerChain, layer, i;

    if (ctx.matched) {
      ctx.matched.push.apply(ctx.matched, matched.path);
    } else {
      ctx.matched = matched.path;
    }

		// 在处理函数里可以获取ctx.router
    ctx.router = router;
    
		// route标志不为true就执行下一个中间件,没有匹配成功或者是use方法注册的路由都会导致route标志为false
    if (!matched.route) return next();
    
    // pathAndMethod是我们注册的middleware,
		// 往ctx里放两个值_matchedRoute和_matchedRouteName
    var matchedLayers = matched.pathAndMethod
    var mostSpecificLayer = matchedLayers[matchedLayers.length - 1]
    ctx._matchedRoute = mostSpecificLayer.path;
    if (mostSpecificLayer.name) {
      ctx._matchedRouteName = mostSpecificLayer.name;
    }
    
		// 匹配成功的layer,拼成一个数组
    layerChain = matchedLayers.reduce(function(memo, layer) {
      memo.push(function(ctx, next) {
        ctx.captures = layer.captures(path, ctx.captures);
        // 对param的处理
        ctx.params = layer.params(path, ctx.captures, ctx.params);
        ctx.routerName = layer.name;
        return next();
      });
      return memo.concat(layer.stack);
    }, []);
    
		// 通过compose将数组形式的layers变成一个嵌套的函数并执行,这里return的是compose后执行的结果
		// 是一个Promise.resolve或者Promise.reject
    return compose(layerChain)(ctx, next);
  };

  dispatch.router = this;

  return dispatch;
};

这里还是要贴一下match的代码,因为下面讲 match的时候发现有挺多细节和match相关

Router.prototype.match = function (path, method) {
  var layers = this.stack;
  var layer;
  var matched = {
    path: [],
    pathAndMethod: [],
    route: false
  };

  for (var len = layers.length, i = 0; i < len; i++) {
    layer = layers[i];

    debug('test %s %s', layer.path, layer.regexp);

    if (layer.match(path)) {
      matched.path.push(layer);

      if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
        matched.pathAndMethod.push(layer);
        // 只有当注册的methods不为空,才会将route标志位设为true,这个标志位在上面的routes方法里有用到,
        // 如果只使用use方法来注册某个路由,这个判断就不会通过,导致注册的中间件不会执行
        // 但是如果使用verb注册过某个路由,use也注册了这个路由或者use注册了一个不带路由的中间件,use注册的中间件就会执行
        if (layer.methods.length) matched.route = true;
      }
    }
  }

  return matched;
};

这样基本的流程我们就说完了,基本流程用一个图表示就是:verb注册 => register => 调用routes()

接下来我们说些进阶的使用和实现

嵌套路由

嵌套路由应该还是比较常用的,我们先来看一个例子

var test = new Router({
	// 注意这里
  prefix: '/test'
});
var forums = new Router();

test.get('/test', (ctx, next) => {
  console.log('test');
  ctx.body = 'test';
});

// 注意这里
test.prefix('/prefix');

// 注意这里
forums.use('/forums/:fid?', test.routes(), test.allowedMethods());

app.use(forums.routes());

app.listen(3001);

这个例子里,要注意三个地方,第一个是test在实例化的时候,传入了prefix前缀,第二个是test调用了prefix方法,为自己注册的router添加了prefix这个前缀,第三个是forums,它调用了use方法,传入了test定义的router,对/forums/123/prefix/test/test/forums/prefix/test/test这样的路径会有响应。

实例化时传入的参数,是在register函数里处理的,调用了Layer的setPrefix方法,可以回看上文的register那一节;prefix方法也是调用了Layer的setPrefix方法,其实还是pathToRegExp帮忙处理的,代码如下

Layer.prototype.setPrefix = function (prefix) {
  if (this.path) {
    this.path = prefix + this.path;
    this.paramNames = [];
    this.regexp = pathToRegExp(this.path, this.paramNames, this.opts);
  }

  return this;
};

然后我们来看看use方法

use

除了在嵌套路由的时候用到,use还可以为路由添加类似中间层的处理,是比较重要的一个函数,但是也有一些坑,use的用法我所知道的有这么几种,

// 直接传处理函数,只有当前这个router实例中的某个路径匹配成功,才会执行处理函数
router.use((ctx, next)=>{
  ...
})
// 传路径加处理函数,路径可以是字符串或者是数组
router.use(['/users', '/id'], (ctx, next)=>{
  ...
})
// 传一个其他router实例的中间件,注意这个例子最终的效果不是分别添加了/users和/id前缀,而是添加了/id/users前缀,还将中间件注册了两次
// 原因是use操作的是routes函数返回的结果,也就是dispatch函数,而dispatch和router实例是绑定的
router.use(['/users', '/id'], otherRouter.routes())

// 上面提到的问题也会导致下面这个例子和想象的结果不太一样,会发现无法响应other路径,查看middleware会发现/prefix/other路径的中间件注册了两次
otherRouter.get('/other', (ctx, next)=>{
  ...
})
app.use(otherRouter.routes());

router.use('/prefix', otherRouter.routes());
app.use(router.routes());

use的代码如下,上面例子里关于dispatch和router绑定的问题有点难讲清楚,大概和复杂类型的变量,赋值给了另一个变量后进行操作,发现原来的变量也变了差不多的意思

Router.prototype.use = function () {
  var router = this;
  var middleware = Array.prototype.slice.call(arguments);
  var path;

	// 处理第一个参数是路径数组的情况,和register一样如果是数组的话每个元素单独调用一回
  // support array of paths
  if (Array.isArray(middleware[0]) && typeof middleware[0][0] === 'string') {
    middleware[0].forEach(function (p) {
      router.use.apply(router, [p].concat(middleware.slice(1)));
    });
    return this;
  }
	// 处理第一个参数是路径的情况
  var hasPath = typeof middleware[0] === 'string';
  if (hasPath) {
    path = middleware.shift();
  }

  middleware.forEach(function (m) {
  	// 如果有router,其实就是已经调用过routes方法,如果没有,就调用register方法
    if (m.router) {
    	// 处理前缀
      m.router.stack.forEach(function (nestedLayer) {
        if (path) nestedLayer.setPrefix(path);
        if (router.opts.prefix) nestedLayer.setPrefix(router.opts.prefix);
        router.stack.push(nestedLayer);
      });
			// 对param的处理
      if (router.params) {
        Object.keys(router.params).forEach(function (key) {
          m.router.param(key, router.params[key]);
        });
      }
    } else {
    	// register的定义function (path, methods, middleware, opts),方便对比,methods传的是空数组
      router.register(path || '(.*)', [], m, { end: false, ignoreCaptures: !hasPath });
    }
  });

  return this;
};

参数处理

在前面的代码中我们已经看到过很多次param和params这样的变量或者函数,这几个变量和函数是为了处理路径里的变量,比如下面例子里的user,这种方式提供了可以抽离的参数校验功能

router
.param('user', (id, ctx, next) => {
  ctx.user = users[id];
  if (!ctx.user) return ctx.status = 404;
  return next();
})
.get('/users/:user', ctx => {
  ctx.body = ctx.user;
})
.get('/users/:user/friends', ctx => {
  return ctx.user.getFriends().then(function(friends) {
    ctx.body = friends;
  });
})

param函数往params变量中添加处理函数,register和use函数中都循环调用了Layer的param函数,routes函数中调用了Layer实例的params方法,这一块说实话还没怎么看明白,也比较细节,看到这里基本上使用过程中发现什么问题可以找得到是哪里,所以就不深入了(),比较重要的应该只有Layer.prototype.param。

allowedMethods

这篇文章没想到会写这么长,也没想到koa-router有那么多细节,感觉80%的代码都贴上来了,这里还想讲一个函数,最后一个,allowedMethods,先看例子

router
.get('a', '/a', (ctx, next) => {
  console.log('a1');
  next();
}, (ctx, next) => {
  console.log('a2');
  next();
});

app.use(router.routes());
app.use(router.allowedMethods());

要注意的是,如果中间件的next链断掉了,上面例子中的某一个next没有写,都会造成allowedMethods不会执行,因为allowedMethods的实现里有一句next().then(),另外的部分都还好理解,就不贴了

终于写完了,下一篇应该会写body吧,敬请期待

 类似资料: