Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

koa-router源码解读 #8

Open
dwqs opened this issue Jul 30, 2016 · 0 comments
Open

koa-router源码解读 #8

dwqs opened this issue Jul 30, 2016 · 0 comments

Comments

@dwqs
Copy link
Owner

dwqs commented Jul 30, 2016

接着 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());
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant