玩转Koa -- koa-router道理剖析

一、媒介

  Koa为了坚持本身的简约,并没有绑缚中间件。然则在现实的开辟中,我们须要和五花八门的中间件打交道,本文将要剖析的是常常用到的路由中间件 — koa-router。

  假如你对Koa的道理还不相识的话,能够先检察Koa道理剖析

二、koa-router概述

  koa-router的源码只要两个文件:router.js和layer.js,离别对应Router对象和Layer对象。

  Layer对象是对单个路由的治理,个中包括的信息有路由途径(path)、路由请求要领(method)和路由实行函数(middleware),而且供应路由的考证以及params参数剖析的要领。

  相比较Layer对象,Router对象则是对一切注册路由的一致处置惩罚,而且它的API是面向开辟者的。

  接下来从以下几个方面周全剖析koa-router的完成道理:

  • Layer对象的完成
  • 路由注册
  • 路由婚配
  • 路由实行流程

三、Layer

  Layer对象主如果对单个路由的治理,是全部koa-router中最小的处置惩罚单位,后续模块的处置惩罚都离不开Layer中的要领,这恰是起首引见Layer的主要缘由。

function Layer(path, methods, middleware, opts) {
  this.opts = opts || {};
  // 支撑路由别号
  this.name = this.opts.name || null;
  this.methods = [];
  this.paramNames = [];
  // 将路由实行函数保留在stack中,支撑输入多个处置惩罚函数
  this.stack = Array.isArray(middleware) ? middleware : [middleware];

  methods.forEach(function(method) {
    var l = this.methods.push(method.toUpperCase());
    // HEAD请求头部信息与GET一致,这里就一同处置惩罚了。
    if (this.methods[l-1] === 'GET') {
      this.methods.unshift('HEAD');
    }
  }, this);

  // 确保范例准确
  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;
  // 1、依据路由途径天生路由正则表达式
  // 2、将params参数信息保留在paramNames数组中
  this.regexp = pathToRegExp(path, this.paramNames, this.opts);
};

  Layer构造函数主要用来初始化路由途径、路由请求要领数组、路由处置惩罚函数数组、路由正则表达式以及params参数信息数组,个中主要采纳path-to-regexp要领依据途径字符串天生正则表达式,经由过程该正则表达式,能够完成路由的婚配以及params参数的捕捉:

// 考证路由
Layer.prototype.match = function (path) {
  return this.regexp.test(path);
}

// 捕捉params参数
Layer.prototype.captures = function (path) {
  // 后续会提到 关于路由级别中间件 无需捕捉params
  if (this.opts.ignoreCaptures) return [];
  return path.match(this.regexp).slice(1);
}

  依据paramNames中的参数信息以及captrues要领,能够猎取到当前路由params参数的键值对:

Layer.prototype.params = function (path, captures, existingParams) {
  var params = existingParams || {};
  for (var len = captures.length, i=0; i<len; i++) {
    if (this.paramNames[i]) {
      var c = captures[i];
      params[this.paramNames[i].name] = c ? safeDecodeURIComponent(c) : c;
    }
  }
  return params;
};

  须要注重上述代码中的safeDecodeURIComponent要领,为了防止服务器收到不可预知的请求,关于任何用户输入的作为URI部份的内容都须要采纳encodeURIComponent举行转义,不然当用户输入的内容中含有’&’、’=’、’?’等字符时,会涌现预感以外的状况。而当我们猎取URL上的参数时,则须要经由过程decodeURIComponent举行解码,而decodeURIComponent只能解码由encodeURIComponent要领或许相似要领编码,假如编码要领不符合请求,decodeURIComponent则会抛出URIError,所以作者在这里对该要领举行了平安化的处置惩罚:

function safeDecodeURIComponent(text) {
  try {
    return decodeURIComponent(text);
  } catch (e) {
    // 编码体式格局不符合请求,返回原字符串
    return text;
  }
}

  Layer还供应了关于单个param前置处置惩罚的要领:

Layer.prototype.param = function (param, fn) {
  var stack = this.stack;
  var params = this.paramNames;
  var middleware = function (ctx, next) {
    return fn.call(this, ctx.params[param], ctx, next);
  };
  middleware.param = param;
  var names = params.map(function (p) {
    return p.name;
  });
  var x = names.indexOf(param);
  if (x > -1) {
    stack.some(function (fn, i) {
      if (!fn.param || names.indexOf(fn.param) > x) {
        // 将单个param前置处置惩罚函数插进去准确的位置
        stack.splice(i, 0, middleware);
        return true; // 跳出轮回
      }
    });
  }

  return this;
};

  上述代码中经由过程some要领寻觅单个param处置惩罚函数的缘由在于以下两点:

  • 坚持param处置惩罚函数位于其他路由处置惩罚函数的前面;
  • 路由中存在多个param参数,须要坚持param处置惩罚函数的前后递次。
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;
};

  Layer中的setPrefix要领用于设置路由途径的前缀,这在嵌套路由的完成中特别主要。

  末了,Layer还供应了依据路由天生url的要领,主要采纳path-to-regexp的compile和parse对路由途径中的param举行替代,而在拼接query的环节,正如前面所说须要对键值对举行烦琐的encodeURIComponent操纵,作者采纳了urijs供应的简约api举行处置惩罚。

四、路由注册

1、Router构造函数

  起首看相识一下Router构造函数:

function Router(opts) {
  if (!(this instanceof Router)) {
    // 限定必需采纳new关键字
    return new Router(opts);
  }

  this.opts = opts || {};
  // 服务器支撑的请求要领, 后续allowedMethods要领会用到
  this.methods = this.opts.methods || [
    'HEAD',
    'OPTIONS',
    'GET',
    'PUT',
    'PATCH',
    'POST',
    'DELETE'
  ];

  this.params = {}; // 保留param前置处置惩罚函数
  this.stack = []; // 存储layer
};

  在构造函数中初始化的params和stack属性最为主要,前者用来保留param前置处置惩罚函数,后者用来保留实例化的Layer对象。而且这两个属性与接下来要讲的路由注册息息相关。

  koa-router中供应两种体式格局注册路由:

  • 详细的HTTP动词注册体式格局,比方:router.get(‘/users’, ctx => {})
  • 支撑一切的HTTP动词注册体式格局,比方:router.all(‘/users’, ctx => {})
2、http METHODS

  源码中采纳methods模块猎取HTTP请求要领名,该模块内部完成主要依赖于http模块:

http.METHODS && http.METHODS.map(function lowerCaseMethod (method) {
  return method.toLowerCase()
})
3、router.verb() and router.all()

  这两种注册路由的体式格局的内部完成基础相似,下面以router.verb()的源码为例:

methods.forEach(function (method) {
  Router.prototype[method] = function (name, path, middleware) {
    var middleware;

    // 1、处置惩罚是不是传入name参数
    // 2、middleware参数支撑middleware1, middleware2...的情势
    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;
  };
});

  该要领第一部份是对传入参数的处置惩罚,关于middleware参数的处置惩罚会让人人联想到ES6中的rest参数,然则rest参数与arguments个中一个致命的区分:

  rest参数只包括那些没有对应形参的实参,而arguments则包括传给函数的一切实参。

  假如采纳rest参数的体式格局,上述函数则必需请求开辟者传入name参数。然则也能够将name和path参数整合成对象,再连系rest参数:

