series

  • [Koa source code] Koa
  • [Koa source code] Koa-router
  • Koa – BodyParser
  • Koa cookie
  • [Koa source code learning] KOa-session

preface

In the previous chapter, we added middleware to Koa to handle requests by calling the USE method, but a real application would need to respond differently to each request. In Koa applications, the KOA-Router module is usually used to support routing. So let’s take a look at how it’s implemented internally.

Routing registered

When the KOA-Router module is introduced, initialization is performed first, extending the methods on the prototype object of the Router, as shown below:

/* koa-router/lib/router.js */
for (var i = 0; i < methods.length; i++) {
  function setMethodVerb(method) {
    // Add a verb method to the Router prototype object
    Router.prototype[method] = function(name, path, middleware) {
      // ...
    };
  }
  setMethodVerb(methods[i]);
}

// Alias for `router.delete()` because delete is a reserved word
Router.prototype.del = Router.prototype['delete'];
Copy the code

Methods are a list of VERBS for HTTP. You first iterate over methods and then call setMethodVerb to add the corresponding verb method to the Router prototype. After that, you can use methods like router.get, router.post, etc. Register routes.

Let’s first look at the Router constructor:

/* koa-router/lib/router.js */
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'
  ];

  // Pre-parameter middleware
  this.params = {};
  / / the routing table
  this.stack = [];
};
Copy the code

Once you have created an instance of the Router, you can add routing information to it. When adding routing middleware, it can be divided into two types according to how it is used:

  1. Verb: Adds middleware using request method directly, including GET, POST, del, and all(including all methods).

  2. Use: Use the use method directly to add middleware.

So let’s take a look at how the KOA-Router handles these two approaches.

verb

For the first, the code looks like this:

/* koa-router/lib/router.js */
Router.prototype[method] = function(name, path, middleware) {
  var middleware;

  // Named routing is supported, and multiple middleware can be passed in at once
  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;
  }

  // Register the route
  this.register(path, [method], middleware, {
    name: name
  });

  return this;
};
Copy the code

As you can see in the KOA-Router, each route can support multiple middleware functions. After the parameters are processed, the register method is called to register the route as follows:

/* koa-router/lib/router.js */
Router.prototype.register = function (path, methods, middleware, opts) {
  opts = opts || {};

  var router = this;
  var stack = this.stack;

  // ...

  // Create a route, where methods and Middleware are arrays
  var route = new Layer(path, methods, middleware, {
    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 || "".ignoreCaptures: opts.ignoreCaptures
  });

  // Reset the matching rule of the route
  if (this.opts.prefix) {
    route.setPrefix(this.opts.prefix);
  }

  // Add preconfigured parameter middleware to the routes that meet the criteria
  for (var i = 0; i < Object.keys(this.params).length; i++) {
    var param = Object.keys(this.params)[i];
    route.param(param, this.params[param]);
  }

  // Add routing information to the routing table
  stack.push(route);

  return route;
};
Copy the code

As you can see, in the register method, an instance of Layer is first created based on the parameters provided, with code like this:

