解讀并實(shí)現(xiàn)一個(gè)簡(jiǎn)單的koa-router

Koa 應(yīng)用程序是一個(gè)包含一組中間件函數(shù)的對(duì)象敬鬓,它是按照類(lèi)似堆棧的方式組織和執(zhí)行的。

這是 koa 對(duì)自己的介紹笙各,其他 koa 依賴(lài)的庫(kù)其實(shí)都可以算是中間件钉答,koa-router 也不例外。

ps: 本文代碼中的中文解釋是對(duì)代碼的講解杈抢,省略號(hào)(...)代表省略部分代碼
文章最后有簡(jiǎn)版router的項(xiàng)目地址

對(duì) koa-router 的猜想

通過(guò) koa 最簡(jiǎn)單的 hellow world 例子可以看出原生對(duì)請(qǐng)求的處理方式:

const Koa = require('koa');
const app = new Koa();

app.use(async ctx => {
  ctx.body = 'Hello World';
});

app.listen(3000);

要是我們想簡(jiǎn)單的實(shí)現(xiàn)路由的話数尿,可以添加一些判斷條件

app.use(async ctx => {
  if (ctx.path === '/one' && ctx.method === 'get') {
    ctx.body = 'Hello World';
  } else {
    ctx.status = 404;
    ctx.body = '';
  }
});

這樣的話能實(shí)現(xiàn)簡(jiǎn)單對(duì)路由的實(shí)現(xiàn),不過(guò)路由越多的話消耗的性能也就越大惶楼,而且不容易對(duì)特殊路由添加中間件右蹦。而更好的方法是使用面向?qū)ο蟮姆绞剑鶕?jù)請(qǐng)求的 path 和 method 返回相應(yīng)的中間件處理函數(shù)和執(zhí)行函數(shù)歼捐。

解讀思路

這里要介紹下我解讀 koa-router 源碼的方法何陆,我會(huì)先把 koa-router 的源碼下載到本地,然后通讀一遍(因?yàn)樵创a算是比較少的)豹储,從大體上知道 koa-router 執(zhí)行流程贷盲,然后通過(guò)單元測(cè)試去 debug 分析。

Router 執(zhí)行流程圖

koa-router 流程.png

我認(rèn)為 koa-router 最基本且核心的API有四個(gè):

  1. router.match
    可以根據(jù)請(qǐng)求的 path 和 method 篩選出匹配的 route
  2. router.register
    注冊(cè) route
  3. router.routes
    返回用于 koa 加載的中間件剥扣,通過(guò) koa-compose 將middlewares 壓縮成一個(gè)函數(shù)
  4. router.method(get巩剖、post等)
    可以根據(jù)path、method 定義 router钠怯,并且可以將middleware綁定在路由上

解讀

我們可以結(jié)合代碼和單元測(cè)試對(duì)源碼進(jìn)行理解佳魔,由最簡(jiǎn)單的測(cè)試開(kāi)始debug:

it('router can be accecced with ctx', function (done) {
      var app = new Koa();
      var router = new Router();
      router.get('home', '/', function (ctx) {
          ctx.body = {
            url: ctx.router.url('home')
          };
      });

      console.log(router.routes()); // 這是我加的,查看最后加載的routes
      app.use(router.routes());
      request(http.createServer(app.callback()))
          .get('/')
          .expect(200)
          .end(function (err, res) {
              if (err) return done(err);
              expect(res.body.url).to.eql("/");
              done();
          });
  });

router.routes() 返回:

function dispatch(ctx, next) {
    debug('%s %s', ctx.method, ctx.path);
    var path = router.opts.routerPath || ctx.routerPath || ctx.path;
    var matched = router.match(path, ctx.method);
    var layerChain, layer, i;
    ...
    ctx.router = router;
    if (!matched.route) return next();
    // 獲取已匹配的 routes (實(shí)例化 Layer 對(duì)象)
    var matchedLayers = matched.pathAndMethod
    ...
    // 若匹配了多個(gè) route呻疹,則將多個(gè)執(zhí)行函數(shù) push 進(jìn)一個(gè)數(shù)組
    layerChain = matchedLayers.reduce(function(memo, layer) {
      ...
      return memo.concat(layer.stack);
    }, []);

    return compose(layerChain)(ctx, next);
  }

router.routes() 返回一個(gè) dispatch 函數(shù)吃引,從中可以看出請(qǐng)求進(jìn)來(lái)會(huì)經(jīng)過(guò) router.match(后面有分析)筹陵,然后將匹配到的 route 的執(zhí)行函數(shù) push 進(jìn)數(shù)組,并通過(guò) compose(koa-compose) 函數(shù)合并返回镊尺。

然后在打印出 compose(layerChain) 方法朦佩,可以看到其實(shí)最后請(qǐng)求執(zhí)行的函數(shù)是對(duì)ctx.body = {url: ctx.router.url('home')}; 的 compose 封裝函數(shù),在效果上相當(dāng)于

app.use(ctx => {
  ctx.body = {
    url: ctx.router.url('home')
  };
});
  • Router 構(gòu)造函數(shù)
function Router(opts) {
  if (!(this instanceof Router)) {
    return new Router(opts);
  }

  this.opts = opts || {};
  // 定義各方法
  this.methods = this.opts.methods || [
    'HEAD',
    'OPTIONS',
    'GET',
    'PUT',
    'PATCH',
    'POST',
    'DELETE'
  ];

  this.params = {};
  // 初始化定義 route 棧
  this.stack = [];
};
  • 分析 router.method 方法
// methods ['get', 'post', 'delete', 'put', 'patch', ...]
methods.forEach(function (method) {
  Router.prototype[method] = function (name, path, middleware) {
    var middleware;

    if (typeof path === 'string' || path instanceof RegExp) {
      // 若第二個(gè)參數(shù)是 string 或 正則表達(dá)式庐氮,則將后面的參數(shù)歸為 middleware
      middleware = Array.prototype.slice.call(arguments, 2);
    } else {
      // 否則說(shuō)明沒(méi)有傳 name 參數(shù)语稠,將第一個(gè)參數(shù)置為path,之后的參數(shù)歸為 middleware
      middleware = Array.prototype.slice.call(arguments, 1);
      path = name;
      name = null;
    }

    // 注冊(cè) route(下面會(huì)講到 register 方法)
    this.register(path, [method], middleware, {
      name: name
    });
    
    // 返回 Router 對(duì)象弄砍,可以鏈?zhǔn)秸{(diào)用
    return this;
  };
});
  • 分析 router.register 方法
Router.prototype.register = function (path, methods, middleware, opts) {
  opts = opts || {};

  var stack = this.stack;
  ...
  // create route
  // 實(shí)例化一個(gè) Layer 對(duì)象仙畦,Layer 對(duì)象將 path 轉(zhuǎn)為 regexp,并增加了匹配 path 的可選 ops 參數(shù)
  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
  });

  console.log(route);
  /**
   * Layer {
   * ...省略部分屬性
   * methods: [ 'HEAD', 'GET' ],
   * stack: [ [Function] ],
   * path: '/',
   * regexp: { /^(?:\/(?=$))?$/i keys: [] } } // 用于匹配 path
   */
  ...
  // 將注冊(cè)的 route 存放在 stack 隊(duì)列中
  stack.push(route);

  return route;
};

register 方法主要用于實(shí)例化 Layer 對(duì)象音婶,并支持多各 path 同時(shí)注冊(cè)慨畸、添加路由前綴等功能(展示代碼忽略)。

  • 分析 router.match
Router.prototype.match = function (path, method) {
  // 獲取已經(jīng)注冊(cè)的 routes (實(shí)例化Layer對(duì)象)
  var layers = this.stack;
  var layer;
  var matched = {
    path: [],
    pathAndMethod: [],
    route: false
  };

  // 循環(huán)查找能夠匹配的route
  for (var len = layers.length, i = 0; i < len; i++) {
    layer = layers[i];

    debug('test %s %s', layer.path, layer.regexp);

    // 根據(jù)layer.regexp.test(path) 匹配
    if (layer.match(path)) {
      matched.path.push(layer);

      // todo ~操作符暫時(shí)沒(méi)懂
      if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
        matched.pathAndMethod.push(layer);
        // 將匹配標(biāo)志 route 設(shè)為 true衣式,這里我覺(jué)得改為 hitRoute 更容易理解
        if (layer.methods.length) matched.route = true;
      }
    }
  }

  return matched;
};

實(shí)現(xiàn)簡(jiǎn)版Router

通過(guò)上面的分析寸士,其實(shí)已經(jīng)講解了 koa-router 核心的部分:構(gòu)造 Router 對(duì)象 => 定義 router 入口 => 匹配路由 => 合并中間件和執(zhí)行函數(shù)輸出;這4個(gè)API可以處理簡(jiǎn)單的 restful 請(qǐng)求碴卧,額外的API例如重定向弱卡、router.use、路由前綴等在了解核心代碼后閱讀起來(lái)就簡(jiǎn)單很多了住册;簡(jiǎn)版其實(shí)就是上面api的精簡(jiǎn)版婶博,原理一致,可以到我的項(xiàng)目看下
simple-koa-router:https://github.com/masongzhi/simple-koa-router

總結(jié)

koa-router 幫我們定義并選擇相應(yīng)的路由荧飞,對(duì)路由添加中間件和一些兼容和驗(yàn)證的工作凡人;在 koa 中間件應(yīng)用的基礎(chǔ)上,比較容易理解中間件的實(shí)現(xiàn)垢箕,koa-router 為我們做了更好的路由層管理划栓,在設(shè)計(jì)上可以參考實(shí)現(xiàn)兑巾,同時(shí)研究?jī)?yōu)美源碼也是對(duì)自己的一種提升条获。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市蒋歌,隨后出現(xiàn)的幾起案子帅掘,更是在濱河造成了極大的恐慌,老刑警劉巖堂油,帶你破解...
    沈念sama閱讀 211,123評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件修档,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡府框,警方通過(guò)查閱死者的電腦和手機(jī)吱窝,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人院峡,你說(shuō)我怎么就攤上這事兴使。” “怎么了照激?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,723評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵发魄,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我俩垃,道長(zhǎng)励幼,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,357評(píng)論 1 283
  • 正文 為了忘掉前任口柳,我火速辦了婚禮苹粟,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘跃闹。我一直安慰自己六水,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,412評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布辣卒。 她就那樣靜靜地躺著掷贾,像睡著了一般。 火紅的嫁衣襯著肌膚如雪荣茫。 梳的紋絲不亂的頭發(fā)上想帅,一...
    開(kāi)封第一講書(shū)人閱讀 49,760評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音啡莉,去河邊找鬼港准。 笑死,一個(gè)胖子當(dāng)著我的面吹牛咧欣,可吹牛的內(nèi)容都是我干的浅缸。 我是一名探鬼主播,決...
    沈念sama閱讀 38,904評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼魄咕,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼衩椒!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起哮兰,我...
    開(kāi)封第一講書(shū)人閱讀 37,672評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤毛萌,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后喝滞,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體阁将,經(jīng)...
    沈念sama閱讀 44,118評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,456評(píng)論 2 325
  • 正文 我和宋清朗相戀三年右遭,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了做盅。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片缤削。...
    茶點(diǎn)故事閱讀 38,599評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖吹榴,靈堂內(nèi)的尸體忽然破棺而出僻他,到底是詐尸還是另有隱情,我是刑警寧澤腊尚,帶...
    沈念sama閱讀 34,264評(píng)論 4 328
  • 正文 年R本政府宣布吨拗,位于F島的核電站,受9級(jí)特大地震影響婿斥,放射性物質(zhì)發(fā)生泄漏劝篷。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,857評(píng)論 3 312
  • 文/蒙蒙 一民宿、第九天 我趴在偏房一處隱蔽的房頂上張望娇妓。 院中可真熱鬧,春花似錦活鹰、人聲如沸哈恰。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,731評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)着绷。三九已至,卻和暖如春锌云,著一層夾襖步出監(jiān)牢的瞬間荠医,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,956評(píng)論 1 264
  • 我被黑心中介騙來(lái)泰國(guó)打工桑涎, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留彬向,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,286評(píng)論 2 360
  • 正文 我出身青樓攻冷,卻偏偏與公主長(zhǎng)得像娃胆,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子等曼,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,465評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • Koa 必須使用 7.6 以上的版本里烦。如果你的版本低于這個(gè)要求,就要先升級(jí) Node涉兽。 基本用法 Koa 提供一個(gè)...
    Gukson666閱讀 2,445評(píng)論 0 1
  • 使用體驗(yàn) koa express 注意:本文全部采用es6語(yǔ)法編寫(xiě)招驴,如果環(huán)境不支持請(qǐng)自行升級(jí)node或者使用bab...
    shanyy閱讀 3,428評(píng)論 0 10
  • Address:https://www.zybuluo.com/XiangZhou/note/208532 Exp...
    天蠍蒗漫閱讀 11,286評(píng)論 2 55
  • 1. 簡(jiǎn)介 這篇文章主要的目的是分析理解express的源碼篙程,網(wǎng)絡(luò)上關(guān)于源碼的分析已經(jīng)數(shù)不勝數(shù)枷畏,這篇文章準(zhǔn)備另辟蹊...
    沒(méi)事造輪子閱讀 1,313評(píng)論 0 8
  • 01 突然想起朋友Y跟我講的一個(gè)故事,她的一個(gè)男同學(xué)打小特別喜歡火車(chē)虱饿,自己研究火車(chē)構(gòu)造拥诡、畫(huà)火車(chē)触趴、收集火車(chē)模型,甚至...
    箏小錢(qián)閱讀 715評(píng)論 9 50