koa-router實現原理

koa router實現原理

本文兩個目的
  1. 瞭解path-to-regexp使用
  2. koa-router源碼解析

path-to-regexp

path-to-regexp用法簡介。

如何匹配識別路由

想想如果我們要識別路由的話,我們可以怎麼做?

最直觀肯定是正則全匹配

'/string' => '/string'

當路由全匹配 /string 的時候我們可以做出一些反饋操作。例如執行一個callback等。

我們還可以利用正則匹配特性

這樣子匹配模式顯然可操作方式更多元,匹配路徑也更多

例如:

/^\/string\/.*?\/xixi$/ 
// => '/string/try/xixi'
path-to-regexp就是一種這樣的工具

試想一下如果我們要對路徑解析匹配,我們需要自己再去寫正則表達式。從而達到匹配效果。

可以寫嗎?

肯定可以,可是太費時了。

path-to-regexp 它可以幫助我們簡單地完成這種操作。

簡介path-to-regexp的一些api

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

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

// path 可以是string/字符串數組/正則表達式
// keys 存放路徑中找到的鍵數組
// options 是一些匹配規則的填充  例如是否爲全匹配 分割符等
// 一個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 => keys token

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

koa-router

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

notic: 注意koa-router的維護權限變更問題

router實現實際上也是一種基於正則的訪問路徑匹配。

如果是使用koa原生代碼

例子:

匹配路徑/simple 返回一個body爲

寫一些簡單的代碼

假設我們匹配路由 使用一個簡單的中間件匹配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();
      });
})

讓我們來看看這koa-router到底做了些什麼

前置知識

理解koa運行機制,內部中間件處理模式。

以上面簡單例子我們可以看出

上方測試代碼的一些點解釋

app.callback()

callback是koa的運行機制。方法代表了啥? 代表了其setup的過程

而我們的常用listen方法 實際上也是調用了http.createServer(app.callback()) 這麼一步唯一

從demo 方法入手

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

因爲是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則關注在路由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實例。

初始化

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方法內。


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方法中一探究竟。

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的話。

則此時雖然路徑匹配,但是實際並不能完全匹配。

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實例的方法連接。

總的來說能吸收的點可能是挺多的。

如果看完了整篇。

  1. 相信你對koa middleware使用應該是更得心應手了。
  2. 相信你對koa-router的源碼架構具體方法實現應該是有所瞭解。

我的博客 zwkang.com

源碼地址(註釋解析版) koa-router分支

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章