7

koa-router源码解析

 3 years ago
source link: https://zwkang.com/?p=817
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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路径

输出: 捕获组数组

返回整个捕获组内容

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分支


Contact Me

all encode by base64

  • Email: a2FuZzk1NjMwQGdtYWlsLmNvbQ==
  • QQ: OTA3NzQ3ODc0QHFxLmNvbQo=

转载需注明出处与作者
否则将被视为侵权

Comments

发表评论 取消回复

电子邮件地址不会被公开。 必填项已用*标注

评论

姓名 *

电子邮件 *

站点

在此浏览器中保存我的名字、电邮和网站。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK