首先简单的介绍下什么koa-router,为什么要使用它,可以简单看下上一篇文章. 了解koa-router

首先我们来看下koa-router的源码的基本结构如下,它是由两部分组成的:

  1. ------- koa-router
  2. | |--- lib
  3. | | |-- layer.js
  4. | | |-- router.js

如上基本结构。

一:router.js 代码基本结构

我们先看 router.js 代码结构如下:

  1. module.exports = Router;
  2.  
  3. function Router(opts) {
  4. if (!(this instanceof Router)) {
  5. return new Router(opts);
  6. }
  7.  
  8. this.opts = opts || {};
  9. this.methods = this.opts.methods || [
  10. 'HEAD',
  11. 'OPTIONS',
  12. 'GET',
  13. 'PUT',
  14. 'PATCH',
  15. 'POST',
  16. 'DELETE'
  17. ];
  18.  
  19. this.params = {};
  20. this.stack = [];
  21. };
  22.  
  23. Router.prototype.del = Router.prototype['delete'];
  24. Router.prototype.use = function () {
  25. // ....
  26. }
  27. Router.prototype.prefix = function (prefix) {
  28. // ....
  29. }
  30. Router.prototype.routes = Router.prototype.middleware = function () {
  31. // ...
  32. }
  33. Router.prototype.allowedMethods = function (options) {
  34. // ...
  35. }
  36. Router.prototype.all = function (name, path, middleware) {
  37. // ...
  38. }
  39. Router.prototype.redirect = function (source, destination, code) {
  40. // ...
  41. }
  42. Router.prototype.register = function (path, methods, middleware, opts) {
  43. // ...
  44. }
  45. Router.prototype.route = function (name) {
  46. // ...
  47. }
  48. Router.prototype.url = function (name, params) {
  49. // ...
  50. }
  51. Router.prototype.match = function (path, method) {
  52. // ...
  53. }
  54. Router.prototype.param = function (param, middleware) {
  55. // ...
  56. }
  57. Router.url = function (path, params) {
  58. // ...
  59. }

如上就是koa-router中的router.js 中的代码结构,定义了一个 Router 函数,然后在该函数的原型上定义了很多方法。然后使用 module.exports = Router; 导出该函数。因此如果我们要使用该router函数的话,需要首先导入该router.js 代码,因此需要 var Router = require('koa-router'); 然后再实例化该router函数,如代码:var router = new Router(); 或者我们直接可以如下编写代码:var router = require('koa-router')(); 比如如下koa-router代码的demo列子:

  1. const Koa = require('koa');
  2. const app = new Koa();
  3.  
  4. const router = require('koa-router')();
  5.  
  6. // 添加路由
  7. router.get('/', ctx => {
  8. ctx.body = '<h1>欢迎光临index page 页面</h1>';
  9. });
  10.  
  11. router.get('/home', ctx => {
  12. ctx.body = '<h1>欢迎光临home页面</h1>';
  13. });
  14.  
  15. // 加载路由中间件
  16. app.use(router.routes());
  17.  
  18. app.use(router.allowedMethods());
  19.  
  20. app.listen(3001, () => {
  21. console.log('server is running at http://localhost:3001');
  22. });

当我们运行该js文件的时候,在浏览器访问 http://localhost:3001/ 的时候,就会显示 "欢迎光临index page 页面" 这些信息,当我们在浏览器访问 http://localhost:3001/home 的时候,在页面上会显示 "欢迎光临home页面" 等信息。它是如何调用的呢?

首先我们来分析下,Router这个构造函数代码;基本源码如下:

  1. function Router(opts) {
  2. if (!(this instanceof Router)) {
  3. return new Router(opts);
  4. }
  5.  
  6. this.opts = opts || {};
  7. this.methods = this.opts.methods || [
  8. 'HEAD',
  9. 'OPTIONS',
  10. 'GET',
  11. 'PUT',
  12. 'PATCH',
  13. 'POST',
  14. 'DELETE'
  15. ];
  16.  
  17. this.params = {};
  18. this.stack = [];
  19. };

如上代码,首先会判断 是否是该Router实列,如果不是的话,就实例化该对象。因此我们 Router() 或 new Router() 这样调用效果是一致的。该Router函数会传入一个对象参数 opts,该对象 opts会有methods这样的key。会传入一些http方法等。

然后 this.methods; 它是一个数组是存放允许使用的HTTP的常用的方法名,后面会使用到,先保存到 this.methods里面。

this.params = {}; 定义了一个对象,它具体干什么用的,我暂时也不知道,先放在这里。
this.stack = []; 定义了一个数组,先放在这里,我也不知道具体干什么用的。

二. 路由注册

第二步我们就是添加我们的路由,比如app.js代码如下:

  1. // 添加路由
  2. router.get('/', ctx => {
  3. ctx.body = '<h1>欢迎光临index page 页面</h1>';
  4. });

在我们的router.js源码中会有这么一段代码,我们来看下:

var methods = require('methods');

methods函数源码如下:

  1. /*!
  2. * methods
  3. * Copyright(c) 2013-2014 TJ Holowaychuk
  4. * Copyright(c) 2015-2016 Douglas Christopher Wilson
  5. * MIT Licensed
  6. */
  7.  
  8. 'use strict';
  9. /**
  10. * Module dependencies.
  11. * @private
  12. */
  13.  
  14. var http = require('http');
  15. /**
  16. * Module exports.
  17. * @public
  18. */
  19.  
  20. module.exports = getCurrentNodeMethods() || getBasicNodeMethods();
  21. /**
  22. * Get the current Node.js methods.
  23. * @private
  24. */
  25.  
  26. function getCurrentNodeMethods() {
  27. return http.METHODS && http.METHODS.map(function lowerCaseMethod(method) {
  28. return method.toLowerCase();
  29. });
  30. }
  31.  
  32. /**
  33. * Get the "basic" Node.js methods, a snapshot from Node.js 0.10.
  34. * @private
  35. */
  36.  
  37. function getBasicNodeMethods() {
  38. return [
  39. 'get',
  40. 'post',
  41. 'put',
  42. 'head',
  43. 'delete',
  44. 'options',
  45. 'trace',
  46. 'copy',
  47. 'lock',
  48. 'mkcol',
  49. 'move',
  50. 'purge',
  51. 'propfind',
  52. 'proppatch',
  53. 'unlock',
  54. 'report',
  55. 'mkactivity',
  56. 'checkout',
  57. 'merge',
  58. 'm-search',
  59. 'notify',
  60. 'subscribe',
  61. 'unsubscribe',
  62. 'patch',
  63. 'search',
  64. 'connect'
  65. ];
  66. }

因此在我们的 router.js 中 这样引入 var methods = require('methods');后,我们的methods的值被保存为如下:

  1. var methods = [
  2. 'get',
  3. 'post',
  4. 'put',
  5. 'head',
  6. 'delete',
  7. 'options',
  8. 'trace',
  9. 'copy',
  10. 'lock',
  11. 'mkcol',
  12. 'move',
  13. 'purge',
  14. 'propfind',
  15. 'proppatch',
  16. 'unlock',
  17. 'report',
  18. 'mkactivity',
  19. 'checkout',
  20. 'merge',
  21. 'm-search',
  22. 'notify',
  23. 'subscribe',
  24. 'unsubscribe',
  25. 'patch',
  26. 'search',
  27. 'connect'
  28. ];

然后router.js 代码中的源码由如下代码:

  1. methods.forEach(function (method) {
  2. Router.prototype[method] = function (name, path, middleware) {
  3. var middleware;
  4.  
  5. if (typeof path === 'string' || path instanceof RegExp) {
  6. middleware = Array.prototype.slice.call(arguments, 2);
  7. } else {
  8. middleware = Array.prototype.slice.call(arguments, 1);
  9. path = name;
  10. name = null;
  11. }
  12.  
  13. this.register(path, [method], middleware, {
  14. name: name
  15. });
  16.  
  17. return this;
  18. };
  19. });

也就是说遍历 methods 上面的数组中保存的 get/post/... 等方法。最后就变成如下这样的:

  1. Router.property['get'] = function(name, path, middleware) {};
  2. Router.property['post'] = function(name, path, middleware) {};
  3. Router.property['put'] = function(name, path, middleware) {};
  4. Router.property['head'] = function(name, path, middleware) {};

..... 等等这样的函数。通过如上代码,我们再来看下我们的app.js中的这句代码就可以理解了:

  1. router.get('/', ctx => {
  2. ctx.body = '<h1>欢迎光临index page 页面</h1>';
  3. });
  4.  
  5. router.get('/home', ctx => {
  6. ctx.body = '<h1>欢迎光临home页面</h1>';
  7. });

如上代码 router 是 Router的实列返回的对象,Router的原型对象上会有 get, post,put .... 等等这样的方法,因此我们可以使用 router.get('/', ctx => {}). 这样添加一个或多个路由了。

其中我们的 Router的原型上(property)的get/post 等方法会有三个参数,第一个参数name,我们可以理解为字符串,它可以理解为路由的路径(比如上面的 '/', 或 '/home') 这样的。第二个参数 path 就是我们的函数了。该函数返回了 ctx对象。我们可以做个简单的打印,如下方法内部:

  1. Router.prototype[method] = function (name, path, middleware) {
  2. console.log(name);
  3. console.log(path);
  4. console.log('-----');
  5. console.log(middleware);
  6. console.log(1111111);
  7. }

当我们 node app.js 重新执行的时候,在node命令行中,可以看到如下打印信息:

可以看到,我们 router.get('/', ctx => {}) 添加路由的时候,console.log(name); 打印的是 '/'; console.log(path); 打印的是 [Function], console.log(middleware); 打印的就是 undefined了。当我们添加 home路由的时候 router.get('/home', ctx => {}), console.log(name) 打印的是 '/home'了,console.log(path); 打印的是 [Function], console.log(middleware); 打印的也是 undefined了。

如上分析我们可以看到 Router中的各个方法已经可以理解添加路由了,下面我们继续看下该方法的内部代码是如何判断的?代码如下:

  1. Router.prototype[method] = function (name, path, middleware) {
  2. var middleware;
  3.  
  4. if (typeof path === 'string' || path instanceof RegExp) {
  5. middleware = Array.prototype.slice.call(arguments, 2);
  6. } else {
  7. middleware = Array.prototype.slice.call(arguments, 1);
  8. path = name;
  9. name = null;
  10. }
  11.  
  12. this.register(path, [method], middleware, {
  13. name: name
  14. });
  15.  
  16. return this;
  17. };

如上代码,其实添加路由还有一种方式,如下代码:

  1. router.get('user', '/users/:id', (ctx, next) => {
  2. ctx.body = 'hello world';
  3. });
  4. const r = router.url('user', 3);
  5. console.log(r); // 生成路由 /users/3

按照官网的解释是:路由也可以有names(名字),router.url 方法方便我们在代码中根据路由名称和参数(可选)去生成具体的 URL。可能在开发环境中会使用到。因此会有如上两种情况。两个参数或三个参数的情况。

Router.prototype.register

因此 如上代码if判断,if (typeof path === 'string' || path instanceof RegExp) {} 如果path是一个字符串,或者是一个正则表达式的实列的话,就从第二个参数截取,也就是说从第二个参数后,或者说把第三个参数赋值给 middleware 这个参数。否则的话,如果path它是一个函数的话,那么就从第一个参数去截取,也就是说把第二个参数赋值给 middleware 这个变量。然后 path = name; name = null; 最后我们会调用 register 方法去注册路由;下面我们来看看下 register 方法的代码如下:

  1. /*
  2. * 该方法有四个参数
  3. * @param {String} path 路由的路径
  4. * @param {String} methods 'get、post、put、'等对应的方法
  5. * @param {Function} middleware 该参数是一个函数。会返回ctx对象。
  6. * @param {opts} {name: name} 如果只有两个参数,该name值为null。否则就有值。
  7. */
  8. Router.prototype.register = function (path, methods, middleware, opts) {
  9. opts = opts || {};
  10.  
  11. var router = this;
  12. var stack = this.stack;
  13.  
  14. // support array of paths
  15. if (Array.isArray(path)) {
  16. path.forEach(function (p) {
  17. router.register.call(router, p, methods, middleware, opts);
  18. });
  19.  
  20. return this;
  21. }
  22.  
  23. // create route
  24. var route = new Layer(path, methods, middleware, {
  25. end: opts.end === false ? opts.end : true,
  26. name: opts.name,
  27. sensitive: opts.sensitive || this.opts.sensitive || false,
  28. strict: opts.strict || this.opts.strict || false,
  29. prefix: opts.prefix || this.opts.prefix || "",
  30. ignoreCaptures: opts.ignoreCaptures
  31. });
  32.  
  33. if (this.opts.prefix) {
  34. route.setPrefix(this.opts.prefix);
  35. }
  36.  
  37. // add parameter middleware
  38. Object.keys(this.params).forEach(function (param) {
  39. route.param(param, this.params[param]);
  40. }, this);
  41.  
  42. stack.push(route);
  43.  
  44. return route;
  45. };

Router.prototype.register 该方法是注册路由的核心函数。该方法直接挂载在Router的原型上,因此我们在router的实列上也可以访问到该方法。因此在使用实列我们之前是这样注册路由的:

  1. router.get('/home', ctx => {
  2. ctx.body = '<h1>欢迎光临home页面</h1>';
  3. });

其实上面的代码相当于如下代码:

  1. router.register('/home', ['GET'], [(ctx, next) => {}], {name: null});

这样的代码。我们可以从上面的代码传进来的参数可以理解成如上的代码了。

我们继续看如上源码,首先会判断path是否是一个数组,如果是一个数组的话,会使用递归的方式依次调用router.register 这个方法。最后返回this对象。因此如下代码也是可以的:

  1. router.get(['/home', '/xxx', '/yyy'], ctx => {
  2. ctx.body = '<h1>欢迎光临home页面</h1>';
  3. });

如上 path路径是一个数组也是支持的,http://localhost:3001/home, http://localhost:3001/xxx, http://localhost:3001/yyy 访问的都会返回 "欢迎光临home页面" 页面的显示。如下所示:

代码继续往后看,实例化Layer函数,该函数我们晚点再来分析,我们继续往下看代码:如下所示:

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

  1. 理解Router.prototype.prefix

会判断opts对象是否有 prefix 的前缀的key,如果有的话就会调用 route.setPrefix() 方法。我们先来看看prefix的作用是什么。基本源代码如下:

  1. Router.prototype.prefix = function (prefix) {
  2. prefix = prefix.replace(/\/$/, '');
  3.  
  4. this.opts.prefix = prefix;
  5.  
  6. this.stack.forEach(function (route) {
  7. route.setPrefix(prefix);
  8. });
  9. return this;
  10. };