Router.prototype[method] = function (options, ...middleware) {
  let { name, path } = options
  if (typeof options === 'string' || options instanceof RegExp) {
    path = options
    name = null
  }
  // ...
  return this;
};

  采纳ES6的新特征,代码变得简约多了。

  第二部份是register要领,传入的method参数的情势就是router.verb()与router.all()的最大区分,在router.verb()中传入的method是单个要领,后者则是以数组的情势传入HTTP一切的请求要领,所以关于这两种注册要领的完成,本质上是没有区分的。

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

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

  // 注册路由中间件时,许可path为数组
  if (Array.isArray(path)) {
    path.forEach(function (p) {
      router.register.call(router, p, methods, middleware, opts);
    });
    return this;
  }

  // 实例化Layer
  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);
  }

  // 设置param前置处置惩罚函数
  Object.keys(this.params).forEach(function (param) {
    route.param(param, this.params[param]);
  }, this);

  stack.push(route);

  return route;
};

  register要领主要担任实例化Layer对象、更新路由前缀和前置param处置惩罚函数,这些操纵在Layer中已说起过,置信人人应当驾轻就熟了。

5、use

  熟习Koa的同砚都晓得use是用来注册中间件的要领,相比较Koa中的全局中间件,koa-router的中间件则是路由级别的。

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

  // 支撑多途径在于中间件能够作用于多条路由途径
  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) {
    // 嵌套路由
    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 {
      // 路由级别中间件 建立一个没有method的Layer实例
      router.register(path || '(.*)', [], m, { end: false, ignoreCaptures: !hasPath });
    }
  });

  return this;
};

  koa-router中间件注册要领主要完成两项功用:

  • 将路由嵌套构造扁平化,个中涉及到路由途径的更新和param前置处置惩罚函数的插进去;
  • 路由级别中间件经由过程注册一个没有method的Layer实例举行治理。

五、路由婚配

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];
    if (layer.match(path)) {
      // 路由途径满足请求
      matched.path.push(layer);

      if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
        // layer.methods.length === 0 该layer为路由级别中间件
        // ~layer.methods.indexOf(method) 路由请求要领也被婚配
        matched.pathAndMethod.push(layer);
        // 仅当路由途径和路由请求要领都被满足才算是路由被婚配
        if (layer.methods.length) matched.route = true;
      }
    }
  }
  return matched;
};

  match要领主要经由过程layer.match要领以及methods属性对layer举行挑选,返回的matched对象包括以下几个部份:

  • path: 保留一切路由途径被婚配的layer;
  • pathAndMethod: 在路由途径被婚配的条件下,保留路由级别中间件和路由请求要领被婚配的layer;
  • route: 仅当存在路由途径和路由请求要领都被婚配的layer,才算是本次路由被婚配上。

  别的,在ES7之前,关于推断数组是不是包括一个元素,都须要经由过程indexOf要领来完成, 而该要领返回元素的下标,如许就不得不经由过程与-1的比较获得布尔值:

  if (layer.methods.indexOf(method) > -1) {
    ...
  }

  而作者奇妙地应用位运算省去了“憎恶的-1”,当然在ES7中能够愉快地运用includes要领:

  if (layer.methods.includes(method)) {
    ...
  }

六、路由实行流程

  明白koa-router中路由的观点以及路由注册的体式格局,接下来就是怎样作为一个中间件在koa中实行。

  koa中注册koa-router中间件的体式格局以下:

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

const app = new Koa();
const router = new Router();

router.get('/', (ctx, next) => {
  // ctx.router available
});

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

  从代码中能够看出koa-router供应了两个中间件要领:routes和allowedMethods。

