koa-router源码分析

koa-router是什么

koa-router的github主页给出的定义是:

Router middleware for koa.

定义非常简洁,含义也一目了然,它就是koa的路由中间件。koa为了自身的轻量,不在内核方法中绑定任何中间件,而路由功能对于一个web框架来说,又是必不可少的基础功能。因此koa-router为koa提供了强大的路由功能。

提到路由中间件,这里补充两点,对于后面理解koa-router非常有帮助。

  • 什么是中间件
中间件(Middleware) 是一个函数,它可以访问请求对象(request object (req)), 响应对象(response object (res)), 和 web 应用中处于请求-响应循环流程中的中间件,一般被命名为 next 的变量。
中间件的功能包括:
执行任何代码。
修改请求和响应对象。
终结请求-响应循环。
调用堆栈中的下一个中间件。
如果当前中间件没有终结请求-响应循环,则必须调用 next() 方法将控制权交给下一个中间件,否则请求就会挂起。
  • router和route的区别

route就是一条路由,它将一个URL路径和一个函数进行映射,如/users => getAllUsers()
router可以理解为一个容器,管理所有route,当接收到一个http请求,根据请求url和请求方法去匹配对应的中间件,这个过程由router管理。

koa-router源码结构

koa-router源码分析

如上图,koa-router的源码结构非常简单,核心文件只有router.js和layer.js两个文件,代码量也非常少,包括注释在内总共不超过1000行。通过package.json文件中的main字段,找到入口文件是“lib/router.js”。router.js文件定义了一个构造函数Router,并在Router上定义了一个静态方法和一些原型方法,如下图所示。
koa-router源码分析

layer.js文件定义了一个构造函数Layer,并在Layer上定义了几个原型方法,如下图所示。
koa-router源码分析

基本用法

var Koa = require('koa');
var Router = require('koa-router');
 
var app = new Koa();
var router = new Router();
 
router.get('/', (ctx, next) => {
  // ctx.router available
});
 
app
  .use(router.routes())
  .listen(3000);

实例化一个koa-router,注册相应路由中间件,然后通过routes方法生成一个koa中间件并挂载到koa上,这样就给koa添加了最基本的路由功能。

router.js源码分析

...
var methods = require('methods');
...

module.exports = Router;

// Router构造函数
function Router(opts) {
  // 宽容性处理,如果没通过new调用,仍然返回new调用后的实例
  if (!(this instanceof Router)) {
    return new Router(opts);
  }

  this.opts = opts || {}; // 实例化时传入的参数
  // 这里的methods和上面require的methods有什么关系?
  this.methods = this.opts.methods || [
    'HEAD',
    'OPTIONS',
    'GET',
    'PUT',
    'PATCH',
    'POST',
    'DELETE'
  ];

  this.params = {};
  this.stack = []; // 存储Layer实例,那么Layer是什么?
};

// 定义get|post|put|...方法
methods.forEach(function (method) {
  ...
});

首先定义一个了构造函数Router,实例化时支持传入一个对象参数,构造函数里初始化了一些实例属性。

构造函数Router里面的this.methods和外部require的methods有什么关系?
this.methods由上面给出的7种http请求方法组成;
外部引入的methods等价于Node.js中的http.METHODS转化为小写后的形式,即:

methods = [
  'acl',
  'bind',
  'checkout',
  'connect',
  'copy',
  'delete',
  'get',
  'head',
  'link',
  'lock',
  'm-search',
  'merge',
  'mkactivity',
  'mkcalendar',
  'mkcol',
  'move',
  'notify',
  'options',
  'patch',
  'post',
  'propfind',
  'proppatch',
  'purge',
  'put',
  'rebind',
  'report',
  'search',
  'subscribe',
  'trace',
  'unbind',
  'unlink',
  'unlock',
  'unsubscribe' ]

this.methods是methods的一个子集,搞清楚它们的值是什么后,我们来看一下它们的含义,this.methods是koa-router默认支持的http请求方法,如果没有在构造函数定义其他方法,那么采用默认支持方法之外的http方法请求时,会抛出异常。(在allowedMethods方法里实现)

Router实例的get/post等方法在哪定义的呢?看下面这段代码

methods.forEach(function (method) {
  Router.prototype[method] = function (name, path, middleware) {
    var middleware; // 是一个数组,支持传入多个中间件

    // 支持传2个参数或3个参数,如果传2个参数,则name值为null
    if (typeof path === 'string' || path instanceof RegExp) {
      middleware = Array.prototype.slice.call(arguments, 2);
    } else {
      middleware = Array.prototype.slice.call(arguments, 1);
      path = name;
      name = null;
    }

    this.register(path, [method], middleware, {
      name: name
    });

    return this;
  };
});

