koa router实现原理

本文两个目的
  1. 了解path-to-regexp使用
  2. koa-router源码解析

path-to-regexp

path-to-regexp用法简介。

如何使用其来匹配识别路由?

想想如果我们要识别路由的话,我们可以怎么做?

最直观肯定是路径字符串全匹配

'/string' => '/string'

当路由全匹配 /string 的时候我们可以做出一些反馈操作。例如执行一个callback等。

我们还可以利用正则匹配特性

这样子匹配模式显然可操作方式更多元,匹配路径也更多

例如对路径path:

/^\/string\/.*?\/xixi$
// => '/string/try/xixi'
path-to-regexp就是一种这样的工具

试想一下如果我们要对路径解析匹配,我们需要自己再去写正则表达式。从而达到匹配效果。

可以写吗?

肯定可以,可是太费时了。

path-to-regexp 它可以帮助我们简单地完成这种操作。

简介path-to-regexp的一些api

how to use it ???
主要api
const pathToRegexp = require('path-to-regexp')

// pathToRegexp(path, keys?, options?)
// pathToRegexp.parse(path)
// pathToRegexp.compile(path)
// pathToRegexp(path, keys?, options?)

// path 可以是string/字符串数组/正则表达式
// keys 存放路径中找到的键数组
// options 是一些匹配规则的填充  例如是否为全匹配 分割符等
path-to-regexp api demo
// 一个demo

如果我们要实现正常的匹配某些键值

eg: 

/user/:name

我们实现这样子的正则如何实现

前部是全匹配,后部用正则分组提取值

eg:

/\/user\/((?!\/).*?)\/?$/.exec('/user/zwkang')

查找匹配正则的字符串  返回一个数组/无值返回一个null

pathToRegexp就是干的这个活。生成需要的正则表达式匹配。当然里面还有一些封装操作,但是本质就是干的这个。
pathToRegexp('/user/:name').exec('/user/zwkang')

path

    option ?
        表示可有可无
        pathToRegexp('/:foo/:bar?').exec('/test')
        pathToRegexp('/:foo/:bar?').exec('/test/route')
        
        * 代表来多少都可以
        + 代表一个或者多个
        
        仔细看你可以发现 这些词跟正则中的量词几乎一致
        
        也可以匹配未命名参数  存储keys时会根据序列下标存储
        同时也支持正则表达式


parse方法

    对path 生成匹配的tokens数组
    
    也就是上文的keys数组
    
    方法适用于string类型
    
Compile 方法

    用compile传入一个path  返回一个可以填充的函数 生成与path匹配的值
    
    pathToRegexp.compile('/user/:id')({id: 123}) => "/user/123"
    
    
    适用于字符串
    
    
pathToRegexp.tokensToRegExp(tokens, keys?, options?) 


pathToRegexp.tokensToFunction(tokens)

名字上可以看出 

一个将tokens数组转化为正则表达式

一个将tokens数组转化为compile方法生成的函数
捋一捋使用步骤
pathToRegexp =返回=> regexp

parse =解析=> path =匹配tokens=> keys token

compile => path => generator function => value => full path string

koa-router

不知道你是否曾使用过koa-router

notic: 注意现在的koa-router的维护权限变更问题

router实现实际上也是一种基于正则的访问路径匹配。

如果是使用koa原生代码

例子:

匹配路径/simple 返回一个body为 {name:'zwkang'}的body string

一个简单的例子,如

假设我们匹配路由 使用一个简单的中间件匹配ctx.url

app.use(async (ctx, next) => {
    const url = ctx.url
    if(/^\/simple$/i.test(url)) {
        ctx.body = {
            name: 'ZWkang'
        }
    } else {
        ctx.body = {
            errorCode: 404,
            message: 'NOT FOUND'
        }
        ctx.status = 404
    }
    return await next()
})

