koa-router源碼學習

koa-router源碼學習

基本用法

const koa = require('koa');
const Router = require('koa-router');

const app = new koa();
const router = new Router();
app.use(router.routes()).use(router.allowedMethods());
app.listen(8080);


router.get( '/user', (ctx, next) => {
  ctx.body = 'hello word';
  next();
});

上面是koa-router的基本用法,創建了一個簡單的路由。
如果不使用koa-router的話,需要自己來判斷路徑,如下所示,可以來判斷url,然後調用響應的方法

const koa = require('koa');
const app = new koa();
app.listen(8080);

app.use(((context, next) => {
  const path = context.path;
  switch (path) {
    case '/':  // do some things;break;
    case '/demo':  // do some things;break;
  }
}));

當然這種方式代碼複雜,不易維護,可以使用koa-router來簡化代碼。

源碼學習

在koa-router中,有兩個很重要的對象,幾個重要的方法

對象:

  • Router對象:對外拋出的路由對象,封裝了路由的方法和屬性,提供給開發者使用
  • Layer對象:Layer對象未對外拋出,是koa-router內部的一個單位路由層的對象

這裏我對Layer進行了自定義命名:單位路由層,指的是由method,path,middleware等組成的一個路由層,而整個路由是由多個路由層組成的。例如

router.get( '/user', (ctx, next) => {
  ctx.body = 'hello word';
  next();
});
// 它就是一個路由層,是一種匹配模式,當path,method匹配的時候,調用指定的middleware

方法:

ps:router指的是Router的實例

  • methods.forEach:將http的請求方法掛載到Router.prototype上,讓router能使用對應的http方法來生成單位路由層
  • register:根據path, methods, middleware等生成layer實例,存儲在router的stack屬性中
  • routes:Router的主方法,返回一個koa中間件函數,在此中間件內調用其他函數,完成路由匹配,並完成路由鏈的調用,也可以使用middleware方法完成該操作
  • match:Router的核心方法,用於判斷當前請求和路由是否匹配
  • compose:koa-compsoe模塊的方法,在koa源碼學習中遇到過,是koa中間件的核心函數

上面介紹了koa-router的主要對象和方法,下面我們通過創建一個簡單的路由和一次HTTP請求,來了解Router內部的函數調用及實現細節

引入

const Router=require('koa-router');

在加載koa-router模塊的時候,導出了Router對象,並執行了methods.forEach函數
該函數的主要作用是,從將從http模塊取得的方法掛載在Router.prototype上
調用methods.forEach

methods.forEach(function (method) {
 // 這裏是掛載請求方法,常用的就 get,post,put,delete,options等
  Router.prototype[method] = function (name, path, middleware) { 
    var middleware;
	
	// 這裏主要是判斷參數的個數,讓path和middleware取得正確的值
    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;
    }
	// 調用Router實例方法register
    this.register(path, [method], middleware, {
      name: name
    });

    return this;
  };
});

調用Router.prototype.register方法
實例化layer實例,並將實例存儲router.stack中

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.forEach(function (p) {
      router.register.call(router, p, methods, middleware, opts);
    });

    return this;
  }

  // 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) {
    route.setPrefix(this.opts.prefix);
  }

  // add parameter middleware
  Object.keys(this.params).forEach(function (param) {
    route.param(param, this.params[param]);
  }, this);

  stack.push(route);

  return route;
};

調用Router.prototype.routes方法

示例代碼
app.use(router.routes());
router.get( '/user', (ctx, next) => {
  ctx.body = 'hello word';
  next();
});

該方法返回一箇中間件函數
該函數主要是調用match函數,取得url路徑匹配結果

var dispatch = 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); // 在此調用match函數
    var layerChain, layer, i;
	
	// 多次調用的時候,將match.path合併爲一個數組
    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;
    }
	
	// 將所有符合的layer組合起來
    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); // 通過compose組成中間件鏈,然後調用
  };

調用Router.prototype.match函數

Router.prototype.match = function (path, method) {
  var layers = this.stack;
  var layer;
  var matched = {
    path: [],
    pathAndMethod: [],
    route: false
  };
	// 將所有的單位路由曾循環,判斷是否滿足當前url
  for (var len = layers.length, i = 0; i < len; i++) {
    layer = layers[i];

    debug('test %s %s', layer.path, layer.regexp);
	// 當正則匹配成功後,則將當前layer加入match.path數組中
    if (layer.match(path)) {
      matched.path.push(layer);

      if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
        matched.pathAndMethod.push(layer);
        if (layer.methods.length) matched.route = true;
      }
    }
  }

  return matched;
};

這裏值得注意的是layer.match方法,如上示例代碼生成的正則爲:

 /^\/hello(?:\/(?=$))?$/i
 // ?:爲非捕獲性分組
 // ?=爲正則前瞻
 (?:\/(?=$))?$ 代表的意思是:以/結束,出現0次或1次
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章