koa源碼部分解析(2)koa-router

版本7.4.0

比較長,建議一邊看一邊打斷點,光看一遍估計是不知道在說什麼的,因爲它的思路不是面向過程的那種一條線,而是互相穿插,繞來繞去的。

koa-router的核心其實是path-to-regexp這個庫,不過koa-router做了非常多的參數處理和封裝,主要提供的功能有:基礎的rest方法的註冊、嵌套路由、路由中間層、參數處理等,我們一個一個來看。

先來看看koa-router的基本使用

var Koa = require('koa');
var Router = require('koa-router');
var app = new Koa();
var router = new Router();

router.get('/a', (ctx, next) => {
  console.log(ctx.router)
  next();
  ctx.body = 'aaa';
}, (ctx, next) => {
  console.log('a2')
});

// 命名路由
router.get('b', '/b', (ctx, next) => {
  console.log(ctx.router)
  ctx.body = 'bbb';
});

app
  .use(router.routes());

app.listen(3000);

這裏比較關鍵的步驟如下:

1.實例化了Router

2.調用實例的get方法,定義了兩個路由a和b

3.調用實例的routes方法,將返回註冊爲應用的中間件

我們要說的,除了上述幾個點以外,還有register、use、param函數,嵌套的路由如何實現,路由的前綴形式等

構造和verb的註冊

我們按順序來看,首先是構造函數,構造函數沒幹啥,初始化了opts、methods、params、stack幾個變量,緊跟着通過methods.forEach在prototype上定義了get、post等方法,methods的列表可以自己看一下,在這裏https://github.com/jshttp/methods/blob/master/index.js。這些方法進行了參數處理後直接調用了register函數

function Router(opts) {
  if (!(this instanceof Router)) {
    return new Router(opts);
  }

  this.opts = opts || {};
  // this.methods只在allowedMethods裏面用到了,如果不提供methods參數,下面這些就是默認實現的方法
  this.methods = this.opts.methods || [
    'HEAD',
    'OPTIONS',
    'GET',
    'PUT',
    'PATCH',
    'POST',
    'DELETE'
  ];
	// params保存參數處理相關內容,stack保存Layer實例
  this.params = {};
  this.stack = [];
};
// 這個methods和上面的this.methods要區分一下
methods.forEach(function (method) {
  Router.prototype[method] = function (name, path, middleware) {
    var middleware;

    if (typeof path === 'string' || path instanceof RegExp) {
    	// 注意這兩個slice將middleware變成了數組,註冊的時候可以傳多個方法
    	// 可以看看上面第一個例子裏的/a路徑註冊的處理函數
      middleware = Array.prototype.slice.call(arguments, 2);
    } else {
      middleware = Array.prototype.slice.call(arguments, 1);
      path = name;
      name = null;
    }
		// 調用了register方法
    this.register(path, [method], middleware, {
      name: name
    });
		// 這一句保證了鏈式調用
    return this;
  };
});

register

在看register方法的代碼前我們來看一看register方法的簡單使用,注意register支持傳入數組形式的path,而第二個參數methods只能是數組形式,

router.register(['/c', '/cc'], ['GET', 'POST'], (ctx, next) => {
  console.log(ctx.router)
  ctx.body = 'register';
});

register方法代碼如下,比較重要的幾個地方,一是通過path.forEach將傳入的數組形式的路徑,分別單獨調用register,二是通過Layer構造了一個route對象,並且push到stack裏,三是對param的處理

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

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

  // support array of paths
  if (Array.isArray(path)) {
  	// 如果path是數組,每個路徑單獨調用register方法
    path.forEach(function (p) {
      router.register.call(router, p, methods, middleware, opts);
    });

    return this;
  }

	// 調用Layer的構造函數
  // create route
  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
  });

  if (this.opts.prefix) {
  	// 處理實例化的時候傳入的prefix選項,參見下文的嵌套路由部分,setPrefix是Layer的函數
    route.setPrefix(this.opts.prefix);
  }
	
  // add parameter middleware
  // 對param的處理
  Object.keys(this.params).forEach(function (param) {
    route.param(param, this.params[param]);
  }, this);
	// 所有註冊的路由全都存在stack裏面,每個path作爲一個元素,一個path可能有多個methods和多個處理函數
  stack.push(route);

  return route;
};

因爲register裏面有用到,所以我們來看一看Layer的構造函數,注意這裏的this.methods和this.stack和前面不一樣了,是Layer自己定義的

function Layer(path, methods, middleware, opts) {
  this.opts = opts || {};
  this.name = this.opts.name || null;
  this.methods = [];
  this.paramNames = [];
  this.stack = Array.isArray(middleware) ? middleware : [middleware];

	// register的methods參數必須是數組是因爲這裏直接拿來就forEach
  methods.forEach(function(method) {
    var m = this.methods.push(method.toUpperCase());
    // 如果註冊了一個get請求,那麼添加一個對應的head請求的處理
    if (this.methods[m-1] === 'GET') {
      this.methods.unshift('HEAD');
    }
  }, this);

  // ensure middleware is a function
  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;
  // 生成路徑匹配用的正則表達式
  // paramNames是pathToRegExp來賦值的,如果路徑是/foo/:bar這樣的,paramNames裏面就會有值
  // 如果是/foo/bar,paramNames就是空數組
  this.regexp = pathToRegExp(path, this.paramNames, this.opts);

  debug('defined route %s %s', this.methods, this.opts.prefix + this.path);
};

實際上Layer返回的route是這樣麼一個對象(下面是以本文第一個例子裏的b的那個路由爲例得到的值),其中opts是一些pathToRegExp接受的選項,regex是pathToRegExp的返回值,stack是我們註冊路由的處理函數,建議自己打斷點看一下

{
  methods: ['HEAD', 'GET'],
  name: null,
  opts: Object
  paramNames: [],
  path: '/b',
  regex: Object,
  stack: [middleware]
}

到這裏爲止關於register的函數我們看完了,可能貼的代碼有點多不方便看,但是最核心的部分其實只有一個pathToRegExp,另外都是一些參數的處理。

routes

回看一眼本文開頭的例子,所有路由註冊完畢以後,我們會調用routes方法,除去allowedMethods,實際上看完routes方法我們就算完成了一個簡單的流程。

在看routes方法之前,我們可以確定的是,routes方法執行後返回的一定是koa中間件的格式,也就是一個函數,並且有ctx和next兩個參數。

routes方法的代碼如下,核心就是dispatch函數,我們的所有路由都會經過這個函數,裏面調用了Router.prototype.match,用我們stack裏面的全部middleware的regexp去匹配請求路徑,匹配成功的middleware的處理函數先轉成數組,再通過compose轉換成單個函數執行。這裏還是比較繞的,建議自己打斷點看一下每一步的結果。

// middleware和routes是一樣的
Router.prototype.routes = Router.prototype.middleware = function () {
  var router = this;

	// 最終的處理路由的地方就是這個函數
  var dispatch = function dispatch(ctx, next) {
    debug('%s %s', ctx.method, ctx.path);
    
		// ctx.path是koa賦值的,ctx.routerPath不知道是哪裏來的
    var path = router.opts.routerPath || ctx.routerPath || ctx.path;
    // 調用了match方法,根據this.stack裏面註冊的路由生成的正則,一個一個匹配,
    // 返回是否匹配成功和匹配到的Layer實例
    var matched = router.match(path, ctx.method);
    var layerChain, layer, i;

    if (ctx.matched) {
      ctx.matched.push.apply(ctx.matched, matched.path);
    } else {
      ctx.matched = matched.path;
    }

		// 在處理函數裏可以獲取ctx.router
    ctx.router = router;
    
		// route標誌不爲true就執行下一個中間件,沒有匹配成功或者是use方法註冊的路由都會導致route標誌爲false
    if (!matched.route) return next();
    
    // pathAndMethod是我們註冊的middleware,
		// 往ctx裏放兩個值_matchedRoute和_matchedRouteName
    var matchedLayers = matched.pathAndMethod
    var mostSpecificLayer = matchedLayers[matchedLayers.length - 1]
    ctx._matchedRoute = mostSpecificLayer.path;
    if (mostSpecificLayer.name) {
      ctx._matchedRouteName = mostSpecificLayer.name;
    }
    
		// 匹配成功的layer,拼成一個數組
    layerChain = matchedLayers.reduce(function(memo, layer) {
      memo.push(function(ctx, next) {
        ctx.captures = layer.captures(path, ctx.captures);
        // 對param的處理
        ctx.params = layer.params(path, ctx.captures, ctx.params);
        ctx.routerName = layer.name;
        return next();
      });
      return memo.concat(layer.stack);
    }, []);
    
		// 通過compose將數組形式的layers變成一個嵌套的函數並執行,這裏return的是compose後執行的結果
		// 是一個Promise.resolve或者Promise.reject
    return compose(layerChain)(ctx, next);
  };

  dispatch.router = this;

  return dispatch;
};

這裏還是要貼一下match的代碼,因爲下面講 match的時候發現有挺多細節和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);

      if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
        matched.pathAndMethod.push(layer);
        // 只有當註冊的methods不爲空,纔會將route標誌位設爲true,這個標誌位在上面的routes方法裏有用到,
        // 如果只使用use方法來註冊某個路由,這個判斷就不會通過,導致註冊的中間件不會執行
        // 但是如果使用verb註冊過某個路由,use也註冊了這個路由或者use註冊了一個不帶路由的中間件,use註冊的中間件就會執行
        if (layer.methods.length) matched.route = true;
      }
    }
  }

  return matched;
};

這樣基本的流程我們就說完了,基本流程用一個圖表示就是:verb註冊 => register => 調用routes()

接下來我們說些進階的使用和實現

嵌套路由

嵌套路由應該還是比較常用的,我們先來看一個例子

var test = new Router({
	// 注意這裏
  prefix: '/test'
});
var forums = new Router();