测试代码
describe('use normal koa path', () => {
    it('use error path', (done) => {
        request(http.createServer(app.callback()))
        .get('/simple/s')
        .expect(404)
        .end(function (err, res) {
            if (err) return done(err);
            expect(res.body).to.be.an('object');
            expect(res.body).to.have.property('errorCode', 404)
            done();
        });
    })
    it('use right path', (done) => {
        request(http.createServer(app.callback()))
        .get('/simple')
        .expect(200)
        .end(function (err, res) {
            if (err) return done(err);
            expect(res.body).to.be.an('object');
            expect(res.body).to.have.property('name', 'ZWkang')
            done();
        });
    })
})

以上我们自己实现url的模式就是这样,单一的匹配,如果多元化匹配,甚至匹配参数,需要考虑正则的书写。

缺点,较为单一,设定方法较为简陋,功能弱小

如果我们使用koa-router的话

// 一个简单的用法
it('simple use should work', (done) => {
    router.get('/simple', (ctx, next) => {
        ctx.body = {
            path: 'simple'
        }
    })
    app.use(router.routes()).use(router.allowedMethods());
    request(http.createServer(app.callback()))
      .get('/simple')
      .expect(200)
      .end(function (err, res) {
        if (err) return done(err);
        expect(res.body).to.be.an('object');
        expect(res.body).to.have.property('path', 'simple');
        done();
      });
})
题外话:app.callback()
上方测试代码的一些点解释

callback是koa的运行机制。方法代表了啥? 代表了其setup的过程

而我们的常用listen方法 实际上也是调用了http.createServer(app.callback()) 这么一步唯一


让我们来看看这koa-router到底做了些什么

前置知识

以上面简单例子我们可以看出,理解koa运行机制,内部中间件处理模式。

从demo 入手进行分析

调用koa时候调用的实例方法包括

router.allowedMethods ===> router.routes ===> router.get

考虑因为是koa,use调用,那么我们可以肯定是标准的koa中间件模式

返回的函数类似于

async (ctx, next) => {
    // 处理路由逻辑
    // 处理业务逻辑
}

源码的开头注释给我们讲述了基本的一些用法

我们可以简单提炼一下

router.verb() 根据http方法指定对应函数

例如router.get().post().put()

.all 方法支持所有http 方法

当路由匹配时,ctx._matchedRoute可以在这里获取路径,如果他是命名路由,这里可以得到路由名ctx._matchedRouteName

请求匹配的时候不会考虑querystring(?xxxx)

允许使用具名函数
在开发时候可以快速定位路由
 * router.get('user', '/users/:id', (ctx, next) => {
 *  // ...
 * });
 *
 * router.url('user', 3);
 * // => "/users/3"
允许多路由使用
 * router.get(
 *   '/users/:id',
 *   (ctx, next) => {
 *     return User.findOne(ctx.params.id).then(function(user) {
 *       ctx.user = user;
 *       next();
 *     });
 *   },
 *   ctx => {
 *     console.log(ctx.user);
 *     // => { id: 17, name: "Alex" }
 *   }
 * );
允许嵌套路由
 * var forums = new Router();
 * var posts = new Router();
 *
 * posts.get('/', (ctx, next) => {...});
 * posts.get('/:pid', (ctx, next) => {...});
 * forums.use('/forums/:fid/posts', posts.routes(), posts.allowedMethods());
 *
 * // responds to "/forums/123/posts" and "/forums/123/posts/123"
 * app.use(forums.routes());
允许路由前缀匹配
 var router = new Router({
    prefix: '/users'
 });
 
 router.get('/', ...); // responds to "/users"
 router.get('/:id', ...); // responds to "/users/:id"
捕获命名的参数添加到ctx.params中
 router.get('/:category/:title', (ctx, next) => {
    console.log(ctx.params);
    // => { category: 'programming', title: 'how-to-node' }
 });

代码整体分析

代码设计上有些点挺巧妙
  1. 职责的分离,上层Router做http层method status之类的处理以及routers middlewares相关的处理。低层Layer.js则关注在路由path的处理上
  2. middlerware的设计

不妨先从layer文件理解。

layer.js

前面说了,这个文件主要是用来处理对path-to-regexp库的操作

文件只有300行左右 方法较少,直接截取方法做详细解释。

layer构造函数
function Layer(path, methods, middleware, opts) {
  this.opts = opts || {};
  this.name = this.opts.name || null; // 命名路由
  this.methods = []; // 允许方法
  // [{ name: 'bar', prefix: '/', delimiter: '/', optional: false, repeat: false, pattern: '[^\\/]+?' }]
  this.paramNames = [];
  this.stack = Array.isArray(middleware) ? middleware : [middleware]; // 中间件堆
  // 初始化参数
  // tips : forEach 第二个参数可以传递this 
  // forEach push数组以后 可以使用数组[l-1]进行判断末尾元素
  // push方法返回值是该数组push后元素个数

  // 外部method参数传入内部
  methods.forEach(function(method) {
    var l = this.methods.push(method.toUpperCase());
    // 如果是GET请求 支持HEAD请求
    if (this.methods[l-1] === 'GET') {
      this.methods.unshift('HEAD');
    }
  }, this);

  // ensure middleware is a function
  // 保证每一个middleware 为函数
  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;
  // 利用pathToRegExp 生成路径的正则表达式
  // 与params相关的数组回落入到我们的this.paramNames中
  // this.regexp一个生成用来切割的数组
  this.regexp = pathToRegExp(path, this.paramNames, this.opts);

  debug('defined route %s %s', this.methods, this.opts.prefix + this.path);
};
我们可以关注在输入与输出。

输入:path, methods, middleware, opts

输出:对象 属性包括(opts, name, methods, paramNames, stack, path, regexp)

我们之前说过了 layer是根据route path 做处理 判断是否匹配,连接库path-to-regexp,这一点很重要。

stack应该与传入的middleware一致。stack是数组形式,以此可见我们的path对应的route允许多个的。

我们接下来关注下

根据path-to-regexp结合自身需要的middleware, koa-router给我们处理了什么封装

原型链上挂载方法有
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;
};

在构造函数初始化的时候,我们生成this.regexp的时候通过传入this.paramNames从而将其根据path解析出的param填出

输入: 路径,捕获组,已存在的参数组
输出: 一个参数键值对对象

处理方式很普通。因为params与captures 是位置相对应的。所以直接可以循环即可。

match
// 判断是否匹配
Layer.prototype.match = function (path) {
  return this.regexp.test(path);
};

首先看的也是输入值与返回值

输入: path

输出: 是否匹配的boolean

我们可以看这个this.regexp是属性值,证明我们是有能力随时改变this.regexp 从而影响这个函数的返回值

captures
// 返回参数值
Layer.prototype.captures = function (path) {
  if (this.opts.ignoreCaptures) return []; // 忽略捕获返回空

  // match 返回匹配结果的数组
  // 从正则可以看出生成的正则是一段全匹配。
  /**
   * eg: 
   *    var test = []
   *    pathToRegExp('/:id/name/(.*?)', test)
   * 
   *    /^\/((?:[^\/]+?))\/name\/((?:.*?))(?:\/(?=$))?$/i
   * 
   *    '/xixi/name/ashdjhk'.match(/^\/((?:[^\/]+?))\/name\/((?:.*?))(?:\/(?=$))?$/i)
   * 
   *    ["/xixi/name/ashdjhk", "xixi", "ashdjhk"]
   */

  return path.match(this.regexp).slice(1); // [value, value .....]
};

输入: path路径

输出: 捕获组数组

返回整个捕获组内容

url
Layer.prototype.url = function(params, options) {
  var args = params;
  console.log(this);
  var url = this.path.replace(/\(\.\*\)/g, "");
  var toPath = pathToRegExp.compile(url); //
  var replaced;

  if (typeof params != "object") {
    args = Array.prototype.slice.call(arguments);
    if (typeof args[args.length - 1] == "object") {
      options = args[args.length - 1];
      args = args.slice(0, args.length - 1);
    }
  }
  var tokens = pathToRegExp.parse(url);
  var replace = {};

  if (args instanceof Array) {
    for (var len = tokens.length, i = 0, j = 0; i < len; i++) {
      if (tokens[i].name) replace[tokens[i].name] = args[j++];
    }
  } else if (tokens.some(token => token.name)) {
    replace = params; // replace = params
  } else {
    options = params; // options = params
  }

  replaced = toPath(replace); // 默认情况下 replace 是默认传入的键值对 //匹配过后就是完整的url

  if (options && options.query) {
    // 是否存在query
    var replaced = new uri(replaced); //
    replaced.search(options.query); //添加query 路由查询
    return replaced.toString();
  }

  return replaced; // 返回URL串
};