/* koa-router/lib/layer.js */
function Layer(path, methods, middleware, opts) {
  this.opts = opts || {};
  this.name = this.opts.name || null;
  this.methods = [];
  this.paramNames = [];
  this.stack = Array.isArray(middleware) ? middleware : [middleware];

  // If the route supports GET requests, then it also needs to support HEAD requests
  for(var i = 0; i < methods.length; i++) {
    var l = this.methods.push(methods[i].toUpperCase());
    if (this.methods[l- 1= = ='GET') {
       this.methods.unshift('HEAD'); }}// ensure middleware is a function
  // ...

  this.path = path;
  // Generate a route matching rule based on the parameters
  this.regexp = pathToRegExp(path, this.paramNames, this.opts);
};
Copy the code

To initialize the Layer, first add middleware to layer.stack, then add methods to layer.methods, and finally call the pathToRegExp method to create the corresponding regular expression based on the passed path and configuration. It is used to detect whether the path of the request matches the route, and if the layer contains dynamic parameters, it is added to layer.paramnames.

Returning to the register method above, after the route is created, there are two additional pieces of logic:

  1. Route. setPrefix: If the prefix option was configured when creating the router, the route configuration needs to be updated as follows:

    /* koa-router/lib/layer.js */
    Layer.prototype.setPrefix = function (prefix) {
      if (this.path) {
        // Join prefixes to reset the matching rule of routes
        this.path = prefix + this.path;
        this.paramNames = [];
        this.regexp = pathToRegExp(this.path, this.paramNames, this.opts);
      }
    
      return this;
    };
    Copy the code

    As you can see, you are simply calling the pathToRegExp method again to regenerate the regEXP and paramNames.

  2. Route. param: If the router defines the pre-middleware corresponding to some parameters, and the layer also contains the dynamic parameter, the pre-middleware corresponding to the parameters will be inserted before the layer middleware for some pre-processing work, and then detailed analysis.

At the end of the register method, add the generated route to router.stack to register the VERB route.

use

For verb-added routes, the middleware will only be added to the list if the requested method and path match exactly. However, for use, it does not need to match method, and path only needs to match the prefix, which can be used to provide some general processing logic. The code looks like this:

/* koa-router/lib/router.js */
Router.prototype.use = function () {
  var router = this;
  var middleware = Array.prototype.slice.call(arguments);
  var path;

  // support array of paths
  // ...

  // Support named routes
  var hasPath = typeof middleware[0= = ='string';
  if (hasPath) {
    path = middleware.shift();
  }

  for (var i = 0; i < middleware.length; i++) {
    var m = middleware[i];
    if (m.router) {
      / / zi lu by

      // Copy child routes
      var cloneRouter = Object.assign(Object.create(Router.prototype), m.router, {
        stack: m.router.stack.slice(0)});for (var j = 0; j < cloneRouter.stack.length; j++) {
        // Copy the routing information in the child route
        var nestedLayer = cloneRouter.stack[j];
        var cloneLayer = Object.assign(
          Object.create(Layer.prototype),
          nestedLayer
        );

        // Reset the route matching rule based on the current route
        if (path) cloneLayer.setPrefix(path);
        if (router.opts.prefix) cloneLayer.setPrefix(router.opts.prefix);
        // Copy the route information in the child route and add it to the current route
        router.stack.push(cloneLayer);
        cloneRouter.stack[j] = cloneLayer;
      }

      // Add preconfigured parameter middleware to the routes that meet the criteria
      if (router.params) {
        function setRouterParams(paramArr) {
          var routerParams = paramArr;
          for (var j = 0; j < routerParams.length; j++) {
            var key = routerParams[j];
            cloneRouter.param(key, router.params[key]);
          }
        }
        setRouterParams(Object.keys(router.params)); }}else {
      // Register middleware, there is no method and the configuration options are different from before
      router.register(path || '(. *)', [], m, { end: false.ignoreCaptures:! hasPath }); }}return this;
};
Copy the code

As you can see, when the use method is used to add middleware, there are two kinds of logic inside:

  1. For a normal middleware function, use the register method to register a route to the router. In this case, the methods array is empty and the options.end passed in is false. The generated regular expression matches only the prefix, not the full path.

  2. If m.outer exists, it indicates that it is a subrouter instance, and the routing information contained in it needs to be expanded and added to the current Router. Therefore, the subrouter. Stack is first iterated to copy the routing information, and setPrefix is also called if path or prefix exists. Reset the matching rule of the route, and then add the copied back route information to the current router. Finally, also determine whether there is a front parameter middleware, if there is, add it to the new layer.stack.

router.routes()

After registering a route to the router, the router.routes method is called to register the entire router as a middleware to the Koa instance, as shown in the following code:

/* koa-router/lib/router.js */
Router.prototype.routes = Router.prototype.middleware = function () {
  var router = this;

  var dispatch = function dispatch(ctx, next) {
    // ...
  };

  // The child route used in router.use
  dispatch.router = this;

  return dispatch;
};
Copy the code

As you can see, calling this method returns a DISPATCH method that matches the form of Koa middleware, so when a request is received from the client, the dispatch method is executed, and the Router, based on its internal routing table, finds the routing information that matches the request and processes the request accordingly.

Handle the request

When the application executes to the middleware corresponding to the router, the above dispatch method is called, with code like this:

/* koa-router/lib/router.js */
Router.prototype.routes = Router.prototype.middleware = function () {
  // ...
  var dispatch = function dispatch(ctx, next) {
    debug('%s %s', ctx.method, ctx.path);

    var path = router.opts.routerPath || ctx.routerPath || ctx.path;
    // Run the match method to find all routes in the table that match path and method
    var matched = router.match(path, ctx.method);
    var layerChain, layer, i;

    if (ctx.matched) {
      ctx.matched.push.apply(ctx.matched, matched.path);
    } else {
      ctx.matched = matched.path;
    }

    ctx.router = router;

    // If no routing information matching the request is found in the routing table, the current router is skipped and the next middleware is executed
    if(! matched.route)return next();

    var matchedLayers = matched.pathAndMethod
    var mostSpecificLayer = matchedLayers[matchedLayers.length - 1]
    ctx._matchedRoute = mostSpecificLayer.path;
    if (mostSpecificLayer.name) {
      ctx._matchedRouteName = mostSpecificLayer.name;
    }

    // Flatten the middleware in layer
    layerChain = matchedLayers.reduce(function(memo, layer) {
      // Insert a middleware in front of each layer to handle dynamic routing parameters
      memo.push(function(ctx, next) {
        ctx.captures = layer.captures(path, ctx.captures);
        ctx.params = layer.params(path, ctx.captures, ctx.params);
        ctx.routerName = layer.name;
        return next();
      });
      returnmemo.concat(layer.stack); } []);// Construct the actuator by calling the compose method, where next is used to jump out of the current router and execute the next middleware
    return compose(layerChain)(ctx, next);
  };
  // ...
};
Copy the code

As you can see, in the dispatch method, router.match method will be called first to find all routing information matching the request from the current router, and the code is as follows:

/* koa-router/lib/router.js */
Router.prototype.match = function (path, method) {
  var layers = this.stack;
  var layer;
  var matched = {
    // Check only the path
    path: [],
    // Both the path and the request method need to match
    pathAndMethod: [],
    // Whether the current router has matching routing information
    route: false
  };

  for (var len = layers.length, i = 0; i < len; i++) {
    layer = layers[i];

    debug('test %s %s', layer.path, layer.regexp);

    // Check whether the paths match
    if (layer.match(path)) {
      matched.path.push(layer);

      if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
        matched.pathAndMethod.push(layer);
        if (layer.methods.length) matched.route = true; }}}return matched;
};
Copy the code

The match method receives the path and method of the current request. Based on these two information, you can find the matching route information from the current router. In this method, router.stack is first iterated, routing information is extracted, and layer.match method is called, with the code as follows:

/* koa-router/lib/layer.js */
Layer.prototype.match = function (path) {
  return this.regexp.test(path);
};
Copy the code

Here, the regular expression generated by pathToRegExp is used to check whether the path of the request matches the path of the current route. If the test passes, it indicates that the route is matched, and it will be added to the matched. We also need to determine if the requested method is in the current layer.methods, and if it is also supported, add it to matche.pathandMethod and set matche.route to true.

In addition to determining the route of type VERB, there is another logic, when layer.methods.length === 0, it indicates that this is middleware added through router.use, and it is also added to matched. However, the matched. Route will not be modified. From this, it can be seen that a router must match at least one route corresponding to the verb.

Back to dispatch of the above methods, we have found in the router all the routing information match the current request, if there is a matching route, you will need to perform these routing corresponding middleware, before execution, because there may be a dynamic routing, and in its corresponding middleware need to be able to use the params to obtain parameters, Therefore, the reduce method is first used. Before each matched route, a new middleware is inserted to process parameters, with the code as follows:

/* koa-router/lib/layer.js */
Layer.prototype.params = function (path, captures, existingParams) {
  var params = existingParams || {};

  for (var len = captures.length, i=0; i<len; i++) {
    if (this.paramNames[i]) {
      var c = captures[i];
      // Build the params parameter, which can then be accessed through ctx.params
      params[this.paramNames[i].name] = c ? safeDecodeURIComponent(c) : c; }}return params;
};
Copy the code

Once the insertion is complete, use the concat method to flatten the middleware in layer.stack, and finally, you have all the middleware that this request, the router, needs to execute.

Once you have all the middleware to execute, you also need an asynchronous executor to execute the middleware, so you also use the compose method, which combines the middleware first and then calls it immediately. In addition to CTX, a next method is passed in as opposed to koA. This method is executed only if a call to Next continues in the last middleware, and is used to transfer execution from the Router to the next middleware handler.

Parametric middleware in front

During the previous route registration, there was a piece of logic to insert the parameter middleware, which would first check router.params with the following code:

/* koa-router/lib/router.js */
Router.prototype.register = function (path, methods, middleware, opts) {
  // ...

  // add parameter middleware
  for (var i = 0; i < Object.keys(this.params).length; i++) {
    var param = Object.keys(this.params)[i];
    route.param(param, this.params[param]);
  }

  // ...
};
Copy the code

Router-params router-param router-param router-param router-param router-param router-param router-param router-param router-param router-param

/* koa-router/lib/router.js */
Router.prototype.param = function(param, middleware) {
  this.params[param] = middleware;
  for (var i = 0; i < this.stack.length; i++) {
    var route = this.stack[i];
    route.param(param, middleware);
  }

  return this;
};
Copy the code

Router. params stores the parameter name and the middleware corresponding to it. When adding a new routing information or adding a new parameter middleware, the layer.param method will be called to modify the layer.stack, and its code is as follows:

/* koa-router/lib/layer.js */
Layer.prototype.param = function (param, fn) {
  // Array of middleware in current layer
  var stack = this.stack;
  // Dynamic path parameters resolved by path
  var params = this.paramNames;
  var middleware = function (ctx, next) {
    // The first argument here represents the actual value of the argument
    return fn.call(this, ctx.params[param], ctx, next);
  };
  middleware.param = param;

  // All dynamic parameters contained in the current layer
  var names = params.map(function (p) {
    return p.name;
  });

  // If the current layer uses this parameter, insert middleware before layer.stack
  var x = names.indexOf(param);
  if (x > - 1) {
    // iterate through the stack, to figure out where to place the handler fn
    stack.some(function (fn, i) {
      // param handlers are always first, so when we find an fn w/o a param property, stop here
      // if the param handler at this part of the stack comes after the one we are adding, stop here
      if(! fn.param || names.indexOf(fn.param) > x) {// inject this param handler right before the current item
        stack.splice(i, 0, middleware);
        return true; // then break the loop}}); }return this;
};
Copy the code

As you can see, router.param provides a mechanism to pre-process all routes that use this parameter by adding its corresponding parameter middleware to the layer.stack.

conclusion

Koa-router provides routing functions for KOA. Normally, the business logic is written in the Router as a module. After writing the business logic, the Router uses the Routes method to wrap it into a function in the form of KOA middleware. This allows you to use the Router in KOA just like normal middleware.