test.get('/test', (ctx, next) => {
  console.log('test');
  ctx.body = 'test';
});

// 注意這裏
test.prefix('/prefix');

// 注意這裏
forums.use('/forums/:fid?', test.routes(), test.allowedMethods());

app.use(forums.routes());

app.listen(3001);

這個例子裏,要注意三個地方,第一個是test在實例化的時候,傳入了prefix前綴,第二個是test調用了prefix方法,爲自己註冊的router添加了prefix這個前綴,第三個是forums,它調用了use方法,傳入了test定義的router,對/forums/123/prefix/test/test/forums/prefix/test/test這樣的路徑會有響應。

實例化時傳入的參數,是在register函數裏處理的,調用了Layer的setPrefix方法,可以回看上文的register那一節;prefix方法也是調用了Layer的setPrefix方法,其實還是pathToRegExp幫忙處理的,代碼如下

Layer.prototype.setPrefix = function (prefix) {
  if (this.path) {
    this.path = prefix + this.path;
    this.paramNames = [];
    this.regexp = pathToRegExp(this.path, this.paramNames, this.opts);
  }

  return this;
};

然後我們來看看use方法

use

除了在嵌套路由的時候用到,use還可以爲路由添加類似中間層的處理,是比較重要的一個函數,但是也有一些坑,use的用法我所知道的有這麼幾種,

// 直接傳處理函數,只有當前這個router實例中的某個路徑匹配成功,纔會執行處理函數
router.use((ctx, next)=>{
  ...
})
// 傳路徑加處理函數,路徑可以是字符串或者是數組
router.use(['/users', '/id'], (ctx, next)=>{
  ...
})
// 傳一個其他router實例的中間件,注意這個例子最終的效果不是分別添加了/users和/id前綴,而是添加了/id/users前綴,還將中間件註冊了兩次
// 原因是use操作的是routes函數返回的結果,也就是dispatch函數,而dispatch和router實例是綁定的
router.use(['/users', '/id'], otherRouter.routes())

// 上面提到的問題也會導致下面這個例子和想象的結果不太一樣,會發現無法響應other路徑,查看middleware會發現/prefix/other路徑的中間件註冊了兩次
otherRouter.get('/other', (ctx, next)=>{
  ...
})
app.use(otherRouter.routes());

router.use('/prefix', otherRouter.routes());
app.use(router.routes());

use的代碼如下,上面例子裏關於dispatch和router綁定的問題有點難講清楚,大概和複雜類型的變量,賦值給了另一個變量後進行操作,發現原來的變量也變了差不多的意思

Router.prototype.use = function () {
  var router = this;
  var middleware = Array.prototype.slice.call(arguments);
  var path;

	// 處理第一個參數是路徑數組的情況,和register一樣如果是數組的話每個元素單獨調用一回
  // support array of paths
  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;
  }
	// 處理第一個參數是路徑的情況
  var hasPath = typeof middleware[0] === 'string';
  if (hasPath) {
    path = middleware.shift();
  }

  middleware.forEach(function (m) {
  	// 如果有router,其實就是已經調用過routes方法,如果沒有,就調用register方法
    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 {
    	// register的定義function (path, methods, middleware, opts),方便對比,methods傳的是空數組
      router.register(path || '(.*)', [], m, { end: false, ignoreCaptures: !hasPath });
    }
  });

  return this;
};

參數處理

在前面的代碼中我們已經看到過很多次param和params這樣的變量或者函數,這幾個變量和函數是爲了處理路徑裏的變量,比如下面例子裏的user,這種方式提供了可以抽離的參數校驗功能

router
.param('user', (id, ctx, next) => {
  ctx.user = users[id];
  if (!ctx.user) return ctx.status = 404;
  return next();
})
.get('/users/:user', ctx => {
  ctx.body = ctx.user;
})
.get('/users/:user/friends', ctx => {
  return ctx.user.getFriends().then(function(friends) {
    ctx.body = friends;
  });
})

param函數往params變量中添加處理函數,register和use函數中都循環調用了Layer的param函數,routes函數中調用了Layer實例的params方法,這一塊說實話還沒怎麼看明白,也比較細節,看到這裏基本上使用過程中發現什麼問題可以找得到是哪裏,所以就不深入了(😂),比較重要的應該只有Layer.prototype.param。

allowedMethods

這篇文章沒想到會寫這麼長,也沒想到koa-router有那麼多細節,感覺80%的代碼都貼上來了,這裏還想講一個函數,最後一個,allowedMethods,先看例子

router
.get('a', '/a', (ctx, next) => {
  console.log('a1');
  next();
}, (ctx, next) => {
  console.log('a2');
  next();
});

app.use(router.routes());
app.use(router.allowedMethods());

要注意的是,如果中間件的next鏈斷掉了,上面例子中的某一個next沒有寫,都會造成allowedMethods不會執行,因爲allowedMethods的實現裏有一句next().then(),另外的部分都還好理解,就不貼了

終於寫完了,下一篇應該會寫body吧,敬請期待

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