layer实例的url方法

实际上一个例如/name/:id

我们解析后会获得一个{id: xxx}的params对象

根据/name/:id 跟params对象我们是不是可以反推出实际的url?

这个url方法提供的就是这种能力。

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 String(p.name);
  });
  var x = names.indexOf(param); // 获得index

  if (x > -1) {
    stack.some(function(fn, i) {
      // param handlers are always first, so when we find an fn w/o a param property, stop here
      // if the param handler at this part of the stack comes after the one we are adding, stop here

      // 两个策略
      // 1. param处理器总是在最前面的,当前fn.param不存在。则直接插入 [a,b] mid => [mid, a, b]
      // 2. [mid, a, b]  mid2 => [mid, mid2, a, b]保证按照params的顺序排列
      // 保证在正常中间件前
      // 保证按照params顺序排列
      if (!fn.param || names.indexOf(fn.param) > x) {
        // 在当前注入中间件
        stack.splice(i, 0, middleware);
        return true; // 停止some迭代。
      }
    });
  }
  return this;
};

这个方法的作用是在当前的stack中添加针对单个param的处理器

实际上就是对layer的stack进行一个操作

setPrefix
Layer.prototype.setPrefix = function(prefix) {
  // 调用setPrefix相当于将layer的一些构造重置
  if (this.path) {
    this.path = prefix + this.path;
    this.paramNames = [];
    this.regexp = pathToRegExp(this.path, this.paramNames, this.opts);
  }

  return this;
};

对当前的path加上前缀并且重置当前的一些实例属性

safeDecodeURIComponent
function safeDecodeURIComponent(text) {
  try {
    return decodeURIComponent(text);
  } catch (e) {
    return text;
  }
}

保证safeDecodeURIComponent 不会抛出任何错误

Layer总结。

layer的stack主要是存储实际的middleware[s]。

主要的功能是针对pathToRegexp做设计。
提供能力给上层的Router做调用实现的。


Router

Router主要是对上层koa框架的响应(ctx, status等处理),以及链接下层layer实例。
Router构造函数
function Router(opts) {
    // 自动new
  if (!(this instanceof Router)) {
    return new Router(opts);
  }

  this.opts = opts || {};
  // methods用于对后面allowedMethod做校验的
  this.methods = this.opts.methods || [
    "HEAD",
    "OPTIONS",
    "GET",
    "PUT",
    "PATCH",
    "POST",
    "DELETE"
  ]; // 初始化http方法

  this.params = {}; // 参数键值对
  this.stack = []; // 存储路由实例
}
methods.forEach(function(method) {
  // 给原型上附加所有http method 方法
  Router.prototype[method] = function(name, path, middleware) {
    var middleware;
    // 兼容参数
    // 允许path为字符串或者正则表达式
    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;
    }
    // 注册到当前实例上
    // 主要是设置一个通用的install middleware 的方法。(mark. tag: function)
    this.register(path, [method], middleware, {
      name: name
    });
    // 链式调用
    return this;
  };
});

给Router原型注册上

http method 的方法,如:Router.prototype.get = xxx

当我们使用实例的时候可以更方便准确使用

router.get('name', path, cb)

这里的middleware显然是可以多个。例如router.get(name, path, cb)

我们可以留意到,这里的主要是调用了另一个方法

notic:
register方法。而这个方法的入参,我们可以留意下。与Layer实例初始化入参极为相似。

带着疑惑我们可以进入到register方法内。

