koa-router源码学习

本文详细探讨了koa-router的源码,包括基本用法、核心对象Router和Layer,以及关键方法如methods.forEach、register、routes和match。通过分析,揭示了Router如何简化HTTP请求处理,以及在内部如何进行路由匹配和中间件调用。

koa-router源码学习

基本用法

const koa = require('koa');
const Router = require('koa-router');

const app = new koa();
const router = new Router();
app.use(router.routes()).use(router.allowedMethods());
app.listen(8080);


router.get( '/user', (ctx, next) => {
  ctx.body = 'hello word';
  next();
});

上面是koa-router的基本用法,创建了一个简单的路由。
如果不使用koa-router的话,需要自己来判断路径,如下所示,可以来判断url,然后调用响应的方法

const koa = require('koa');
const app = new koa();
app.listen(8080);

app.use(((context, next) => {
  const path = context.path;
  switch (path) {
    case '/':  // do some things;break;
    case '/demo':  // do some things;break;
  }
}));

当然这种方式代码复杂,不易维护,可以使用koa-router来简化代码。

源码学习

在koa-router中,有两个很重要的对象,几个重要的方法

对象:
  • Router对象:对外抛出的路由对象,封装了路由的方法和属性,提供给开发者使用
  • Layer对象:Layer对象未对外抛出,是koa-router内部的一个单位路由层的对象

这里我对Layer进行了自定义命名:单位路由层,指的是由method,path,middleware等组成的一个路由层,而整个路由是由多个路由层组成的。例如

router.get( '/user', (ctx, next) => {
  ctx.body = 'hello word';
  next();
});
// 它就是一个路由层,是一种匹配模式,当path,method匹配的时候,调用指定的middleware
方法:

ps:router指的是Router的实例

  • methods.forEach:将http的请求方法挂载到Router.prototype上,让router能使用对应的http方法来生成单位路由层
  • register:根据path, methods, middleware等生成layer实例,存储在router的stack属性中
  • routes:Router的主方法,返回一个koa中间件函数,在此中间件内调用其他函数,完成路由匹配,并完成路由链的调用,也可以使用middleware方法完成该操作
  • match:Router的核心方法,用于判断当前请求和路由是否匹配
  • compose:koa-compsoe模块的方法,在koa源码学习中遇到过,是koa中间件的核心函数

上面介绍了koa-router的主要对象和方法,下面我们通过创建一个简单的路由和一次HTTP请求,来了解Router内部的函数调用及实现细节

引入

const Router=require('koa-router');

在加载koa-router模块的时候,导出了Router对象,并执行了methods.forEach函数
该函数的主要作用是,从将从http模块取得的方法挂载在Router.prototype上
调用methods.forEach

methods.forEach(function (method) {
 // 这里是挂载请求方法,常用的就 get,post,put,delete,options等
  Router.prototype[method] = function (name, path, middleware) { 
    var middleware;
	
	// 这里主要是判断参数的个数,让path和middleware取得正确的值
    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;
    }
	// 调用Router实例方法register
    this.register(path, [method], middleware, {
      name: name
    });

    return this;
  };
});

调用Router.prototype.register方法
实例化layer实例,并将实例存储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,
    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;
};

调用Router.prototype.routes方法

示例代码
app.use(router.routes());
router.get( '/user', (ctx, next) => {
  ctx.body = 'hello word';
  next();
});

该方法返回一个中间件函数
该函数主要是调用match函数,取得url路径匹配结果

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

    var path = router.opts.routerPath || ctx.routerPath || ctx.path;
    var matched = router.match(path, ctx.method); // 在此调用match函数
    var layerChain, layer, i;
	
	// 多次调用的时候,将match.path合并为一个数组
    if (ctx.matched) {
      ctx.matched.push.apply(ctx.matched, matched.path);
    } else {
      ctx.matched = matched.path;
    }

    ctx.router = router;

    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;
    }
	
	// 将所有符合的layer组合起来
    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); // 通过compose组成中间件链,然后调用
  };

调用Router.prototype.match函数

Router.prototype.match = function (path, method) {
  var layers = this.stack;
  var layer;
  var matched = {
    path: [],
    pathAndMethod: [],
    route: false
  };
	// 将所有的单位路由曾循环,判断是否满足当前url
  for (var len = layers.length, i = 0; i < len; i++) {
    layer = layers[i];

    debug('test %s %s', layer.path, layer.regexp);
	// 当正则匹配成功后,则将当前layer加入match.path数组中
    if (layer.match(path)) {
      matched.path.push(layer);

      if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
        matched.pathAndMethod.push(layer);
        if (layer.methods.length) matched.route = true;
      }
    }
  }

  return matched;
};

这里值得注意的是layer.match方法,如上示例代码生成的正则为:

 /^\/hello(?:\/(?=$))?$/i
 // ?:为非捕获性分组
 // ?=为正则前瞻
 (?:\/(?=$))?$ 代表的意思是:以/结束,出现0次或1次
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值