koa && koa-router

一个简单的示例

const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
    ctx.body = 'hello world'
})

app.listen(3000);

koa是对node请求响应进行封装, 从这个示例中我们不难猜出通过listen方法进行http.createServer的调用。
koa的是基于中间件的思想, 在调用use的时候会将一系列的中间件进行存储,启用server的时候回调经过中间件包装的函数。

其主要的几个代码如下

new 实例

new的时候进行一些属性的设置, 包括context、request、response等。

constructor(options) {
    super();
    options = options || {};
    this.proxy = options.proxy || false;
    this.subdomainOffset = options.subdomainOffset || 2;
    this.proxyIpHeader = options.proxyIpHeader || 'X-Forwarded-For';
    this.maxIpsCount = options.maxIpsCount || 0;
    this.env = options.env || process.env.NODE_ENV || 'development';
    if (options.keys) this.keys = options.keys;
    this.middleware = [];
    this.context = Object.create(context);
    this.request = Object.create(request);
    this.response = Object.create(response);
    // util.inspect.custom support for node 6+
    /* istanbul ignore else */
    if (util.inspect.custom) {
      this[util.inspect.custom] = this.inspect;
    }
  }

use

进行中间件的挂载

use(fn) {
    this.middleware.push(fn);
    return this;
  }

listen

调用http.createServer时会对回调函数进行封装, 通过compose将中间件包装成高阶函数, 最后在this.handleRequest中进行调用。
respond将返回的结果进行处理,最终返回至客户端。

listen(...args) {
    debug('listen');
    const server = http.createServer(this.callback());
    return server.listen(...args);
  }
  callback() {
    const fn = compose(this.middleware);

    if (!this.listenerCount('error')) this.on('error', this.onerror);

    const handleRequest = (req, res) => {
      const ctx = this.createContext(req, res);
      return this.handleRequest(ctx, fn);
    };

    return handleRequest;
  }

  handleRequest(ctx, fnMiddleware) {
    const res = ctx.res;
    res.statusCode = 404;
    const onerror = err => ctx.onerror(err);
    const handleResponse = () => respond(ctx);
    onFinished(res, onerror);
    return fnMiddleware(ctx).then(handleResponse).catch(onerror);
  }
  
  function respond(ctx) {
    // allow bypassing koa
    if (false === ctx.respond) return;

    if (!ctx.writable) return;

    const res = ctx.res;
    let body = ctx.body;
    const code = ctx.status;
    ...
    res.end(body);
 }

compose

compose返回的是个高阶函数, 在函数内设置dispatch方法的返回值为promise对象, fn的调用参数为ctx和 next。
通过 ctx获取上下文信息, 通过next的调用可触发下一个中间件(其过程为通常说的剥洋葱效果)。

function compose (middleware) {
  return function (context, next) {
    // last called middleware #
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

koa-router

示例

const parentRouter = new Router();
const nestedRouter = new Router();

nestedRouter
    .get(/^\/\w$/i, function (ctx, next) {
         return next();
    })
    .get('/first-nested-route', function (ctx, next) {
        return next();
    })
    .get('/second-nested-route', function (ctx, next) {
        ctx.body = 'hello sub';
        return next();
    });

parentRouter.use('/parent-route', function (ctx, next) {
    return next();
}, nestedRouter.routes());

app.use(parentRouter.routes());

app.listen(3000);

如果在不用router插件的话, 对于不同的URL走不同的分支,我们需要自己去做判断解决这些问题。
koa-router很好的帮我们解决了路由匹配的问题。

new 一个router

new 一个router的时候会对一些属性做初始化或者赋值操作。 其中params用来保存参数信息, stack则用来存储layer信息。

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 = {};
  this.stack = [];
};

get过程

该方法主要是获取中间件, 然后调用register方法。

Router.prototype[method] = function(name, 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;
      }

      this.register(path, [method], middleware, {
        name: name
      });

      return this;
    };

register

从该方法可以看出每次对path设置get、post等方法都会new一个Layer作为rout对象返回, 最终把route放入stack数组中。
layer用来存储单个路由信息,包括URL、 params、生成正则匹配regexp以及match方法。
从代码上还可以看出如果path为数组则对该数组进行递归的注册。

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

  const router = this;
  const stack = this.stack;

  // support array of paths
  if (Array.isArray(path)) {
    for (let i = 0; i < path.length; i++) {
      const curPath = path[i];
      router.register.call(router, curPath, methods, middleware, opts);
    }

    return this;
  }

  // create route
  const route = new Layer(path, methods, middleware, {...});

  if (this.opts.prefix) {
    route.setPrefix(this.opts.prefix);
  }

  // add parameter middleware
  for (let i = 0; i < Object.keys(this.params).length; i++) {
    const param = Object.keys(this.params)[i];
    route.param(param, this.params[param]);
  }

  stack.push(route);

  debug('defined route %s %s', route.methods, route.path);

  return route;
};