register 方法

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

  var router = this;
  var stack = this.stack;
  if (Array.isArray(path)) {
    path.forEach(function(p) {
      router.register.call(router, p, methods, middleware, opts);
    });
    return this;
  }

  var route = new Layer(path, methods, middleware, {
    end: opts.end === false ? opts.end : true, // 需要明确声明为end
    name: opts.name, // 路由的名字
    sensitive: opts.sensitive || this.opts.sensitive || false, // 大小写区分 正则加i
    strict: opts.strict || this.opts.strict || false, // 非捕获分组 加(?:)
    prefix: opts.prefix || this.opts.prefix || "", // 前缀字符
    ignoreCaptures: opts.ignoreCaptures || false // 给layer使用 忽略捕获
  });

  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);
  // 当前Router实例stack push单个layer实例
  stack.push(route);

  return route;
};

我们可以看到整个register方法,是设计给注册单一路径的。

针对多路径在forEach调用register方法。这种写法在koa-router实现里并不少见。。

看了register方法,我们的疑惑得到了证实,果然入参大多是用来初始化layer实例的。

初始化layer实例后,我们将它放置到router实例下的stack中。

根据一些opts再进行处理判断。不多大抵是无伤大雅的。

这样一来我们就知道了register的用法

  1. 初始化layer实例
  2. 将其注册到router实例中。

我们知道我们调用router实例时候。

要使用中间件 我们往往需要完成两步

  1. use(router.routes())
  2. use(router.allowedMethods())

我们知道一个极简的中间件调用形式总是

app.use(async (ctx, next) => {
    await next()
})

我们的不管koa-body 还是koa-router

传入app.use总是一个

async (ctx, next) => {
    await next()
}

这样的函数,是符合koa 中间件需求的。

带着这样的想法

我们可以来到routes方法中一探究竟。

routes原型方法
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;
    // matched已经是进行过处理了 获得了layer对象承载
    var matched = router.match(path, ctx.method);
    var layerChain, layer, i;
    // 考虑多个router实例的情况
    if (ctx.matched) {
      // 因为matched总是一个数组
      // 这里用apply类似于concat
      ctx.matched.push.apply(ctx.matched, matched.path);
    } else {
      // 匹配的路径
      ctx.matched = matched.path;
    }
    // 当前路由
    ctx.router = router;
    // 如果存在匹配的路由
    if (!matched.route) return next();
    // 方法与路径都匹配的layer
    var matchedLayers = matched.pathAndMethod;
    // 最后一个layer
    var mostSpecificLayer = matchedLayers[matchedLayers.length - 1];
    //
    ctx._matchedRoute = mostSpecificLayer.path;

    // 如果layer存在命名
    if (mostSpecificLayer.name) {
      ctx._matchedRouteName = mostSpecificLayer.name;
    }
    // 匹配的layer进行compose操作

    // update capture params routerName等

    // 例如我们使用了多个路由的话。
    // => ctx.capture, ctx.params, ctx.routerName => layer Stack[s]
    // => ctx.capture, ctx.params, ctx.routerName => next layer Stack[s]
    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;
};

我们知道路由匹配的本质是实际路由与定义路径相匹配。

那么routes生成的中间件实际上就是在考虑做这种匹配的处理。

从返回值我们可以看到

=> dispatch方法。

这个dispacth方法实际上就是我们前面说的极简方式。

function dispatch(ctx, next) {}

可以说是相差无几。

我们知道stack当前存储的是多个layer实例。

而根据路径的匹配,我们可以知道

一个后端路径,简单可以分为http方法,与路径定义匹配。

例如:/name/:id

这个时候来了个请求/name/3

是不是匹配了。(params = {id: 3})

但是请求方法如果是get呢? 定义的这个/name/:id是个post的话。

则此时虽然路径匹配,但是实际并不能完全匹配。

原型方法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);
      // matched中压入layer

      if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
        // 校验方法
        matched.pathAndMethod.push(layer);
        // 路径与方法中都压入layer
        if (layer.methods.length) matched.route = true;
        // 证明没有支持的方法。route为true 后面跳过中间件处理
      }
    }
  }

  return matched;
};

看看这个match方法吧。

对stack中的layaer进行判断。

返回的matched对象中

path属性: 仅仅路径匹配即可。

pathAndMethod属性: 仅仅http方法与路径匹配即可。

route属性: 需要layer的方法长度不为0(有定义方法。)


所以dispatch中我们首先

ctx.matched = matched.path

