美文网首页webpack
koa && koa-router

koa && koa-router

作者: 百里哈哈 | 来源:发表于2020-09-18 11:18 被阅读0次

    一个简单的示例

    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

        本文链接:https://www.haomeiwen.com/subject/boadyktx.html