1、allowedMethods()
Router.prototype.allowedMethods = function (options) {
  options = options || {};
  var implemented = this.methods;

  return function allowedMethods(ctx, next) {
    return next().then(function() {
      var allowed = {};

      if (!ctx.status || ctx.status === 404) {
        ctx.matched.forEach(function (route) {
          route.methods.forEach(function (method) {
            allowed[method] = method;
          });
        });

        var allowedArr = Object.keys(allowed);

        if (!~implemented.indexOf(ctx.method)) {
          // 服务器不支撑该要领的状况
          if (options.throw) {
            var notImplementedThrowable;
            if (typeof options.notImplemented === 'function') {
              notImplementedThrowable = options.notImplemented();
            } else {
              notImplementedThrowable = new HttpError.NotImplemented();
            }
            throw notImplementedThrowable;
          } else {
            // 响应 501 Not Implemented
            ctx.status = 501;
            ctx.set('Allow', allowedArr.join(', '));
          }
        } else if (allowedArr.length) {
          if (ctx.method === 'OPTIONS') {
            // 猎取服务器对该路由途径支撑的要领鸠合
            ctx.status = 200;
            ctx.body = '';
            ctx.set('Allow', allowedArr.join(', '));
          } else if (!allowed[ctx.method]) {
            if (options.throw) {
              var notAllowedThrowable;
              if (typeof options.methodNotAllowed === 'function') {
                notAllowedThrowable = options.methodNotAllowed();
              } else {
                notAllowedThrowable = new HttpError.MethodNotAllowed();
              }
              throw notAllowedThrowable;
            } else {
              // 响应 405 Method Not Allowed
              ctx.status = 405;
              ctx.set('Allow', allowedArr.join(', '));
            }
          }
        }
      }
    });
  };
};

  allowedMethods()中间件主要用于处置惩罚options请求,响应405和501状况。上述代码中的ctx.matched中保留的恰是前面matched对象中的path(在routes要领中设置,后面会提到。),在matched对象中的path数组不为空的条件条件下:

  • 服务器不支撑当前请求要领,返回501状况码;
  • 当前请求要领为OPTIONS,返回200状况码;
  • path中的layer不支撑该要领,返回405状况;
  • 关于上述三种状况,服务器都邑设置Allow响应头,返回该路由途径上支撑的请求要领。
2、routes()
Router.prototype.routes = Router.prototype.middleware = function () {
  var router = this;
  // 返回中间件处置惩罚函数
  var dispatch = function dispatch(ctx, next) {
    var path = router.opts.routerPath || ctx.routerPath || ctx.path;
    var matched = router.match(path, ctx.method);
    var layerChain, layer, i;

    // 【1】为后续的allowedMethods中间件预备
    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;
    }
    layerChain = matchedLayers.reduce(function(memo, layer) {
      // 【3】路由的前置处置惩罚中间件 主要担任将params、路由别号以及捕捉数组属性挂载在ctx高低文对象中。
      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);
    }, []);
    // 【4】应用koa中间件构造的体式格局,构成一个‘小洋葱’模子
    return compose(layerChain)(ctx, next);
  };

  // 【2】router属性用来use要领中区分路由级别中间件
  dispatch.router = this;
  return dispatch;
};

  routes()中间件主要完成了四大功用。

  • 将matched对象的path属性挂载在ctx.matched上,供应给后续的allowedMethods中间件运用。(见代码中的【1】)
  • 将返回的dispatch函数设置router属性,以便在前面提到的Router.prototype.use要领中区分路由级别中间件和嵌套路由。(见代码中的【2】)
  • 插进去一个新的路由前置处置惩罚中间件,将layer剖析出来的params对象、路由别号以及捕捉数组挂载在ctx高低文中,这类操纵同理Koa在处置惩罚请求之前先构建context对象。(见代码中的【3】)
  • 而关于路由婚配到浩瀚layer,koa-router经由过程koa-compose举行处置惩罚,这和koa关于中间件处置惩罚的体式格局一样的,所以koa-router完整就是一个小型洋葱模子。

七、总结

  koa-router虽然是koa的一个中间件,然则其内部也包括浩瀚的中间件,这些中间件经由过程Layer对象依据路由途径的差别举行分别,使得它们不再像koa的中间件那样每次请求都实行,而是针对每次请求采纳match要领婚配出响应的中间件,再应用koa-compose构成一个中间件实行链。

  以上就是koa-router完成道理的全部内容,愿望能够协助你更好的明白koa-router。

    原文作者:戴庆云
    原文地址: https://segmentfault.com/a/1190000017905043
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