下一步走到register函数,register函数的作用是根据路由、http请求方法、处理函数、自定义配置实例化一个Layer,Layer实例包含一些属性(如regexp)用来进行路由匹配。这里可能对Layer还是不太理解,首先Layer是一个构造函数,它定义了一些重要的属性,这些属性的作用是为了在routes方法里进行路由匹配和执行对应的中间件,Layer也就相当于我们在第一部分提到的route,它定义了一个URL和对应中间件的映射关系,有了这个对应关系,我们就可以对到来的请求进行路由匹配和执行对应的中间件。然后把Layer实例push到Router实例的stack中。

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.forEach(function (p) {
      router.register.call(router, p, methods, middleware, opts);
    });

    return this;
  }

  // create route
  var route = new Layer(path, methods, middleware, {
    end: opts.end === false ? opts.end : true, // When false the path will match at the beginning. (default: 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) {
    route.setPrefix(this.opts.prefix);
  }

  // add parameter middleware
  Object.keys(this.params).forEach(function (param) {
    route.param(param, this.params[param]);
  }, this);

  stack.push(route);

  return route;
};

接下来我们看一个非常重要的函数,routes函数返回了一个dispatch函数,名字是不是非常眼熟,koa-compose里也有一个dispatch函数。返回的dispatch函数作为一个koa中间件。它从koa中间件的第一个参数context对象中拿到url,然后进行路由匹配,匹配失败则执行一下koa中间件;匹配成功,则执行相应的中间件。

Router.prototype.routes = Router.prototype.middleware = function () {
  var router = this;

  var dispatch = function dispatch(ctx, next) {
    debug('%s %s', ctx.method, ctx.path);

    var path = router.opts.routerPath || ctx.routerPath || ctx.path;
    // 调用Router原型上的match方法,返回一个matched对象
    // match方法在下面再分析
    var matched = router.match(path, ctx.method);
    var layerChain, layer, i;

    // 当koa上挂载多个Router实例时,if可能会被执行到
    if (ctx.matched) {
      ctx.matched.push.apply(ctx.matched, matched.path);
    } else {
      ctx.matched = matched.path;
    }

    ctx.router = router;

    // 没有匹配到路由,执行下一个中间件
    // 如果匹配到路由,匹配的路由方法中没有调用next()方法时,下一个中间件就不会执行了
    if (!matched.route) return next();

    var matchedLayers = matched.pathAndMethod
    var mostSpecificLayer = matchedLayers[matchedLayers.length - 1]
    ctx._matchedRoute = mostSpecificLayer.path;
    if (mostSpecificLayer.name) {
      ctx._matchedRouteName = mostSpecificLayer.name;
    }

    // 对匹配到的路由中间件数组做一个处理
    // 在每一个路由中间件前面添加一个中间件,
    // 给ctx添加3个属性(路由path、路由参数对象、路由命名)
    // 这样后面的路由中间件就能方便地获取到ctx上的这3个属性
    layerChain = matchedLayers.reduce(function(memo, layer) {
      memo.push(function(ctx, next) {
        ctx.captures = layer.captures(path, ctx.captures);
        ctx.params = layer.params(path, ctx.captures, ctx.params);
        ctx.routerName = layer.name;
        return next();
      });
      return memo.concat(layer.stack);
    }, []);

    return compose(layerChain)(ctx, next);
  };

  dispatch.router = this;

  return dispatch;
};

下面我们看一下Router原型上的match方法的代码

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

  // 遍历所有路由(layer实例,即处理后的route)
  for (var len = layers.length, i = 0; i < len; i++) {
    layer = layers[i];

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

    // 通过正则匹配path
    if (layer.match(path)) {
      matched.path.push(layer);
        
      // 匹配http请求方法
      if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
        matched.pathAndMethod.push(layer); // 这里是真正要执行的路由方法
        if (layer.methods.length) matched.route = true;
      }
    }
  }

  return matched;
};

Router原型上的match方法主要是调用Layer原型上的的match方法,即通过正则匹配path,匹配成功之后再匹配http请求方法,当都匹配成功,将matched.route置为true,表示路由匹配成功。

至此,Router原型上的几个核心函数就讲解完了,其他一些函数就不展开分析了。

layer.js源码分析

layer.js定义了一个构造函数Layer,它对path|methods|middleware|opts进行一些处理,比如将路由path统一转换为正则表达式,将中间件统一转换为数组形式等,相当定义我们的route。Layer原型对象上定义了一些方法,如match用于路由路径匹配,params方法用于提取路由参数对象,captures用于提取路由path等。

// 构造函数
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];

  // GET方法前添加一个HEAD方法
  methods.forEach(function(method) {
    var l = this.methods.push(method.toUpperCase());
    if (this.methods[l-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;
  // 将路由、路由参数转化成正则表达式形式(match方法即通过这个正则表达式去匹配path)
  this.regexp = pathToRegExp(path, this.paramNames, this.opts);

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

本文对koa-router整体流程和核心函数进行了一些简单的分析,有表述不正确的地方,希望大家批评指出。

参考列表:
https://cnodejs.org/topic/578...
https://cnodejs.org/topic/579...
https://www.cnblogs.com/chris...

相关推荐