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;
};

你可能感兴趣的:(koa && koa-router)