SoFunction
Updated on 2025-04-03

Analysis on the principle of playing Koa-router

1. Preface

In order to keep itself simple, Koa does not have a bundled middleware. However, in actual development, we need to deal with all kinds of middleware. What this article will analyze is the commonly used routing middleware -- koa-router.

If you don't know the principles of Koa, you can check it out firstKoa principle analysis

2. Overview of koa-router

The source code of koa-router has only two files: and, corresponding to the Router object and the Layer object respectively.

The Layer object is the management of a single route, which contains information such as a route path, a route request method, and a route execution function (middleware), and provides route verification and params parameter resolution methods.

Compared with the Layer object, the Router object is a unified processing of all registered routes, and its API is for developers.

Next, we will comprehensively analyze the implementation principle of koa-router from the following aspects:

  • Implementation of Layer Objects
  • Routing registration
  • Routing Matching
  • Routing execution process

3. Layer

The Layer object is mainly used to manage a single route and is the smallest processing unit in the entire koa-router. The subsequent processing of modules is inseparable from the methods in Layer, which is the important reason why Layer is introduced first.

function Layer(path, methods, middleware, opts) {
  = opts || {};
 // Support routing alias  =  || null;
  = [];
  = [];
 // Save the route execution function in the stack, supporting input of multiple processing functions  = (middleware) ? middleware : [middleware];

 (function(method) {
  var l = (());
  // The HEAD request header information is consistent with GET, so we will handle it together here.  if ([l-1] === 'GET') {
   ('HEAD');
  }
 }, this);

 // Make sure the type is correct (function(fn) {
  var type = (typeof fn);
  if (type !== 'function') {
   throw new Error(
    () + " `" + ( || path) +"`: `middleware` "
    + "must be a function, not `" + type + "`"
   );
  }
 }, this);

  = path;
 // 1. Generate routing regular expressions based on routing paths // 2. Save params parameter information in the paramNames array  = pathToRegExp(path, , );
};

The Layer constructor is mainly used to initialize routing paths, routing request method arrays, routing processing function arrays, routing regular expressions, and params parameter information arrays, which mainly usepath-to-regexpThe method generates a regular expression based on the path string. Through this regular expression, the route matching and params parameter capture can be achieved:

// Verify the route = function (path) {
 return (path);
}

// Capture params parameters = function (path) {
 // It will be mentioned later for routing-level middleware without capturing params if () return [];
 return ().slice(1);
}

According to the parameter information in paramNames and the captrues method, you can get the key-value pair of the current routing params parameter:

 = function (path, captures, existingParams) {
 var params = existingParams || {};
 for (var len = , i=0; i<len; i++) {
  if ([i]) {
   var c = captures[i];
   params[[i].name] = c ? safeDecodeURIComponent(c) : c;
  }
 }
 return params;
};

It is necessary to pay attention to the safeDecodeURIComponent method in the above code. In order to avoid the server receiving unpredictable requests, any content input by the user as the URI part needs to be escaped by encodeURIComponent. Otherwise, when the content input by the user contains characters such as '&', '=', '?', etc., unexpected situations will occur. When we get parameters on the URL, we need to decode URIComponent for decoding, and decodeURIComponent can only be decoded by encodeURIComponent method or similar method. If the encoding method does not meet the requirements, decodeURIComponent will throw a URIError, so the author has dealt with the method safely here:

function safeDecodeURIComponent(text) {
 try {
  return decodeURIComponent(text);
 } catch (e) {
  // The encoding method does not meet the requirements, return to the original string  return text;
 }
}

Layer also provides methods for pre-processing for a single param:

 = function (param, fn) {
 var stack = ;
 var params = ;
 var middleware = function (ctx, next) {
  return (this, [param], ctx, next);
 };
  = param;
 var names = (function (p) {
  return ;
 });
 var x = (param);
 if (x &gt; -1) {
  (function (fn, i) {
   if (! || () &gt; x) {
    // Insert a single param preprocessing function into the correct position    (i, 0, middleware);
    return true; // Break out of the loop   }
  });
 }

 return this;
};

The reason for finding a single param processing function through some method in the above code is the following two points:

  • Keep the param processing function ahead of other routing processing functions;
  • There are multiple param parameters in the route, and the order of param processing functions needs to be maintained.
 = function (prefix) {
 if () {
   = prefix + ; // Stitch new routing paths   = [];
  // Generate regular expressions based on new routing path string   = pathToRegExp(, , );
 }
 return this;
};

The setPrefix method in Layer is used to set the prefix of the routing path, which is especially important in the implementation of nested routing.

Finally, Layer also provides a method of generating urls based on routes, mainly usingpath-to-regexpThe compile and parse replace the param in the routing path. In the splicing query, as mentioned earlier, it is necessary to perform cumbersome encodeURIComponent operations on key-value pairs. The author adopts theurijsThe simple API provided for processing.

4. Routing registration

1. Router constructor

First, let’s take a look at the Router constructor:

function Router(opts) {
 if (!(this instanceof Router)) {
  // The new keyword must be used  return new Router(opts);
 }

  = opts || {};
 // The request method supported by the server will be used in the subsequent allowedMethods method.  =  || [
  'HEAD',
  'OPTIONS',
  'GET',
  'PUT',
  'PATCH',
  'POST',
  'DELETE'
 ];

  = {}; // Save param preprocessing function  = []; // Storage layer};

The params and stack attributes initialized in the constructor are the most important. The former is used to save the param preprocessing function, and the latter is used to save the instantiated Layer object. And these two attributes are closely related to the routing registration that we will talk about next.

There are two ways to register routing in koa-router:

  • Specific HTTP verb registration methods, for example: ('/users', ctx => {})
  • Supports all HTTP verb registration methods, for example: ('/users', ctx => {})

2、http METHODS

Used in source codemethodsThe module obtains the HTTP request method name, and the internal implementation of this module mainly depends on the http module:

 && (function lowerCaseMethod (method) {
 return ()
})

3、() and ()

The internal implementations of these two ways of registering routing are basically similar. Take the source code of () as an example:

(function (method) {
 [method] = function (name, path, middleware) {
  var middleware;

  // 1. Process whether to pass name parameter  // 2. The middleware parameter supports the form of middleware1, middleware2...  if (typeof path === 'string' || path instanceof RegExp) {
   middleware = (arguments, 2);
  } else {
   middleware = (arguments, 1);
   path = name;
   name = null;
  }
  
  // Core processing logic for routing registration  (path, [method], middleware, {
   name: name
  });

  return this;
 };
});

The first part of this method is to process the incoming parameters. The processing of middleware parameters will remind everyone of the rest parameter in ES6, but there is a fatal difference between rest parameter and arguments:

The rest parameter only contains those arguments that do not have corresponding formal parameters, while arguments contain all arguments passed to the function.

If the rest parameter is used, the above function must require the developer to pass in the name parameter. However, you can also integrate the name and path parameters into objects and combine them with the rest parameters:

[method] = function (options, ...middleware) {
 let { name, path } = options
 if (typeof options === 'string' || options instanceof RegExp) {
  path = options
  name = null
 }
 // ...
 return this;
};

With the new features of ES6, the code has become much more concise.

The second part is the register method. The form of the method passed in is the biggest difference between() and(). The method passed in () is a single method, and the latter passes in all HTTP request methods in the form of an array. Therefore, there is essentially no difference in the implementation of these two registration methods.

4、register

 = function (path, methods, middleware, opts) {
 opts = opts || {};

 var router = this;
 var stack = ;

 // When registering routing middleware, path is allowed to be an array if ((path)) {
  (function (p) {
   (router, p, methods, middleware, opts);
  });
  return this;
 }

 // Instantiate Layer var route = new Layer(path, methods, middleware, {
  end:  === false ?  : true,
  name: ,
  sensitive:  ||  || false,
  strict:  ||  || false,
  prefix:  ||  || "",
  ignoreCaptures: 
 });

 // Set prefix if () {
  ();
 }

 // Set param preprocessing function ().forEach(function (param) {
  (param, [param]);
 }, this);

 (route);

 return route;
};

The register method is mainly responsible for instantiating the Layer object, updating the route prefix and pre-param processing functions. These operations have been mentioned in the Layer, and I believe everyone should be familiar with it.

5、use

Students who are familiar with Koa know that use is a method used to register middleware. Compared with the global middleware in Koa, the middleware of koa-router is at the routing level.
= function () {

 var router = this;
 var middleware = (arguments);
 var path;

 // Supports multipathing because middleware may act on multiple routing paths if ((middleware[0]) &amp;&amp; typeof middleware[0][0] === 'string') {
  middleware[0].forEach(function (p) {
   (router, [p].concat((1)));
  });

  return this;
 }
 // Process routing path parameters var hasPath = typeof middleware[0] === 'string';
 if (hasPath) {
  path = ();
 }

 (function (m) {
  // Nested routing  if () {
   // Nested routing flattening   (function (nestedLayer) {
    // Update the nested route path    if (path) (path);
    // Update the route path mounted to the parent route    if () ();

    (nestedLayer);
   }); 

   // Don't forget to update the param preprocessing operation on the parent route to the new route.   if () {
    ().forEach(function (key) {
     (key, [key]);
    });
   }
  } else {
   // Route level middleware Create a Layer instance without a method   (path || '(.*)', [], m, { end: false, ignoreCaptures: !hasPath });
  }
 });

 return this;
};

The koa-router middleware registration method mainly completes two functions:

  • Flatten the route nested structure, which involves updating the route path and inserting the param preprocessing function;
  • The routing level middleware is managed by registering a Layer instance without a method.

5. Routing Matching

 = function (path, method) {
 var layers = ;
 var layer;
 var matched = {
  path: [],
  pathAndMethod: [],
  route: false
 };

 for (var len = , i = 0; i &lt; len; i++) {
  layer = layers[i];
  if ((path)) {
   // The routing path meets the requirements   (layer);

   if ( === 0 || ~(method)) {
    // === 0 This layer is a routing level middleware    // ~(method) The routing request method is also matched    (layer);
    // Only if both the routing path and the routing request method are satisfied can the route be considered to be matched    if ()  = true;
   }
  }
 }
 return matched;
};

The match method mainly filters the layer through the method and methods attributes. The matched object returned contains the following parts:

  • path: Save all matching layer;
  • pathAndMethod: Save the layer whose routing level middleware and routing request method are matched on the premise that the routing path is matched;
  • route: Only when there is a layer whose routing path and route request method are matched can the route be considered to be matched.

In addition, before ES7, it is necessary to determine whether the array contains an element, and the indexOf method needs to be implemented, and this method returns the subscript of the element, so that the Boolean value has to be obtained by comparing it with -1:

 if ((method) > -1) {
  ...
 }

The author cleverly uses bit operations to save "noying -1", of course, in ES7, you can happily use the include method:

 if ((method)) {
  ...
 }

6. Routing execution process

Understanding the concept of routing in koa-router and the way of routing registration is how to execute it in koa as a middleware.

The way to register koa-router middleware in koa is as follows:

const Koa = require('koa');
const Router = require('koa-router');

const app = new Koa();
const router = new Router();

('/', (ctx, next) => {
 //  available
});

app
 .use(())
 .use(());

From the code, we can see that koa-router provides two middleware methods: routes and allowedMethods.

1、allowedMethods()

 = function (options) {
 options = options || {};
 var implemented = ;

 return function allowedMethods(ctx, next) {
  return next().then(function() {
   var allowed = {};

   if (! ||  === 404) {
    (function (route) {
     (function (method) {
      allowed[method] = method;
     });
    });

    var allowedArr = (allowed);

    if (!~()) {
     // If the server does not support this method     if () {
      var notImplementedThrowable;
      if (typeof  === 'function') {
       notImplementedThrowable = ();
      } else {
       notImplementedThrowable = new ();
      }
      throw notImplementedThrowable;
     } else {
      // Response 501 Not Implemented       = 501;
      ('Allow', (', '));
     }
    } else if () {
     if ( === 'OPTIONS') {
      // Get the set of methods supported by the server for this routing path       = 200;
       = '';
      ('Allow', (', '));
     } else if (!allowed[]) {
      if () {
       var notAllowedThrowable;
       if (typeof  === 'function') {
        notAllowedThrowable = ();
       } else {
        notAllowedThrowable = new ();
       }
       throw notAllowedThrowable;
      } else {
       // Response 405 Method Not Allowed        = 405;
       ('Allow', (', '));
      }
     }
    }
   }
  });
 };
};

The allowedMethods() middleware is mainly used to handle options requests and respond to 405 and 501 status. The above code saves the path in the previous matched object (set in the routes method, mentioned later). Under the premise that the path array in the matched object is not empty:

  • The server does not support the current request method, and returns a 501 status code;
  • The current request method is OPTIONS, returning 200 status code;
  • The layer in path does not support this method, and returns a 405 status;

For the above three cases, the server will set the Allow response header to return the request method supported on the routing path.

2、routes()

 =  = function () {
 var router = this;
 // Return the middleware processing function var dispatch = function dispatch(ctx, next) {
  var path =  ||  || ;
  var matched = (path, );
  var layerChain, layer, i;

  // 【1】Preparation for subsequent allowedMethods middleware  if () {
   (, );
  } else {
    = ;
  }

   = router;

  // No matching route Skip directly  if (!) return next();

  var matchedLayers = 
  var mostSpecificLayer = matchedLayers[ - 1]
  ctx._matchedRoute = ;
  if () {
   ctx._matchedRouteName = ;
  }
  layerChain = (function(memo, layer) {
   // [3] The pre-processing middleware for routing is mainly responsible for mounting params, route alias and capture array properties in the ctx context object.   (function(ctx, next) {
     = (path, );
     = (path, , );
     = ;
    return next();
   });
   return ();
  }, []);
  // 【4】Use the koa middleware organization method to form a 'little onion' model  return compose(layerChain)(ctx, next);
 };

 // 【2】The router attribute is used to distinguish the routing level middleware in the use method  = this;
 return dispatch;
};

The routes() middleware mainly implements four major functions.

  • Mount the path attribute of the matched object on and provide it to the subsequent allowedMethods middleware for use. (See [1] in the code)
  • The returned dispatch function sets the router property to distinguish route-level middleware and nested routes in the previously mentioned methods. (See [2] in the code)
  • Insert a new routing pre-processing middleware to mount the parsed params object, route alias, and capture arrays parsed by the layer in the context of ctx. This operation is similar to Koa building the context object before processing the request. (See [3] in the code)
  • For routing matching to many layers, koa-router is processed through koa-compose, whichHow koa handles middlewareSame, so koa-router is completely a small onion model.

7. Summary

Although koa-router is a middleware of koa, it also contains many middlewares. These middlewares are divided according to different routing paths through Layer objects, so that they no longer execute every request like koa's middleware. Instead, they use match method to match the corresponding middleware for each request, and then use koa-compose to form a middleware execution chain.

The above is the entire content of the implementation principle of koa-router. I hope it can help you better understand koa-router. I also hope everyone supports me.