真实的国产乱ⅩXXX66竹夫人,五月香六月婷婷激情综合,亚洲日本VA一区二区三区,亚洲精品一区二区三区麻豆

成都創(chuàng)新互聯(lián)網(wǎng)站制作重慶分公司

玩轉(zhuǎn)Koa之koa-router原理解析

一、前言

“專業(yè)、務(wù)實、高效、創(chuàng)新、把客戶的事當(dāng)成自己的事”是我們每一個人一直以來堅持追求的企業(yè)文化。 創(chuàng)新互聯(lián)公司是您可以信賴的網(wǎng)站建設(shè)服務(wù)商、專業(yè)的互聯(lián)網(wǎng)服務(wù)提供商! 專注于成都做網(wǎng)站、網(wǎng)站建設(shè)、軟件開發(fā)、設(shè)計服務(wù)業(yè)務(wù)。我們始終堅持以客戶需求為導(dǎo)向,結(jié)合用戶體驗與視覺傳達,提供有針對性的項目解決方案,提供專業(yè)性的建議,創(chuàng)新互聯(lián)建站將不斷地超越自我,追逐市場,引領(lǐng)市場!

Koa為了保持自身的簡潔,并沒有捆綁中間件。但是在實際的開發(fā)中,我們需要和形形色色的中間件打交道,本文將要分析的是經(jīng)常用到的路由中間件 -- koa-router。

如果你對Koa的原理還不了解的話,可以先查看Koa原理解析。

二、koa-router概述

koa-router的源碼只有兩個文件:router.js和layer.js,分別對應(yīng)Router對象和Layer對象。

Layer對象是對單個路由的管理,其中包含的信息有路由路徑(path)、路由請求方法(method)和路由執(zhí)行函數(shù)(middleware),并且提供路由的驗證以及params參數(shù)解析的方法。

相比較Layer對象,Router對象則是對所有注冊路由的統(tǒng)一處理,并且它的API是面向開發(fā)者的。

接下來從以下幾個方面全面解析koa-router的實現(xiàn)原理:

  • Layer對象的實現(xiàn)
  • 路由注冊
  • 路由匹配
  • 路由執(zhí)行流程

三、Layer

Layer對象主要是對單個路由的管理,是整個koa-router中最小的處理單元,后續(xù)模塊的處理都離不開Layer中的方法,這正是首先介紹Layer的重要原因。

function Layer(path, methods, middleware, opts) {
 this.opts = opts || {};
 // 支持路由別名
 this.name = this.opts.name || null;
 this.methods = [];
 this.paramNames = [];
 // 將路由執(zhí)行函數(shù)保存在stack中,支持輸入多個處理函數(shù)
 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、根據(jù)路由路徑生成路由正則表達式
 // 2、將params參數(shù)信息保存在paramNames數(shù)組中
 this.regexp = pathToRegExp(path, this.paramNames, this.opts);
};

Layer構(gòu)造函數(shù)主要用來初始化路由路徑、路由請求方法數(shù)組、路由處理函數(shù)數(shù)組、路由正則表達式以及params參數(shù)信息數(shù)組,其中主要采用path-to-regexp方法根據(jù)路徑字符串生成正則表達式,通過該正則表達式,可以實現(xiàn)路由的匹配以及params參數(shù)的捕獲:

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

// 捕獲params參數(shù)
Layer.prototype.captures = function (path) {
 // 后續(xù)會提到 對于路由級別中間件 無需捕獲params
 if (this.opts.ignoreCaptures) return [];
 return path.match(this.regexp).slice(1);
}

根據(jù)paramNames中的參數(shù)信息以及captrues方法,可以獲取到當(dāng)前路由params參數(shù)的鍵值對:

Layer.prototype.params = function (path, captures, existingParams) {
 var params = existingParams || {};
 for (var len = captures.length, i=0; i

需要注意上述代碼中的safeDecodeURIComponent方法,為了避免

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前置處理函數(shù)插入正確的位置
    stack.splice(i, 0, middleware);
    return true; // 跳出循環(huán)
   }
  });
 }

 return this;
};

上述代碼中通過some方法尋找單個param處理函數(shù)的原因在于以下兩點:

Layer.prototype.setPrefix = function (prefix) {
 if (this.path) {
  this.path = prefix + this.path; // 拼接新的路由路徑
  this.paramNames = [];
  // 根據(jù)新的路由路徑字符串生成正則表達式
  this.regexp = pathToRegExp(this.path, this.paramNames, this.opts);
 }
 return this;
};

Layer中的setPrefix方法用于設(shè)置路由路徑的前綴,這在嵌套路由的實現(xiàn)中尤其重要。

最后,Layer還提供了根據(jù)路由生成url的方法,主要采用path-to-regexp的compile和parse對路由路徑中的param進行替換,而在拼接query的環(huán)節(jié),正如前面所說需要對鍵值對進行繁瑣的encodeURIComponent操作,作者采用了urijs提供的簡潔api進行處理。

四、路由注冊

1、Router構(gòu)造函數(shù)

首先看了解一下Router構(gòu)造函數(shù):

function Router(opts) {
 if (!(this instanceof Router)) {
  // 限制必須采用new關(guān)鍵字
  return new Router(opts);
 }

 this.opts = opts || {};
 // 服務(wù)器支持的請求方法, 后續(xù)allowedMethods方法會用到
 this.methods = this.opts.methods || [
  'HEAD',
  'OPTIONS',
  'GET',
  'PUT',
  'PATCH',
  'POST',
  'DELETE'
 ];

 this.params = {}; // 保存param前置處理函數(shù)
 this.stack = []; // 存儲layer
};

在構(gòu)造函數(shù)中初始化的params和stack屬性最為重要,前者用來保存param前置處理函數(shù),后者用來保存實例化的Layer對象。并且這兩個屬性與接下來要講的路由注冊息息相關(guān)。

koa-router中提供兩種方式注冊路由:

2、http METHODS

源碼中采用methods模塊獲取HTTP請求方法名,該模塊內(nèi)部實現(xiàn)主要依賴于http模塊:

http.METHODS && http.METHODS.map(function lowerCaseMethod (method) {
 return method.toLowerCase()
})

3、router.verb() and router.all()

這兩種注冊路由的方式的內(nèi)部實現(xiàn)基本類似,下面以router.verb()的源碼為例:

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

  // 1、處理是否傳入name參數(shù)
  // 2、middleware參數(shù)支持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;
 };
});

該方法第一部分是對傳入?yún)?shù)的處理,對于middleware參數(shù)的處理會讓大家聯(lián)想到ES6中的rest參數(shù),但是rest參數(shù)與arguments其中一個致命的區(qū)別:

rest參數(shù)只包含那些沒有對應(yīng)形參的實參,而arguments則包含傳給函數(shù)的所有實參。

如果采用rest參數(shù)的方式,上述函數(shù)則必須要求開發(fā)者傳入name參數(shù)。但是也可以將name和path參數(shù)整合成對象,再結(jié)合rest參數(shù):

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參數(shù)的形式就是router.verb()與router.all()的最大區(qū)別,在router.verb()中傳入的method是單個方法,后者則是以數(shù)組的形式傳入HTTP所有的請求方法,所以對于這兩種注冊方法的實現(xiàn),本質(zhì)上是沒有區(qū)別的。

4、register

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

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

 // 注冊路由中間件時,允許path為數(shù)組
 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
 });

 // 設(shè)置前綴
 if (this.opts.prefix) {
  route.setPrefix(this.opts.prefix);
 }

 // 設(shè)置param前置處理函數(shù)
 Object.keys(this.params).forEach(function (param) {
  route.param(param, this.params[param]);
 }, this);

 stack.push(route);

 return route;
};

register方法主要負責(zé)實例化Layer對象、更新路由前綴和前置param處理函數(shù),這些操作在Layer中已經(jīng)提及過,相信大家應(yīng)該輕車熟路了。