得到路径匹配的layer

实际中间件处理的,是http方法且路径匹配的layer

这种情况下。而实际上,所谓中间件就是一个个数组

它的堆叠方式可能是多维的,也可能是一维的。

如果一个route进行了匹配

ctx._matchedRoute代表了它的路径。

这里ctx._matchedRoute是方法且路径匹配数组的layer的最后一个。

相信取最后一个大家也知道为什么。多个路径,除开当前处理,在下一个中间件处理时候,总是返回最后一个即可。

最后将符合的layer组合起来

例如 如果有多个layer的情况下,layer也有多个stack的情况下

// 例如我们使用了多个路由的话。
// => ctx.capture, ctx.params, ctx.routerName => layer Stack[?s]
// => ctx.capture, ctx.params, ctx.routerName => next layer Stack[?s]

运行顺序就会如上所示
相当于在将多个layer实例的stack展平,且在每一个layer实例前,添加ctx属性进行使用。

最后用compose将这个展平的数组一起拿来使用。

其实在这里我们可以留意到,所谓的中间件也不过是一堆数组罢了。

但是这里的在每个layer实例前使用ctx属性倒是个不错的想法。

对中间件的操作例如prefix等。就是不断的对内部的stack位置属性的调整。

allowedMethods方法
Router.prototype.allowedMethods = function(options) {
  options = options || {};
  var implemented = this.methods;
  // 返回一个中间件用于 app.use注册。
  return function allowedMethods(ctx, next) {
    return next().then(function() {
      var allowed = {};
      // 判断ctx.status 或者状态码为404
      console.log(ctx.matched, ctx.method, implemented);

      if (!ctx.status || ctx.status === 404) {
        // routes方法生成的ctx.matched
        // 就是筛选出来的layer匹配组
        ctx.matched.forEach(function(route) {
          route.methods.forEach(function(method) {
            allowed[method] = method;
          });
        });

        var allowedArr = Object.keys(allowed);
        // 实现了的路由匹配
        if (!~implemented.indexOf(ctx.method)) {
          // 位运算符 ~(-1) === 0 !0 == true
          // options参数 throw如果为true的话则直接扔出错误
          // 这样可以给上层中间价做处理
          // 默认是抛出一个HttpError
          if (options.throw) {
            var notImplementedThrowable;
            if (typeof options.notImplemented === "function") {
              notImplementedThrowable = options.notImplemented(); // set whatever the user returns from their function
            } else {
              notImplementedThrowable = new HttpError.NotImplemented();
            }
            throw notImplementedThrowable;
          } else {
            // 否则跑出501
            // 501=>服务器未实现方法
            ctx.status = 501;
            ctx.set("Allow", allowedArr.join(", "));
          }
          // 如果允许的话
        } else if (allowedArr.length) {
          // 对options请求进行操作。
          // options请求与get请求类似,但是请求没有请求体 只有头。
          // 常用语查询操作
          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(); // set whatever the user returns from their function
              } else {
                notAllowedThrowable = new HttpError.MethodNotAllowed();
              }
              throw notAllowedThrowable;
            } else {
              // 405 方法不被允许
              ctx.status = 405;
              ctx.set("Allow", allowedArr.join(", "));
            }
          }
        }
      }
    });
  };
};

这个方法主要是默认的给我们路由中间件添加404 405 501的这些状态控制。

我们也可以在高层中间件统一处理也可以。

使用位运算符+indexOf也是一种常见的用法。


全文总结

至此整篇的koa-router源码基本就解析完毕了。

虽然Router的源码还有很多方法本文没有写出,但是大多都是给上层提供layer实例的方法连接,欢迎到github链接从源码处查看。

总的来说能吸收的点可能是挺多的。

如果看完了整篇。

  1. 相信你对koa middleware使用应该是更得心应手了。
  2. 相信你对koa-router的源码架构具体方法实现应该是有所了解。
  3. 学习如何阅读源码,构建测试用例,了解入参与输出。

我的博客 zwkang.com

源码地址(注释解析版) koa-router分支


ZWkang
594 声望18 粉丝

所有的flag都完不成的。