IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    koa-router源码解读

    dwqs发表于 2016-04-03 22:50:41
    love 0

    接着 koa源码解读 一文中的末尾接着唠嗑 koa-router。

    链式调用

    在 koa 中,对中间件的使用是支持链接调用的。同样,

    对于多个路径的请求,koa-router 也支持链式调用:

    router
      .get('/', function *(next) {
        this.body = 'Hello World!';
      })
      .post('/users', function *(next) {
        // ...
      })
      .put('/users/:id', function *(next) {
        // ...
      })
      .del('/users/:id', function *(next) {
        // ...
      });
    

    因为每个动词方法都会返回router本身:

    methods.forEach(function (method) {
      Router.prototype[method] = function (name, path, middleware) {
        var 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;
      };
    });
    

    路由实现

    Node 本身提供了数十个 HTTP 请求动词,koa-router 只是实现了部分常用的:

    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'
      ];
      //省略
    };
    

    这些请求动词的实现是通过第三方模块 methods 支持的,然后 koa-router 内部进行了注册处理:

    methods.forEach(function (method) {
      Router.prototype[method] = function (name, path, middleware) {
        //见上述代码
        this.register(path, [method], middleware, {
          name: name
        });
    
        return this;
      };
    });
    

    this.register 接受请求路径,方法,中间件作为参数,返回已经注册的路由:

    Router.prototype.register = function (path, methods, middleware, opts) {
      opts = opts || {};
    
      var stack = this.stack;
       // create route
      var route = new Layer(path, methods, middleware, {
        //Layer是具体实现,包括匹配、中间件处理等
        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 || "",
      });
       //other code
      return route;
    };
    

    由上述代码可知,koa-router 是支持中间件来处理路由的:

    myRouter.use(function* (next) {
        console.log('aaaaaa');
        yield next;
    });
    
    myRouter.use(function* (next) {
        console.log('bbbbbb');
        yield next;
    });
    
    myRouter.get('/', function *(next) {
        console.log('ccccccc');
        this.response.body = 'Hello World!';
    });
    
    myRouter.get('/test', function *(next) {
        console.log('dddddd');
        this.response.body = 'test router middleware';
    });
    

    通过 router.use 来注册中间件,中间件会按照顺序执行,并会在匹配的路由的回调之前调用:

    router middleware

    对于不匹配的路由则不会调用。同时,如果注册的路由少了 yield next, 则之后的中间件以及被匹配的路由的回调就不会被调用;路由的中间件也是支持链接调用的:

    Router.prototype.use = function () {
      var router = this;
      //other code
      return this;
    };
    

    中间件也支持特定路由和数组路由:

    // session middleware will run before authorize
    router
      .use(session())
      .use(authorize());
    
    // use middleware only with given path
    router.use('/users', userAuth());
    
    // or with an array of paths
    router.use(['/users', '/admin'], userAuth());
    

    从上述分析可知,对于同一个路由,能用多个中间件处理:

    router.get(
      '/users/:id',
      function (ctx, next) {
        return User.findOne(ctx.params.id).then(function(user) {
          ctx.user = user;
          return next();
        });
      },
      function (ctx) {
        console.log(ctx.user);
        // => { id: 17, name: "Alex" }
      }
    );
    

    这样的写法看起来会更紧凑。

    路由前缀

    Koa-router允许为路径统一添加前缀:

    var myRouter = new Router({
        prefix: '/koa'
    });
    
    // 等同于"/koa"
    myRouter.get('/', function* () {
        this.response.body = 'koa router';
    });
    
    // 等同于"/koa/:id"
    myRouter.get('/:id', function* () {
        this.response.body = 'koa router-1';
    });
    

    也可以在路由初始化后设置统一的前缀,koa-router 提供了 prefix 方法:

    Router.prototype.prefix = function (prefix) {
      prefix = prefix.replace(/\/$/, '');
    
      this.opts.prefix = prefix;
    
      this.stack.forEach(function (route) {
        route.setPrefix(prefix);
      });
    
      return this;
    };
    

    所以,以下代码是和上述等价的:

    var myRouter = new Router();
    myRouter.prefix('/koa');
    
    // 等同于"/koa"
    myRouter.get('/', function* () {
        this.response.body = 'koa router';
    });
    
    // 等同于"/koa/:id"
    myRouter.get('/:id', function* () {
        this.response.body = 'koa router-1';
    });
    

    参数处理和重定向

    路径的参数通过 this.params 属性获取,该属性返回一个对象,所有路径参数都是该对象的成员:

    // 访问 /programming/how-to-node
    router.get('/:category/:title', function *(next) {
      console.log(this.params);
      // => { category: 'programming', title: 'how-to-node' }
    });
    

    param 方法可以对参数设置条件,可用于常规验证和自动加载的验证:

    router
      .get('/users/:user', function *(next) {
        this.body = this.user;
      })
      .param('user', function *(id, next) {
        var users = [ '0号用户', '1号用户', '2号用户'];
        this.user = users[id];
        if (!this.user) return this.status = 404;
        yield next;
      })
    

    param 接受两个参数:路由的参数和处理参数的中间件:

    Router.prototype.param = function (param, middleware) {
      this.params[param] = middleware;
      this.stack.forEach(function (route) {
        route.param(param, middleware);
      });
      return this;
    };
    

    如果 /users/:user 的参数 user 对应的不是有效用户(比如访问 /users/3),param 方法注册的中间件会查到,就会返回404错误。

    也可以将参数验证不通过的路由通过 redirect 重定向到另一个路径,并返回301状态码:

    router.redirect('/login', 'sign-in');
    
    // 等同于
    router.all('/login', function *() {
      this.redirect('/sign-in');
      this.status = 301;
    });
    

    all 是一个私有方法,会处理某路由的所有的动词请求,相当于一个中间件。如果在 all 之前或者之后出现了处理同一个路由的动词方法,则要调用 yield next,否则另一个就不会执行:

    myRouter.get('/login',function* (next) {
        this.body = 'login';
        // 没有yield next,all不会执行
        yield next;
    }).get('/sign',function* () {
        this.body = 'sign';
    }).all('/login',function* () {
        console.log('login');
    });
    
    myRouter.get('/sign2',function* () {
        this.body = 'sign';
    }).all('/login2',function* () {
        console.log('login2');
        //没有yield next,get不会执行
        yield next;
    }).get('/login2',function* (next) {
        this.body = 'login';
    });
    

    redirect 方法的第一个参数是请求来源,第二个参数是目的地,两者都可以用路径模式的别名代替,还有第三个参数是状态码,默认是 301:

    Router.prototype.redirect = function (source, destination, code) {
      // lookup source route by name
      if (source[0] !== '/') {
        source = this.url(source);
      }
    
      // lookup destination route by name
      if (destination[0] !== '/') {
        destination = this.url(destination);
      }
    
      return this.all(source, function *() {
        this.redirect(destination);
        this.status = code || 301;
      });
    };
    

    命名路由和嵌套路由

    对于非常复杂的路由,koa-router 支持给复杂的路径模式起别名。别名作为第一个参数传递给动词方法:

    router.get('user', '/users/:id', function *(next) {
     // ...
    });
    

    然后可以通过 url 实例方法来生成路由:

    router.url('user', 3);
    // => "/users/3"
    
    //等价于
    router.url('user', { id: 3 });
    //=> 'users/3'
    

    该方法接收两个参数:路由别名和参数对象:

    Router.prototype.url = function (name, params) {
      var route = this.route(name);
    
      if (route) {
        var args = Array.prototype.slice.call(arguments, 1);
        return route.url.apply(route, args);
      }
    
      return new Error("No route found for name: " + name);
    };
    

    第一个参数用于 route 方式查找匹配的别名,找到则返回 true,否则返回 false:

    Router.prototype.route = function (name) {
      var routes = this.stack;  //路由别名
    
      for (var len = routes.length, i=0; i<len; i++) {
        if (routes[i].name && routes[i].name === name) {
          return routes[i];
        }
      }
    
      return false;
    };
    

    除了实例方法 url 外,koa-router 还提供一个静态的方法 url 生成路由:

    var url = Router.url('/users/:id', {id: 1});
    // => "/users/1"
    

    第一个参数是路径模式,第二个参数是参数对象。

    除了给路由命名,koa-router 还支持路由嵌套处理:

    var forums = new Router();
    var posts = new Router();
    
    posts.get('/', function (ctx, next) {...});
    posts.get('/:pid', function (ctx, next) {...});
    forums.use('/forums/:fid/posts', posts.routes(), posts.allowedMethods());
    
    // responds to "/forums/123/posts" and "/forums/123/posts/123"
    app.use(forums.routes());
    

    转载请注明:淡忘~浅思 » koa-router源码解读



沪ICP备19023445号-2号
友情链接