5、use

熟悉Koa的同學(xué)都知道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;
 }
 // 處理路由路徑參數(shù)
 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 {
   // 路由級別中間件 創(chuàng)建一個沒有method的Layer實例
   router.register(path || '(.*)', [], m, { end: false, ignoreCaptures: !hasPath });
  }
 });

 return this;
};

koa-router中間件注冊方法主要完成兩項功能:

五、路由匹配

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);
    // 僅當(dāng)路由路徑和路由請求方法都被滿足才算是路由被匹配
    if (layer.methods.length) matched.route = true;
   }
  }
 }
 return matched;
};

match方法主要通過layer.match方法以及methods屬性對layer進行篩選,返回的matched對象包含以下幾個部分:

另外,在ES7之前,對于判斷數(shù)組是否包含一個元素,都需要通過indexOf方法來實現(xiàn), 而該方法返回元素的下標(biāo),這樣就不得不通過與-1的比較得到布爾值:

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

而作者巧妙地利用位運算省去了“討厭的-1”,當(dāng)然在ES7中可以愉快地使用includes方法:

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

六、路由執(zhí)行流程

理解koa-router中路由的概念以及路由注冊的方式,接下來就是如何作為一個中間件在koa中執(zhí)行。

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)) {
     // 服務(wù)器不支持該方法的情況
     if (options.throw) {
      var notImplementedThrowable;
      if (typeof options.notImplemented === 'function') {
       notImplementedThrowable = options.notImplemented();
      } else {
       notImplementedThrowable = new HttpError.NotImplemented();
      }
      throw notImplementedThrowable;
     } else {
      // 響應(yīng) 501 Not Implemented
      ctx.status = 501;
      ctx.set('Allow', allowedArr.join(', '));
     }
    } else if (allowedArr.length) {
     if (ctx.method === 'OPTIONS') {
      // 獲取服務(wù)器對該路由路徑支持的方法集合
      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 {
       // 響應(yīng) 405 Method Not Allowed
       ctx.status = 405;
       ctx.set('Allow', allowedArr.join(', '));
      }
     }
    }
   }
  });
 };
};

allowedMethods()中間件主要用于處理options請求,響應(yīng)405和501狀態(tài)。上述代碼中的ctx.matched中保存的正是前面matched對象中的path(在routes方法中設(shè)置,后面會提到。),在matched對象中的path數(shù)組不為空的前提條件下:

對于上述三種情況,服務(wù)器都會設(shè)置Allow響應(yīng)頭,返回該路由路徑上支持的請求方法。

2、routes()

Router.prototype.routes = Router.prototype.middleware = function () {
 var router = this;
 // 返回中間件處理函數(shù)
 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】為后續(xù)的allowedMethods中間件準(zhǔn)備
  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】路由的前置處理中間件 主要負責(zé)將params、路由別名以及捕獲數(shù)組屬性掛載在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方法中區(qū)別路由級別中間件
 dispatch.router = this;
 return dispatch;
};

routes()中間件主要實現(xiàn)了四大功能。

七、總結(jié)

koa-router雖然是koa的一個中間件,但是其內(nèi)部也包含眾多的中間件,這些中間件通過Layer對象根據(jù)路由路徑的不同進行劃分,使得它們不再像koa的中間件那樣每次請求都執(zhí)行,而是針對每次請求采用match方法匹配出相應(yīng)的中間件,再利用koa-compose形成一個中間件執(zhí)行鏈。

以上便是koa-router實現(xiàn)原理的全部內(nèi)容,希望可以幫助你更好的理解koa-router。也希望大家多多支持創(chuàng)新互聯(lián)。


當(dāng)前標(biāo)題:玩轉(zhuǎn)Koa之koa-router原理解析
當(dāng)前路徑:
http://weahome.cn/article/igpghi.html

其他資訊

在線咨詢

微信咨詢

電話咨詢

028-86922220(工作日)

18980820575(7×24)

提交需求

返回頂部