中间件注册过程

在koa中通过use进行中间件的注册, 而use中注册的是一个包括ctx、next的方法。 从app.use(parentRouter.routes());中我们不难推测调用routes方法即返回这样一个对象。

routes调用

通过该方法的调用返回一个带有ctx、next参数的方法。 dispatch的主要功能点:

  1. 获取当前的path, 通过router.match获取匹配后的信息。
  2. 获取所以匹配到的layer, 依次遍历将其内容封装成中间件,push到数组中。
  3. 通过compose方法将已封装的中间件数组进行粘合, 然后调用。 即实现路由功能。
Router.prototype.routes = Router.prototype.middleware = function () {
  const router = this;

  let dispatch = function dispatch(ctx, next) {
    debug('%s %s', ctx.method, ctx.path);

    const path = router.opts.routerPath || ctx.routerPath || ctx.path;
    const matched = router.match(path, ctx.method);
    let layerChain;

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

    ctx.router = router;

    if (!matched.route) return next();

    const matchedLayers = matched.pathAndMethod
    const mostSpecificLayer = matchedLayers[matchedLayers.length - 1]
    ctx._matchedRoute = mostSpecificLayer.path;
    if (mostSpecificLayer.name) {
      ctx._matchedRouteName = mostSpecificLayer.name;
    }

    layerChain = matchedLayers.reduce(function(memo, layer) {
      memo.push(function(ctx, next) {
        ctx.captures = layer.captures(path, ctx.captures);
        ctx.params = ctx.request.params = layer.params(path, ctx.captures, ctx.params);
        ctx.routerPath = layer.path;
        ctx.routerName = layer.name;
        ctx._matchedRoute = layer.path;
        if (layer.name) {
          ctx._matchedRouteName = layer.name;
        }
        return next();
      });
      return memo.concat(layer.stack);
    }, []);

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

  dispatch.router = this;

  return dispatch;
};

router.use

在示例中我们还看到,在父路由上通过use方法可以注册子路由,从而实现多级路由功能。
接下来我们可以大致看一下use的过程。

在use中可以传递多个middleare参数。

  1. 当middleare为数组时,进行递归调用
  2. 当middleare上包含router属性时, 说明是通过router.routes()返回的,此时为一个子路由的注册。
    针对这种情况,首先对子路由的信息进行copy, 对其layer信息进行copy,然后将copy后的layer信息push的父路由的stack中;
  3. 如果是单个middleare 中间件方法时, 则直接执行注册。
Router.prototype.use = function () {
  const router = this;
  const middleware = Array.prototype.slice.call(arguments);
  let path;

  // support array of paths
  if (Array.isArray(middleware[0]) && typeof middleware[0][0] === 'string') {
    let arrPaths = middleware[0];
    for (let i = 0; i < arrPaths.length; i++) {
      const p = arrPaths[i];
      router.use.apply(router, [p].concat(middleware.slice(1)));
    }

    return this;
  }

  const hasPath = typeof middleware[0] === 'string';
  if (hasPath) path = middleware.shift();

  for (let i = 0; i < middleware.length; i++) {
    const m = middleware[i];
    if (m.router) { // 子路由判断
      const cloneRouter = Object.assign(Object.create(Router.prototype), m.router, {
        stack: m.router.stack.slice(0)
      });

      for (let j = 0; j < cloneRouter.stack.length; j++) {
        const nestedLayer = cloneRouter.stack[j];
        const cloneLayer = Object.assign(
          Object.create(Layer.prototype),
          nestedLayer
        );

        if (path) cloneLayer.setPrefix(path);
        if (router.opts.prefix) cloneLayer.setPrefix(router.opts.prefix);
        // 将克隆后的layer push到当前路由的stack中
        router.stack.push(cloneLayer);
        cloneRouter.stack[j] = cloneLayer;
      }

      if (router.params) {
        function setRouterParams(paramArr) {
          const routerParams = paramArr;
          for (let j = 0; j < routerParams.length; j++) {
            const key = routerParams[j];
            cloneRouter.param(key, router.params[key]);
          }
        }
        setRouterParams(Object.keys(router.params));
      }
    } else {
      const keys = [];
      pathToRegexp(router.opts.prefix || '', keys);
      const routerPrefixHasParam = router.opts.prefix && keys.length;
      router.register(path || '([^\/]*)', [], m, { end: false, ignoreCaptures: !hasPath && !routerPrefixHasParam });
    }
  }

  return this;
};
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,928评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,192评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,468评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,186评论 1 286
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,295评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,374评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,403评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,186评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,610评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,906评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,075评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,755评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,393评论 3 320
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,079评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,313评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,934评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,963评论 2 351