该prefix的作用就是给路由全局加前缀的含义;比如app.js改成如下代码:

  1. const Koa = require('koa');
  2. const app = new Koa();
  3.  
  4. const router = require('koa-router')({
  5. prefix: '/prefix'
  6. });
  7. // 添加路由
  8. router.get('/', ctx => {
  9. ctx.body = '<h1>欢迎光临index page 页面</h1>';
  10. });
  11.  
  12. router.get('/home', ctx => {
  13. ctx.body = '<h1>欢迎光临home页面</h1>';
  14. });
  15.  
  16. router.get('user', '/users/:id', (ctx, next) => {
  17. ctx.body = 'hello world';
  18. });
  19. const r = router.url('user', 3);
  20. console.log(r); // 生成路由 /users/3
  21.  
  22. // 加载路由中间件
  23. app.use(router.routes());
  24.  
  25. app.use(router.allowedMethods());
  26.  
  27. app.listen(3001, () => {
  28. console.log('server is running at http://localhost:3001');
  29. });

现在当我们继续访问 http://localhost:3001/home 或 http://localhost:3001/ 的时候,页面是访问不到的,如果我们加上前缀 '/prefix' 是可以访问的到的,如 http://localhost:3001/prefix/home 或 http://localhost:3001/prefix。其中代码 route.setPrefix(this.opts.prefix);中的setPrefix的方法中的route就是new Layer的实列对象了,因此setPrefix的方法就是在Layer.js 里面,setPrefix方法如下所示:

Layer.prototype.setPrefix

  1. Layer.prototype.setPrefix = function (prefix) {
  2. if (this.path) {
  3. this.path = prefix + this.path;
  4. this.paramNames = [];
  5. this.regexp = pathToRegExp(this.path, this.paramNames, this.opts);
  6. }
  7.  
  8. return this;
  9. };

如上代码可以看到,如果有路由路径的话,this.path = prefix + this.path; 因此路由路径发生改变了。然后把路径 使用 pathToRegExp 转换成正则表达式保存到 this.regexp 中。最后返回Layer对象。

想要了解 pathToRegExp,可以看我之前的一篇文章,了解pathToRegExp

下面我们来看下Layer.js 代码的结构如下:

  1. var debug = require('debug')('koa-router');
  2. var pathToRegExp = require('path-to-regexp');
  3. var uri = require('urijs');
  4.  
  5. module.exports = Layer;
  6.  
  7. function Layer(path, methods, middleware, opts) {
  8.  
  9. };
  10.  
  11. Layer.prototype.match = function (path) {
  12. // ...
  13. }
  14.  
  15. Layer.prototype.params = function (path, captures, existingParams) {
  16. // ...
  17. }
  18.  
  19. Layer.prototype.captures = function (path) {
  20. // ...
  21. }
  22.  
  23. Layer.prototype.url = function (params, options) {
  24. // ...
  25. }
  26.  
  27. Layer.prototype.param = function (param, fn) {
  28. // ...
  29. }
  30.  
  31. Layer.prototype.setPrefix = function (prefix) {
  32. // ...
  33. }

Layer.js 如上代码结构晚点再折腾,我们还是回到 router.js中的register函数代码上了;

  1. /*
  2. * 该方法有四个参数
  3. * @param {String} path 路由的路径
  4. * @param {String} methods 'get、post、put、'等对应的方法
  5. * @param {Function} middleware 该参数是一个函数。会返回ctx对象。
  6. * @param {opts} {name: name} 如果只有两个参数,该name值为null。否则就有值。
  7. */
  8. Router.prototype.register = function (path, methods, middleware, opts) {
  9. // create route
  10. var route = new Layer(path, methods, middleware, {
  11. end: opts.end === false ? opts.end : true,
  12. name: opts.name,
  13. sensitive: opts.sensitive || this.opts.sensitive || false,
  14. strict: opts.strict || this.opts.strict || false,
  15. prefix: opts.prefix || this.opts.prefix || "",
  16. ignoreCaptures: opts.ignoreCaptures
  17. });
  18. }

在该函数内部会引用 Layer.js 进来,然后实列化该对象。因此我们可以理解Layer.js 的作用是:

注意:Layer类的作用可以理解为,创建一个实列对象来管理每一个路由。也就是说每一个路由都会实例化一个Layer对象。

注意:如上opts中的参数像 end、sensitive、strict、ignoreCaptures等这些参数是pathToRegExp库中参数用法。
我们可以从opts这个配置上传入进来后,在Layer.js 中会调用 pathToRegExp 将路径字符串转换为正则表达式时,会将该这些参数传入到 pathToRegExp 这个js中去。

因此对于app.js 中注册路由这段代码来讲 router.get('/', ctx => {});的话,注册路由实例化Layer对象。

  1. /*
  2. path: '/',
  3. methods: 'GET',
  4. middleware: [Function]
  5. */
  6. var route = new Layer(path, methods, middleware, {
  7. end: false,
  8. name: null,
  9. sensitive: false,
  10. strict: false,
  11. prefix: '',
  12. ignoreCaptures: opts.ignoreCaptures = undefined
  13. });

就会调用Layer.js 的构造函数 Layer, 如下代码:

  1. /**
  2. * Initialize a new routing Layer with given `method`, `path`, and `middleware`.
  3. *
  4. * @param {String|RegExp} path Path string or regular expression.
  5. * @param {Array} methods Array of HTTP verbs.
  6. * @param {Array} middleware Layer callback/middleware or series of.
  7. * @param {Object=} opts
  8. * @param {String=} opts.name route name
  9. * @param {String=} opts.sensitive case sensitive (default: false)
  10. * @param {String=} opts.strict require the trailing slash (default: false)
  11. * @returns {Layer}
  12. * @private
  13. */
  14.  
  15. function Layer(path, methods, middleware, opts) {
  16. this.opts = opts || {};
  17. this.name = this.opts.name || null;
  18. this.methods = [];
  19. this.paramNames = [];
  20. this.stack = Array.isArray(middleware) ? middleware : [middleware];
  21.  
  22. methods.forEach(function(method) {
  23. var l = this.methods.push(method.toUpperCase());
  24. if (this.methods[l-1] === 'GET') {
  25. this.methods.unshift('HEAD');
  26. }
  27. }, this);
  28.  
  29. // ensure middleware is a function
  30. this.stack.forEach(function(fn) {
  31. var type = (typeof fn);
  32. if (type !== 'function') {
  33. throw new Error(
  34. methods.toString() + " `" + (this.opts.name || path) +"`: `middleware` "
  35. + "must be a function, not `" + type + "`"
  36. );
  37. }
  38. }, this);
  39.  
  40. this.path = path;
  41. this.regexp = pathToRegExp(path, this.paramNames, this.opts);
  42.  
  43. debug('defined route %s %s', this.methods, this.opts.prefix + this.path);
  44. };

如上是Layer.js 代码,this.methods === Layer.methods 保存了所有http的方法,如果是Get请求的话,最后 this.methods = ['HEAD', 'GET'] 了。this.stack === Layer.stack 则保存的是 我们的函数,如下:

  1. // 添加路由
  2. router.get('/', ctx => {
  3. ctx.body = '<h1>欢迎光临index page 页面</h1>';
  4. });

中的 function(ctx) {} 这个函数了。最后 通过 pathToRegExp.js 会将我们的路由字符串转换成正则表达式,保存到 this.regexp === Layer.regexp 变量中。

现在我们再回到 router.js中的 Router.prototype.register 方法中,再接着执行如下代码:

  1. // add parameter middleware
  2. Object.keys(this.params).forEach(function (param) {
  3. route.param(param, this.params[param]);
  4. }, this);
  5.  
  6. stack.push(route);
  7.  
  8. return route;

如上代码,目前的 this.params 还是 {}. 因此不会遍历进去。最后代码:stack.push(route); 含义是把当前的Layer对象的实列保存到 this.stack中。

注意:
1. Router.stack 的作用是保存每一个路由,也就是Layer的实列对象。
2. Layer.stack 的作用是 保存的是 每个路由的回调函数中间件。
两者的区别是:一个路由可以添加多个回调函数的。

最后代码返回了 route,也就是反回了 Layer对象的实列。

三:加载路由中间件

在app.js 中的代码,如下调用:

  1. // 加载路由中间件
  2. app.use(router.routes());

如上代码,我们可以分成二步,第一步是:router.routes(); 这个方法返回值,再把返回值传给 app.use(); 调用即可加载路由中间件了。因此我们首先来看第一步:router.routes()方法内部到底做了什么事情了。如下代码:
理解Router.prototype.routes

  1. /**
  2. * Returns router middleware which dispatches a route matching the request.
  3. *
  4. * @returns {Function}
  5. */
  6.  
  7. Router.prototype.routes = Router.prototype.middleware = function () {
  8. var router = this;
  9. var dispatch = function dispatch(ctx, next) {
  10. debug('%s %s', ctx.method, ctx.path);
  11.  
  12. var path = router.opts.routerPath || ctx.routerPath || ctx.path;
  13. var matched = router.match(path, ctx.method);
  14. var layerChain, layer, i;
  15.  
  16. if (ctx.matched) {
  17. ctx.matched.push.apply(ctx.matched, matched.path);
  18. } else {
  19. ctx.matched = matched.path;
  20. }
  21.  
  22. ctx.router = router;
  23.  
  24. if (!matched.route) return next();
  25.  
  26. var matchedLayers = matched.pathAndMethod
  27. var mostSpecificLayer = matchedLayers[matchedLayers.length - 1]
  28. ctx._matchedRoute = mostSpecificLayer.path;
  29. if (mostSpecificLayer.name) {
  30. ctx._matchedRouteName = mostSpecificLayer.name;
  31. }
  32.  
  33. layerChain = matchedLayers.reduce(function(memo, layer) {
  34. memo.push(function(ctx, next) {
  35. ctx.captures = layer.captures(path, ctx.captures);
  36. ctx.params = layer.params(path, ctx.captures, ctx.params);
  37. ctx.routerName = layer.name;
  38. return next();
  39. });
  40. return memo.concat(layer.stack);
  41. }, []);
  42.  
  43. return compose(layerChain)(ctx, next);
  44. };
  45.  
  46. dispatch.router = this;
  47.  
  48. return dispatch;
  49. };

如上代码:Router.prototype.routes = Router.prototype.middleware; router.routes 的别名也叫 router.middleware. 当我们使用 router.routes() 调用的时候,返回了一个dispatch函数,dispatch.router = this;  Router实列对象也是dispatch中的router属性。返回了一个dispatch函数后,我们就使用 app.use(router.routes); 会将路由模块添加到koa的中间件处理机制了。koa的中间件机制是以一个函数存在的,因此我们routes函数也就返回了一个函数。

具体想要了解koa中的洋葱型模型,可以看我这篇文章。

app.use(fn); 会将所有的中间件函数存放到 this.middleware 数组中,当我们在app.js中使用 app.listen()方法的时候,如下代码:

  1. app.listen(3001, () => {
  2. console.log('server is running at http://localhost:3001');
  3. });

koa中的部分 listen方法代码如下:

  1. listen(...args) {
  2. debug('listen');
  3. const server = http.createServer(this.callback());
  4. return server.listen(...args);
  5. }

最后当我们在浏览器中访问 http://localhost:3001/prefix/home 时候 会自动执行路由中的dispatch函数了。

我们再回到 Router.prototype.routes = Router.prototype.middleware = function () {} 中的dispatch函数,看看该函数内部做了什么事情了。

该dispatch 函数有两个参数 ctx 和 next. 这两个参数是koa中的基本知识,就不多介绍该两个参数了。

var router = this; 保存Router实列对象。var path = router.opts.routerPath || ctx.routerPath || ctx.path; 这句代码就拿到了 路由字符串了,比如当我们访问 http://localhost:3001/prefix/home 时候,ctx.path 就返回了 '/prefix/home'; 接着执行 var matched = router.match(path, ctx.method); 代码,会进行路由匹配。
理解Router.prototype.match:

router.match() 方法如下:

  1. /**
  2. * Match given `path` and return corresponding routes.
  3. *
  4. * @param {String} path
  5. * @param {String} method
  6. * @returns {Object.<path, pathAndMethod>} returns layers that matched path and
  7. * path and method.
  8. * @private
  9. */
  10.  
  11. Router.prototype.match = function (path, method) {
  12. var layers = this.stack;
  13. var layer;
  14. var matched = {
  15. path: [],
  16. pathAndMethod: [],
  17. route: false
  18. };
  19.  
  20. for (var len = layers.length, i = 0; i < len; i++) {
  21. layer = layers[i];
  22.  
  23. debug('test %s %s', layer.path, layer.regexp);
  24. // 这里是使用由路由字符串生成的正则表达式判断当前路径是否符合该正则
  25. if (layer.match(path)) {
  26.  
  27. // 将对应的 Layer 实例加入到结果集的 path 数组中
  28. matched.path.push(layer);
  29.  
  30. // 如果对应的 layer 实例中 methods 数组为空或者数组中有找到对应的方法
  31. if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
  32. // 将 layer 放入到结果集的 pathAndMethod 中
  33. matched.pathAndMethod.push(layer);
  34. if (layer.methods.length) matched.route = true;
  35. }
  36. }
  37. }
  38.  
  39. return matched;
  40. };

var matched = router.match(path, ctx.method); 调用该方法,会传入两个参数,第一个参数就是路由字符串 '/prefix/home'; 第二个参数 ctx.method, 也就是 'get' 方法。在match方法内部。this.stack === router.stack了,保存了每个路由的实列对象,我们可以打印下 this.stack, 它的值是如下所示:

  1. [ Layer {
  2. opts:
  3. { end: true,
  4. name: null,
  5. sensitive: false,
  6. strict: false,
  7. prefix: '/prefix',
  8. ignoreCaptures: undefined },
  9. name: null,
  10. methods: [ 'HEAD', 'GET' ],
  11. paramNames: [],
  12. stack: [ [Function] ],
  13. path: '/prefix/',
  14. regexp: { /^\/prefix(?:\/(?=$))?$/i keys: [] } },
  15. Layer {
  16. opts:
  17. { end: true,
  18. name: null,
  19. sensitive: false,
  20. strict: false,
  21. prefix: '/prefix',
  22. ignoreCaptures: undefined },
  23. name: null,
  24. methods: [ 'HEAD', 'GET' ],
  25. paramNames: [],
  26. stack: [ [Function] ],
  27. path: '/prefix/home',
  28. regexp: { /^\/prefix\/home(?:\/(?=$))?$/i keys: [] } },
  29. Layer {
  30. opts:
  31. { end: true,
  32. name: 'user',
  33. sensitive: false,
  34. strict: false,
  35. prefix: '/prefix',
  36. ignoreCaptures: undefined },
  37. name: 'user',
  38. methods: [ 'HEAD', 'GET' ],
  39. paramNames:
  40. [ { name: 'id',
  41. prefix: '/',
  42. delimiter: '/',
  43. optional: false,
  44. repeat: false,
  45. partial: false,
  46. asterisk: false,
  47. pattern: '[^\\/]+?' } ],
  48. stack: [ [Function] ],
  49. path: '/prefix/users/:id',
  50. regexp:
  51. { /^\/prefix\/users\/((?:[^\/]+?))(?:\/(?=$))?$/i
  52. keys:
  53. [ { name: 'id',
  54. prefix: '/',
  55. delimiter: '/',
  56. optional: false,
  57. repeat: false,
  58. partial: false,
  59. asterisk: false,
  60. pattern: '[^\\/]+?' } ] } } ]

可以看到数组中有三个Layer对象,那是因为我们注册了三次路由,比如我们的app.js代码如下:

  1. // 添加路由
  2. router.get('/', ctx => {
  3. ctx.body = '<h1>欢迎光临index page 页面</h1>';
  4. });
  5.  
  6. router.get('/home', ctx => {
  7. ctx.body = '<h1>欢迎光临home页面</h1>';
  8. });
  9.  
  10. router.get('user', '/users/:id', (ctx, next) => {
  11. ctx.body = 'hello world';
  12. });

注册了多少次路由,Layer类就会实例化多少次,而我们的Router.stack就是保存的是Layer实例化对象。保存的值是如上所示:

然后就遍历循环 this.stack了。如果 if (layer.match(path)) {},如果其中一个Layer对象匹配到该路由路径的话,就把该Layer对象存入到 matched.path.push(layer);matched对象中的path数组中了。具体的含义可以看上面的代码注释。

通过上面返回的结果集, 我们知道一个请求来临的时候, 我们可以使用正则来匹配路由是否符合, 然后在 path 数组或者 pathAndMethod 数组中找到对应的 Layer 实例对象. 我们再回到 Router.prototype.routes = function() {} 中的如下代码:

  1. if (ctx.matched) {
  2. ctx.matched.push.apply(ctx.matched, matched.path);
  3. } else {
  4. ctx.matched = matched.path;
  5. }

默认ctx.matched 为undefined,因此使用 matched.path 赋值该 ctx.matched了。当我们在浏览器访问:http://localhost:3001/prefix/home 时候,那么就会在match函数内部匹配到 '/prefix/home' 路由了,因此:matched.path 返回的值如下:

  1. [ Layer {
  2. opts:
  3. { end: true,
  4. name: null,
  5. sensitive: false,
  6. strict: false,
  7. prefix: '/prefix',
  8. ignoreCaptures: undefined },
  9. name: null,
  10. methods: [ 'HEAD', 'GET' ],
  11. paramNames: [],
  12. stack: [ [Function] ],
  13. path: '/prefix/home',
  14. regexp: { /^\/prefix\/home(?:\/(?=$))?$/i keys: [] } } ]

最终 ctx.matched 值也就是上面的值了。

ctx.router = router; 代码,也就是说把router对象挂载到 ctx中的router对象了。

if (!matched.route) return next(); 该代码的含义是:如果没有匹配到对应的路由的话,则直接跳过如下代码,执行下一个中间件。
如下三句代码的含义:

  1. var matchedLayers = matched.pathAndMethod
  2. var mostSpecificLayer = matchedLayers[matchedLayers.length - 1]
  3. ctx._matchedRoute = mostSpecificLayer.path;

matchedLayers 值是:

  1. [ Layer {
  2. opts:
  3. { end: true,
  4. name: null,
  5. sensitive: false,
  6. strict: false,
  7. prefix: '/prefix',
  8. ignoreCaptures: undefined },
  9. name: null,
  10. methods: [ 'HEAD', 'GET' ],
  11. paramNames: [],
  12. stack: [ [Function] ],
  13. path: '/prefix/home',
  14. regexp: { /^\/prefix\/home(?:\/(?=$))?$/i keys: [] } } ]

因此 mostSpecificLayer 也是上面的值哦;然后 ctx._matchedRoute = mostSpecificLayer.path = '/prefix/home' 了。
接着代码判断:

  1. if (mostSpecificLayer.name) {
  2. ctx._matchedRouteName = mostSpecificLayer.name;
  3. }

如上我们可以看到 mostSpecificLayer.name 为null,因此就不会进入if内部语句代码。当然如果改对象的name不为null的话,就会把该对应的name值保存到ctx对象上的_matchedRouteName属性上了。

接着代码如下

  1. /*
  2. 该函数的主要思想是:构建路径对应路由的处理中间件函数数组,
  3. 在每个匹配的路由对应的中间件处理函数数组前添加一个用于处理。
  4. */
  5. layerChain = matchedLayers.reduce(function(memo, layer) {
  6. memo.push(function(ctx, next) {
  7. ctx.captures = layer.captures(path, ctx.captures);
  8. ctx.params = layer.params(path, ctx.captures, ctx.params);
  9. ctx.routerName = layer.name;
  10. return next();
  11. });
  12. return memo.concat(layer.stack);
  13. }, []);
  14.  
  15. return compose(layerChain)(ctx, next);

理解 koa-compose 的思想,可以看这篇文章

它的作用是将多个中间件函数合并成一个中间件函数,然后执行该函数。

matchedLayers.reduce中的reduce是将接收一个函数作为累加器,数组中的每个值(从左到右)开始缩减,最终计算为一个值。

Router.prototype.allowedMethod 方法的作用就是用于处理请求的错误。 具体的可以看下源码,已经很晚了,Layer.js内部也有一些方法还未讲解到,大家有空可以去折腾下。koa-router源码先分析到这里了。

koa-router 源码由浅入深的分析(7.4.0版本的)的更多相关文章

  1. 手写@koa/router源码

    上一篇文章我们讲了Koa的基本架构,可以看到Koa的基本架构只有中间件内核,并没有其他功能,路由功能也没有.要实现路由功能我们必须引入第三方中间件,本文要讲的路由中间件是@koa/router,这个中 ...

  2. 从源码的角度分析ViewGruop的事件分发

    从源码的角度分析ViewGruop的事件分发. 首先我们来探讨一下,什么是ViewGroup?它和普通的View有什么区别? 顾名思义,ViewGroup就是一组View的集合,它包含很多的子View ...

  3. 安卓图表引擎AChartEngine(二) - 示例源码概述和分析

    首先看一下示例中类之间的关系: 1. ChartDemo这个类是整个应用程序的入口,运行之后的效果显示一个list. 2. IDemoChart接口,这个接口定义了三个方法, getName()返回值 ...

  4. java基础解析系列(十)---ArrayList和LinkedList源码及使用分析

    java基础解析系列(十)---ArrayList和LinkedList源码及使用分析 目录 java基础解析系列(一)---String.StringBuffer.StringBuilder jav ...

  5. 第九节:从源码的角度分析MVC中的一些特性及其用法

    一. 前世今生 乍眼一看,该标题写的有点煽情,最近也是在不断反思,怎么能把博客写好,让人能读下去,通俗易懂,深入浅出. 接下来几个章节都是围绕框架本身提供特性展开,有MVC程序集提供的,也有其它程序集 ...

  6. 通过官方API结合源码,如何分析程序流程

    通过官方API结合源码,如何分析程序流程通过官方API找到我们关注的API的某个方法,然后把整个流程执行起来,然后在idea中,把我们关注的方法打上断点,然后通过Step Out,从内向外一层一层分析 ...

  7. HTTP请求库——axios源码阅读与分析

    概述 在前端开发过程中,我们经常会遇到需要发送异步请求的情况.而使用一个功能齐全,接口完善的HTTP请求库,能够在很大程度上减少我们的开发成本,提高我们的开发效率. axios是一个在近些年来非常火的 ...

  8. 如何实现一个HTTP请求库——axios源码阅读与分析 JavaScript

    概述 在前端开发过程中,我们经常会遇到需要发送异步请求的情况.而使用一个功能齐全,接口完善的HTTP请求库,能够在很大程度上减少我们的开发成本,提高我们的开发效率. axios是一个在近些年来非常火的 ...

  9. qt creator源码全方面分析(3-3)

    目录 qtcreatordata.pri 定义stripStaticBase替换函数 设置自定义编译和安装 QMAKE_EXTRA_COMPILERS Adding Compilers 示例1 示例2 ...

随机推荐

  1. nodejs操作session和cookie

    session: 安装模块 cnpm install express-session 引入session注册到路由 var express = require('express'); var sess ...

  2. 页面优化,DocumentFragment对象详解

    一.前言 最近项目不是很忙,所以去看了下之前总想整理的重汇和回流的相关资料,关于回流优化,提到了DocumentFragment的使用,这个对象在3年前我记得是有看过的,但是一直没深入了解过,所以这里 ...

  3. Spring Boot 2.x(三):搭建开发环境(整合Spring Data JPA)

    为什么是JPA JPA虽然小众,但是足够优雅╮(╯_╰)╭,由于微服务的兴起,服务粒度的细化,多表联合的场景逐渐减少,更多的是一些简单的单表查询,而这正是JPA的强项所在.所以,以后的实战项目中我也会 ...

  4. Python判断相等

    判断相等方法有好几个:== .is . isinstance .issubclass .operator 模块. == :两个对象内容是否相等. >>> a = [22,44]> ...

  5. C# 如何添加Excel页眉页脚(图片、文字、奇偶页不同)

    简介 我们可以通过代码编程来对Excel工作表实现很多操作,在下面的示例中,将介绍如何来添加Excel页眉.页脚.在页眉处,我们可以添加文字,如公司名称.页码.工作表名.日期等,也可以添加图片,如LO ...

  6. C# 设置Excel超链接(一)

    在日常工作中,在编辑文档时,为了方便自己或者Boss能够实时查看到需要的网页或者文档时,需要对在Excel中输入的相关文字设置超链接,那么对于一些在Excel中插入的图片我们该怎么实现超链接呢,下面给 ...

  7. Linux文件系统的基本结构

    Linux文件系统结构 通过下面两张图片来认识一下Linux文件系统的结构. 当前工作目录 实践: 文件名称 这些规则不仅适用于文件,也适用于文件夹. 实践: ls命令 ls命令表示列出当前工作目录的 ...

  8. 【20190228】JavaScript-获取子元素

    在写JavaScript的时候发现了一个获取子节点的坑,如以下的html结构 <div id="parent"> <div>1</div> &l ...

  9. 少侠学代码系列(二)->JS实现

    少侠:小子,休息好了没,赶紧的 帅气的我:好了好了,嚷什么 少侠:(拔刀)嗯? 帅气的我:少侠,淡定淡定,我们来看秘籍吧,刚刚我们说了JS实现是由三个部分组成的 核心(ECMAScript),文档对象 ...

  10. 《我们不一样》Alpha冲刺_1-5

    第一天    日期:2018/6/15 1.1 今日完成任务情况以及遇到的问题. 马    兰.马   娟:用户.管理员数据库表的设计 李国栋.张惠惠:前端登录界面代码书写 伊力亚.张   康:配置s ...