Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test #52

Open
ZWkang opened this issue Aug 21, 2021 · 0 comments
Open

test #52

ZWkang opened this issue Aug 21, 2021 · 0 comments

Comments

@ZWkang
Copy link
Owner

ZWkang commented Aug 21, 2021


title: '声明式与命令式编程'
date: '2019-08-13T07:08:47Z'
author:
name: ZWkang

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant