The third article is about one of the most important middleware in the KOA ecosystem: the KOA-Router

Koa and koA-compose compose compose compose compose compose compose compose compose compose

Koa – what is the router

First, because KOA is a platform for managing middleware, registering a middleware is performed using USE. No matter what the request is, all the middleware will execute it once (if it doesn’t end in the middle). So, this is very confusing for developers. How do we write the logic if we want to do routing?

app.use(ctx= > {
  switch (ctx.url) {
    case '/':
    case '/index':
      ctx.body = 'index'
      break
    case 'list':
      ctx.body = 'list'
      break
    default:
      ctx.body = 'not found'}})Copy the code

Admittedly, this is a simple approach, but certainly not suitable for large projects, where dozens of interfaces are too cumbersome to be controlled by a single Switch. Not to mention that requests may only support GET or POST, and that this approach does not support requests /info/: UID that contain parameters in the URL very well. This is not a problem with Express, which already provides methods with the same name as Get, POST, and so on to register callbacks: Express

const express = require('express')
const app = express()

app.get('/'.function (req, res) {
  res.send('hi there.')})Copy the code

But KOA does a lot of streamlining, splitting out a lot of logic as separate middleware. As a result, many Express projects migrate to KOA, requiring additional middleware installations, with the KOA-Router being the most commonly used. In KOA, an additional KOA-Router installation is required to implement a similar routing function: KOA

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

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

router.get('/'.async ctx => {
  ctx.body = 'hi there.'
})

app.use(router.routes())
  .use(router.allowedMethods())
Copy the code

It does seem like a bit more code, since a lot of the logic has been moved from within the framework to middleware. Some of the trade-offs to maintain a concise KOA framework. The logic of KOA-Router is indeed more complicated than that of KOA. You can think of KOA as a market, and koA-Router is one of the stalls. Koa only needs to ensure the stable operation of the market, but it is the KOA-Router that sets up stalls inside that really deals with customers

The general structure of the KOA-Router

The structure of the KOA-Router is not very complex, so there are only two files:

.├ ── layer.js ├─ ├.jaCopy the code

Layer is mainly for the encapsulation of some information, and the main roadbed is provided by router:

File Description
layer Information storage: path, METHOD, regular match corresponding to path, parameters in path, middleware corresponding to path
router Main logic: expose the function of registered route externally, provide middleware to process route, check the REQUESTED URL and invoke route processing in the corresponding layer

Koa-router running process

We can use the basic example thrown above to illustrate how the KOA-Router is executed:

const router = new Router() // Instantiate a Router object

// Register a route listener
router.get('/'.async ctx => {
  ctx.body = 'hi there.'
})

app
  .use(router.routes()) // Register the middleware of the Router object with the Koa instance, the main processing logic for subsequent requests
  .use(router.allowedMethods()) // Add response handling for OPTIONS and some processing that METHOD does not support
Copy the code

Something about creating an instance

First, when the KOA-Router is instantiated, it is possible to pass a configuration item parameter as the initial configuration information. However, this configuration item is simply described in the READme as:

Param Type Description
[opts] Object
[opts.prefix] String Prefix Router Paths

Router registration prefix = Router registration prefix = Router registration prefix = Router registration prefix = Router registration prefix = Router registration prefix

const Router = require('koa-router')
const router = new Router({
  prefix: '/my/awesome/prefix'
})

router.get('/index', ctx => { ctx.body = 'pong! ' })

// curl /my/awesome/prefix/index => pong!
Copy the code

P.S. But remember, ifprefixIn order to/End, the route registration can omit the prefix/It would have turned up/Repetitive situation

The code to instantiate the 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'
  ]

  this.params = {}
  this.stack = []
}
Copy the code

There is only one assignment of methods visible, but after looking at the other source code, it is found that in addition to prefix there are some parameters passed in the instantiation, but it is not clear why the documentation does not mention them:

Param Type Default Description
sensitive Boolean false Whether to match case strictly
strict Boolean false If set tofalseMatches the following path/Is optional
methods Array[String] ['HEAD','OPTIONS','GET','PUT','PATCH','POST','DELETE'] Set the METHOD supported by the route
routerPath String null

sensitive

If sensitive is enabled, routes are listened for with a stricter matching rule, regardless of case in the URL, and matched exactly as they were registered:

const Router = require('koa-router')
const router = new Router({
  sensitive: true
})

router.get('/index', ctx => { ctx.body = 'pong! ' })

// curl /index => pong!
// curl /Index => 404
Copy the code

strict

Strict, similar to sensitive, is used to make path matching stricter. By default, the slash (/) at the end of a path is optional. If this parameter is enabled and no slash is added to the end of a route during route registration, the matching route cannot be added with the slash (/) either:

const Router = require('koa-router')
const router = new Router({
  strict: true
})

router.get('/index', ctx => { ctx.body = 'pong! ' })

// curl /index => pong!
// curl /Index => pong!
// curl /index/ => 404
Copy the code

methods

The methods configuration item exists because if we have an interface that needs to support both GET and POST, router. GET and router. POST will be ugly. So we might want to use router.all to simplify operations:

const Router = require('koa-router')
const router = new Router()

router.all('/ping', ctx => { ctx.body = 'pong! ' })

// curl -X GET /index => pong!
// curl -X POST /index => pong!
Copy the code

This is so perfect that our requirements can be easily realized. However, if we try some other methods more, something embarrassing will happen:

> curl -X DELETE /index  => pong!
> curl -X PUT    /index  => pong!
Copy the code

This is obviously not what we expected, so in this case, based on the current KOA-Router, we need to make the following changes to achieve the desired functionality:

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

const app = new Koa()
// Modify section 1
const methods = ['GET'.'POST']
const router = new Router({
  methods
})

// Modify section 2
router.all('/'.async (ctx, next) => {
  // Ideally, these decisions should be left to middleware
  if(! ~methods.indexOf(ctx.method)) {return await next()
  }

  ctx.body = 'pong! '
})
Copy the code

With these two modifications, we can achieve the desired function:

> curl -X GET    /index  => pong!
> curl -X POST   /index  => pong!
> curl -X DELETE /index  => Not Implemented
> curl -X PUT    /index  => Not Implemented
Copy the code

Personally, I think this is a logical problem in the implementation of allowedMethods, but maybe I didn’t get the author’s point. Some key sources of allowedMethods are as follows:

Router.prototype.allowedMethods = function (options) {
  options = options || {}
  let implemented = this.methods

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

      // If ctx.body is assigned, no subsequent logic will necessarily be executed
      // So it's up to us to make our own judgments in the middleware
      if(! ctx.status || ctx.status ===404) {
        if(! ~implemented.indexOf(ctx.method)) {if (options.throw) {
            let notImplementedThrowable
            if (typeof options.notImplemented === 'function') {
              notImplementedThrowable = options.notImplemented() // set whatever the user returns from their function
            } else {
              notImplementedThrowable = new HttpError.NotImplemented()
            }
            throw notImplementedThrowable
          } else {
            ctx.status = 501
            ctx.set('Allow', allowedArr.join(', '))}}else if (allowedArr.length) {
          // ...}})}}Copy the code

First of all, allowedMethods exist as a post-placed middleware, because next is called first in the returned function, followed by the judgment on METHOD. A consequence of this is that if we perform operations like CTx. body = XXX in the callback of the route, In fact, the status value of this request will be changed so that it does not become 404, and the logic of METHOD checking will not be properly triggered. To trigger METHOD logic correctly, we need to manually determine whether ctx. METHOD is what we want in the route listener, and then skip the current middleware execution. And this judgment step is actually different from allowedMethods middleware! The logic of ~implemented.indexof (ctx.method) is completely repetitive, and it’s not clear why the KOA-router does this.

Of course, allowedMethods cannot exist as a pre-middleware, because a Koa may be linked to multiple routers, and configurations between routers may be different, so it is not guaranteed that all routers are the same as the METHOD that the current Router can handle. Therefore,, I personally feel that the existence significance of methods parameter is not very great.

routerPath

The existence of this parameter.. Feelings can lead to some pretty weird situations. Router.routes () = router.routes();

Router.prototype.routes = Router.prototype.middleware = function () {
  let router = this
  let dispatch = function dispatch(ctx, next) {
    let path = router.opts.routerPath || ctx.routerPath || ctx.path
    let matched = router.match(path, ctx.method)
    // If a match is found, the corresponding middleware is executed
    // Perform subsequent operations
  }
  return dispatch
}
Copy the code

Because what we actually registered with KOA is a middleware that executes dispatch every time a request comes in, and uses this configuration item in dispatch to determine whether a router has been hit, an expression like this: Router. Opts. RouterPath | | CTX. RouterPath | | CTX. The path, the router that represent the current router instances, that is to say, if we instantiate a router, if fill in routerPath, This causes routerPath to be used in preference for routing checks for any request:

const router = new Router({
  routerPath: '/index'
})

router.all('/index'.async (ctx, next) => {
  ctx.body = 'pong! '
})
app.use(router.routes())

app.listen(8888, _ = >console.log('server run as http://127.0.0.1:8888))
Copy the code

If you have code like this, whatever URL is requested will be assumed to be /index for matching:

> curl http://127.0.0.1:8888
pong!
> curl http://127.0.0.1:8888/index
pong!
> curl http://127.0.0.1:8888/whatever/path
pong!
Copy the code

Implement the forwarding function skillfully with routerPath

Similarly, the short circuit operator has three expressions. The second CTX is the context of the current request. That is, if we have a middleware that executes before routes, we can also assign a value to change the URL used for route judgment:

const router = new Router()

router.all('/index'.async (ctx, next) => {
  ctx.body = 'pong! '
})

app.use((ctx, next) = > {
  ctx.routerPath = '/index' // Manually change the routerPath
  next()
})
app.use(router.routes())

app.listen(8888, _ = >console.log('server run as http://127.0.0.1:8888))
Copy the code

Such code can achieve the same effect. The routerPath passed in the instantiation is a bit tricky, but there is an appropriate scenario for changing the routerPath in the middleware. This can be simply interpreted as an implementation of forwarding, which is invisible to the client and still accesses the original URL. But changing ctx.routerPath in the middleware can easily match the route to where we want to forward it

// Old version logon logic processing
router.post('/login', ctx => {
  ctx.body = 'old login logic! '
})

// New version of logon processing logic
router.post('/login-v2', ctx => {
  ctx.body = 'new login logic! '
})

app.use((ctx, next) = > {
  if (ctx.path === '/login') { // The old version request is matched and forwarded to the new version
    ctx.routerPath = '/login-v2' // Manually change the routerPath
  }
  next()
})
app.use(router.routes())
Copy the code

This implements a simple forward:

> curl -X POST http://127.0.0.1:8888/login
new login logic!
Copy the code

Register route listening

Router.get, router.post, router.post, router.get, router.post But this is really just a shortcut that calls the register method from the Router internally:

Router.prototype.register = function (path, methods, middleware, opts) {
  opts = opts || {}

  let router = this
  let stack = this.stack

  // support array of paths
  if (Array.isArray(path)) {
    path.forEach(function (p) {
      router.register.call(router, p, methods, middleware, opts)
    })

    return this
  }

  // create route
  let 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
  })

  if (this.opts.prefix) {
    route.setPrefix(this.opts.prefix)
  }

  // add parameter middleware
  Object.keys(this.params).forEach(function (param) {
    route.param(param, this.params[param])
  }, this)

  stack.push(route)

  return route
}
Copy the code

This method is marked as private in the comments, but some of its parameters are not shown in various places in the code. God knows why those parameters are kept, but since they exist, we need to know what it does. This is the basic method of routing listening, and the function signature is roughly as follows:

Param Type Default Description
path String/Array[String] One or more paths
methods Array[String] Which routes need to be listened onMETHOD
middleware Function/Array[Function] Middleware array of functions that route the callback function actually called
opts Object {} Some configuration parameters for registering routes, as mentioned abovestrict,sensitiveandprefixIt’s all reflected here

As you can see, the function basically implements this flow:

  1. checkpathIs it an array? If yes, iterateitemCall itself
  2. Instantiate aLayerObject to set some initialization parameters
  3. Set middleware processing for certain parameters, if any
  4. Puts the instantiated object intostackStored in the

So before introducing these parameters, it is necessary to briefly describe the Layer constructor:

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]

  methods.forEach(function(method) {
    var l = this.methods.push(method.toUpperCase());
    if (this.methods[l- 1= = ='GET') {
      this.methods.unshift('HEAD')}},this)

  // ensure middleware is a function
  this.stack.forEach(function(fn) {
    var type = (typeof fn)
    if(type ! = ='function') {
      throw new Error(
        methods.toString() + "`" + (this.opts.name || path) +"`: `middleware` "
        + "must be a function, not `" + type + "`")}},this)

  this.path = path
  this.regexp = pathToRegExp(path, this.paramNames, this.opts)
}
Copy the code

Layer is responsible for storing the information monitored by the route, including the URL when registering the route each time, the regular expression generated by the URL, the parameters existing in the URL, and the middleware corresponding to the route. All of these are stored by Layer, and the main focus is on the array parameters during the instantiation process:

  • methods
  • paramNames
  • stack

Methods stores the valid METHOD that the route listens for and converts case to case in the process of instantiation. Because of the plugin used for paramNames, it doesn’t look very clear, but the paramNames array is actually pushed inside the pathToRegExp, If you’re comfortable with this, pathToRegExp(Path, & this.paramnames, this.opts) is the array used when concatenating hash path parameters. Stack stores the middleware function that listens for the path. Part of the router. Middleware logic depends on this array

path

The processing logic in the function header is mainly to support multipathing while registering. If the first path parameter is found to be an array, the function will traverse the path parameter to call itself. So the same route for multiple urls can be handled like this:

router.register(['/'['/path1'['/path2'.'path3']]], ['GET'], ctx => {
  ctx.body = 'hi there.'
})
Copy the code

This is a completely valid setting:

> curl http://127.0.0.1:8888/
hi there.
> curl http://127.0.0.1:8888/path1
hi there.
> curl http://127.0.0.1:8888/path3
hi there.
Copy the code

methods

The methods parameter is considered as an array by default. Even if only one METHOD is listened, an array needs to be passed in as the parameter. If the array is empty, even if the URL matches, it will be skipped directly and the next middleware will be implemented, which will be mentioned in router.routes

middleware

Middleware is the thing that routing actually does at once. It’s still KOA compliant middleware, and there can be more than one of them, and it works the way the Onion works. This is one of the most important aspects of koA-Router, enabling some of our middleware to execute only at specific urls. Multiple middleware pieces written here are valid for that URL.

P.S. inkoa-routerAlso provides a method calledrouter.use, this will register a basedrouterMiddleware for instance

opts

Opts is used to set configuration rules for route generation, including the following optional parameters:

Param Type Default Description
name String Set the value of the routename, namedrouter
prefix String Very chicken rib parameters, no eggs at all, which seems to set the route prefix, actually has no use
sensitive Boolean false Whether to match case strictly, override instantiationRouterThe configuration in
strict Boolean false Whether to match case strictly, if set tofalseMatches the following path/Is optional
end Boolean true Whether the path match is the end of the full URL
ignoreCaptures Boolean Whether to ignore the capture group in the result of the route matching re
name

The first is name, which is mainly used in these places:

  1. Easier to locate when throwing exceptions
  2. Can be achieved byrouter.url(<name>),router.route(<name>)Get the correspondingrouterinformation
  3. While the middleware is executing,nameWill be toctx.routerNameIn the
router.register('/test1'['GET'], _ => {}, {
  name: 'module'
})

router.register('/test2'['GET'], _ => {}, {
  name: 'module'
})

console.log(router.url('module') = = ='/test1') // true

try {
  router.register('/test2'['GET'].null, {
    name: 'error-module'})}catch (e) {
  console.error(e) // Error: GET `error-module`: `middleware` must be a function, not `object`
}
Copy the code

If multiple routers use the same name, the router.url call returns the first registered router:

// route Is used to obtain named routes
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] // Match the first one and return it directly}}return false
}

// url Gets the url corresponding to the route and generates the real URL using the parameters passed in
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)
}
Copy the code
Let’s digress on router. Url

Router. URL is a good option if you want to jump to certain urls in a project:

router.register(
  '/list/:id'['GET'], ctx => {
    ctx.body = `Hi ${ctx.params.id}, query: ${ctx.querystring}`
  }, {
    name: 'list'
  }
)

router.register('/'['GET'], ctx => {
  // /list/1? name=Niko
  ctx.redirect(
    router.url('list', { id: 1 }, { query: { name: 'Niko'}}})))// curl -L http://127.0.0.1:8888 => Hi 1, query: name=Niko
Copy the code

As you can see, router.url actually calls the URL method of the Layer instance, which handles some of the arguments passed in at build time. Source code address: The layer.js#L116 function accepts two arguments, params and options. Because the layer instance stores the corresponding path information, params is the replacement of some parameters stored in the path. There is only one query field to concatenate the data following search:

const Layer = require('koa-router/lib/layer')
const layer = new Layer('/list/:id/info/:name', [], [_= >{})console.log(layer.url({ id: 123.name: 'Niko' }))
console.log(layer.url([123.'Niko']))
console.log(layer.url(123.'Niko'))
console.log(
  layer.url(123.'Niko', {
    query: {
      arg1: 1.arg2: 2}}))Copy the code

If params is not an object, it will be considered to be called by layer.url(parameter, parameter, parameter, opTS). Convert it to layer.url([parameter, parameter], opts). The logic at this point only needs to handle three cases:

  1. Parameter substitution in array form
  2. hashParameter substitution of the form
  3. There is no parameter

This argument substitution means that a URL will be used by a third-party library to process the argument part of the link, that is, the /:XXX part, and pass in a hash to do something similar to template substitution:

// We can simply think of this operation as:
let hash = { id: 123.name: 'Niko' }
'/list/:id/:name'.replace(/ (? :\/:)(\w+)/g, ($_1) = >` /${hash[$1]}`)
Copy the code

The layer.url is then processed to generate a hash like structure for the various parameters, eventually replacing the hash to get the full URL.

prefix

In the process of instantiating Layer above, it seems that the weight of opts.prefix is higher, but then there is a judgment logic to call setPrefix to re-assign the value. After searching the whole source code, it is found that the only difference is that, There will be a debug that applies the prefix passed in when registering the router, and everything else will be overwritten by the prefix passed in when instantiating the router.

And if you want to apply prefix correctly, you need to call setPrefix because the store for path during Layer instantiation is the path parameter passed in remotely. Applying the prefix requires manually triggering setPrefix:

// Layer instantiation operations
function Layer(path, methods, middleware, opts) {
  // omit irrelevant operations
  this.path = path
  this.regexp = pathToRegExp(path, this.paramNames, this.opts)
}

// The prefix is applied only when setPrefix is called
Layer.prototype.setPrefix = function (prefix) {
  if (this.path) {
    this.path = prefix + this.path
    this.paramNames = []
    this.regexp = pathToRegExp(this.path, this.paramNames, this.opts)
  }

  return this
}
Copy the code

This is shown in several methods exposed to the user, such as get, set, and use. Of course in the document also provides all the router can be set directly prefix, the method of the router. The prefix: document so simple tell you can set the prefix, ministries cycle prefix, call all layer. The methods setPrefix:

router.prefix('/things/:thing_id')
Copy the code

But after looking through the layer.setPrefix source code, it turns out that there is actually a dark pit. Because the implementation of setPrefix takes the prefix argument and concatenates it to the head of the current path. A problem with this is that if we call setPrefix multiple times it will result in multiple prefix superpositions instead of substitutions:

router.register('/index'['GET'], ctx => {
  ctx.body = 'hi there.'
})

router.prefix('/path1')
router.prefix('/path2')

/ / > curl http://127.0.0.1:8888/path2/path1/index
// hi there.
Copy the code

The prefix method overlays the prefix instead of overwriting it

Sensitive and strict

These parameters will override the same parameters that were passed when the Router was instantiated.

end

End is an interesting argument, as shown in other modules referenced in koa-Router, path-to-regexp:

if (end) {
  if(! strict) route +='(? : ' + delimiter + ')? '

  route += endsWith === '$' ? '$' : '(? = ' + endsWith + ') '
} else {
  if(! strict) route +='(? : ' + delimiter + '(? = ' + endsWith + '))? '
  if(! isEndDelimited) route +='(? = ' + delimiter + '|' + endsWith + ') '
}

return new RegExp(A '^' + route, flags(options))
Copy the code

EndWith can simply be understood as $in the re, the end of the match. Looking at the logic of the code, roughly speaking, if end: true is set, $will be added at the end of the match in any case. If end: false, it is triggered only if strict: false or isEndDelimited: false are set at the same time. So we can use these two parameters to achieve URL fuzzy matching:

router.register(
  '/list'['GET'], ctx => {
    ctx.body = 'hi there.'
  }, {
    end: false.strict: true})Copy the code

That is, the resulting regular expression for matching routes looks something like this:

/^\/list(? =\/|$)/i// The re can be obtained with the following code
require('path-to-regexp').tokensToRegExp('/list/', {end: false.strict: true})
Copy the code

The ending $is optional, which results in any request we send that starts with /list being picked up by the middleware.

ignoreCaptures

The ignoreCaptures parameter is used to set whether the matching path parameter in the URL needs to be returned to the middleware. When ignoreCaptures is set, these two parameters become empty objects:

router.register('/list/:id'['GET'], ctx => {
  console.log(ctx.captures, ctx.params)
  // ['1'], { id: '1' }
})

// > curl /list/1

router.register('/list/:id'['GET'], ctx => {
  console.log(ctx.captures, ctx.params)
  // [], {}
}, {
  ignoreCaptures: true
})
// > curl /list/1
Copy the code

This is obtained by calling two methods from Layer during middleware execution. The first call to captures gets all the arguments, and if ignoreCaptures is set, an empty array is returned. Params is then called to pass in all the parameters generated when registering the route and their actual values, and then generates a full hash to inject into the CTX object:

// Middleware logic
ctx.captures = layer.captures(path, ctx.captures)
ctx.params = layer.params(path, ctx.captures, ctx.params)
ctx.routerName = layer.name
return next()
// Logical end of middleware

// method provided by layer
Layer.prototype.captures = function (path) {
  if (this.opts.ignoreCaptures) return []
  return path.match(this.regexp).slice(1)
}

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]
      params[this.paramNames[i].name] = c ? safeDecodeURIComponent(c) : c
    }
  }

  return params
}

// Do something like this:
// [18, 'Niko'] + ['age', 'name']
/ / = >
// { age: 18, name: 'Niko' }
Copy the code

The role of the router. The param

The Layer object is instantiated in the register and is not placed directly on the stack. Instead, it is pushed onto the stack after doing something like this:

Object.keys(this.params).forEach(function (param) {
  route.param(param, this.params[param])
}, this)

stack.push(route) / / loading
Copy the code

This is used to add middleware processing for a URL parameter, and is strongly associated with router.param:

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

The operations are similar in that the former is used to add all parAM middleware for new route listeners, while the latter is used to add PARAM middleware for all existing routes. Router.param has this. Params [param] = XXX. This allows you to loop through this.params to retrieve all middleware during subsequent new route listening.

Router. param is also documented. The address of the document can be used for validation purposes, but because of the special processing in layer.param, we don’t have to worry about the order of param execution. Layer ensures that param is executed before middleware that relies on this parameter:

router.register('/list/:id'['GET'], (ctx, next) => {
  ctx.body = `hello: ${ctx.name}`
})

router.param('id', (param, ctx, next) => {
  console.log(`got id: ${param}`)
  ctx.name = 'Niko'
  next()
})

router.param('id', (param, ctx, next) => {
  console.log('param2')
  next()
})


// > curl /list/1
// got id: 1
// param2
// hello: Niko
Copy the code

Shortcuts like GET/POST are the most commonly used

With the basic register method out of the way, let’s take a look at a few router.verb methods exposed to developers:

// get|put|post|patch|delete|del
// Loop to register multiple METHOD shortcuts
methods.forEach(function (method) {
  Router.prototype[method] = function (name, path, middleware) {
    let 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
  }
})

Router.prototype.del = Router.prototype['delete'] // And the last alias processing, because del is not a valid METHOD
Copy the code

Unfortunately, the VERB method removes a large number of OPTS arguments, leaving only a name field by default. It simply handles the logic associated with the named route and then calls Register to complete the operation.

Router. use- The middleware inside the router

Router. use can be used to register middleware. There are two cases of using router.use:

  1. Generic middleware functions
  2. existingrouterThe instance is passed in as middleware
Normal use

Here is the key code for the use method:

Router.prototype.use = function () {
  var router = this
  middleware.forEach(function (m) {
    if (m.router) { // This is passed in with 'router.routes()'
      m.router.stack.forEach(function (nestedLayer) {
        if (path) nestedLayer.setPrefix(path)
        if (router.opts.prefix) nestedLayer.setPrefix(router.opts.prefix) // Call 'prefix' of the Router instance of 'use'
        router.stack.push(nestedLayer)
      })

      if (router.params) {
        Object.keys(router.params).forEach(function (key) {
          m.router.param(key, router.params[key])
        })
      }
    } else { // Generic middleware registration
      router.register(path || '(. *)', [], m, { end: false.ignoreCaptures: !hasPath })
    }
  })
}

// There is such a step in the routes method
Router.prototype.routes = Router.prototype.middleware = function () {
  function dispatch() {
    // ...
  }

  dispatch.router = this // Assigns the router instance to the returned function

  return dispatch
}
Copy the code

The first is the more conventional way, passing in a function, an optional path, to register the middleware. Use (‘path’) does not exist on its own, but must have a route listener that matches its path:

router.use('/list', ctx => {
  // If there is only such a middleware, it will not be executed anyway
})

// There must be a register callback with the same path
router.get('/list', ctx => { })

app.use(router.routes())
Copy the code

Here’s why:

  1. .useand.getAre based on.registerTo implement, but.useinmethodsAn empty array is passed in the argument
  2. When a path is matched, all matched middleware is pulled out and the corresponding middleware is checkedmethodsIf thelength ! = = 0One is marked for the current matching groupflag
  3. This is checked before implementing middlewareflagIf no, none of the middleware in the path is setMETHOD, will directly skip to other processes (Such as allowedMethod)
Router.prototype.match = function (path, method) {
  var layers = this.stack
  var layer
  var matched = {
    path: [].pathAndMethod: [].route: false
  }

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

    if (layer.match(path)) {
      matched.path.push(layer)

      if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
        matched.pathAndMethod.push(layer)

        // Flag is set only after methods are found that are not null
        if (layer.methods.length) matched.route = true}}}return matched
}

// And there is such an operation in 'routes'
Router.prototype.routes = Router.prototype.middleware = function () {
  function dispatch(ctx, next) {

    // If there is no 'flag', skip it
    if(! matched.route)return next()
  }

  return dispatch
}
Copy the code
Pass in other Router instances

As you can see, if router.routes() is selected to reuse the middleware, all routes for the instance are iterated and prefix is set. And push the modified Layer to the current router. Note that the Layer setPrefix is concatenated, not overlaid, as mentioned above. Use manipulates the Layer object, so this usage will cause the previous middleware path to be modified. And if the middleware passing in use is already registered with KOA, this will cause the same middleware to execute twice (if next is called) :

const middlewareRouter = new Router()
const routerPage1 = new Router({
  prefix: '/page1'
})

const routerPage2 = new Router({
  prefix: '/page2'
})

middlewareRouter.get('/list/:id'.async (ctx, next) => {
  console.log('trigger middleware')
  ctx.body = `hi there.`
  await next()
})

routerPage1.use(middlewareRouter.routes())
routerPage2.use(middlewareRouter.routes())

app.use(middlewareRouter.routes())
app.use(routerPage1.routes())
app.use(routerPage2.routes())
Copy the code

As with the code above, there are actually two problems:

  1. The final valid access path is/page2/page1/list/1Because theprefixSplices, not overlays
  2. When we call in middlewarenextLater,console.logIt will print three times in a row, because all of theroutesIt’s all dynamic, actuallyprefixHave been modified to/page2/page1

Be careful not to assume that this approach can be used for route reuse

Processing of requests

And, finally, how the Router handles a request when it comes in. A Router instance can throw two middleware registrations to the KOA:

app.use(router.routes())
app.use(router.allowedMethods())
Copy the code

Routes takes care of the primary logic. AllowedMethods is responsible for providing a back-end method-checking middleware.

AllowedMethods are nothing to say, just some validation against the currently requested method and return some error messages. Many of the methods described above are for the ultimate Routes service:

Router.prototype.routes = Router.prototype.middleware = function () {
  var router = this

  var dispatch = function dispatch(ctx, next) {
    var path = router.opts.routerPath || ctx.routerPath || ctx.path
    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(! 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
    }

    layerChain = matchedLayers.reduce(function(memo, layer) {
      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()
      })
      return memo.concat(layer.stack)
    }, [])

    return compose(layerChain)(ctx, next)
  };

  dispatch.router = this

  return dispatch
}
Copy the code

As you can see, the KoA-Router also provides an alias, Middleware, to do the same thing. And the function call eventually returns a middleware function that is actually attached to the KOA. Koa’s middleware is pure middleware and will execute the contained middleware regardless of the request. Therefore, it is not recommended to create multiple Router instances to use prefix, which would result in mounting multiple Dispatches on the KOA to check whether the URL conforms to the rule

Once in the middleware, URL determination is done, which is what we mentioned above can be used to do foraward implementation. Router. match (); matche.path (); matche.path ();

Router.prototype.match = function (path, method) {
  var layers = this.stack // This is the layer object corresponding to all middleware in the Router instance
  var layer
  var matched = {
    path: [].pathAndMethod: [].route: false
  }

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

    if (layer.match(path)) { // Here is a simple re match
      matched.path.push(layer)

      if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
        // Push valid middleware in
        matched.pathAndMethod.push(layer)

        // Check whether METHOD exists
        if (layer.methods.length) matched.route = true}}}return matched
}

// A simple re match
Layer.prototype.match = function (path) {
  return this.regexp.test(path)
}
Copy the code

The reason why it is said to check whether there is CTX. Matched is processed, instead of assigning the value to this attribute directly. This is because, as mentioned above, one KOA instance may register multiple KOA-Router instances. As a result, after the middleware of one router instance completes execution, other router instances may also match a URL, but this will ensure that the matched instances are always added, rather than overwritten every time.

pathwithpathAndMethodAre allmatchReturns two arrays, the difference between the twopathReturns data that matches the URL successfully, andpathAndMethodMatches the URL and matches the METHOD

const router1 = new Router()
const router2 = new Router()

router1.post('/', _ => {})

router1.get('/'.async (ctx, next) => {
  ctx.redirectBody = 'hi'
  console.log(`trigger router1, matched length: ${ctx.matched.length}`)
  await next()
})

router2.get('/'.async (ctx, next) => {
  ctx.redirectBody = 'hi'
  console.log(`trigger router2, matched length: ${ctx.matched.length}`)
  await next()
})

app.use(router1.routes())
app.use(router2.routes())

// >  curl http://127.0.0.1:8888/
// => trigger router1, matched length: 2
// => trigger router2, matched length: 3
Copy the code

Regarding middleware implementation, koa-compose is also used in koa-router to incorporate Onions:

var matchedLayers = matched.pathAndMethod

layerChain = matchedLayers.reduce(function(memo, layer) {
  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()
  })
  return memo.concat(layer.stack)
}, [])

return compose(layerChain)(ctx, next)
Copy the code

This block of code will add a CTX attribute assignment middleware operation before all matched middleware, which means that the reduce execution will have at least X2 middleware functions corresponding to the Onion model. Layer may contain multiple middleware, and don’t forget middleware, which is why concat is used in Reduce rather than push because you need to modify the CTX for this middleware trigger before each middleware execution. This includes matched URL parameters and information such as the name of the current middleware.

[
  layer1[0].// The corresponding middleware 1 in the first register
  layer1[1].// The corresponding middleware 2 in the first register
  layer2[0]  // The corresponding middleware 1 in the second register
]

/ / = >

[
  (ctx, next) = > {
    ctx.params = layer1.params // Set the value of the first register
    return next()
  },
  layer1[0].// The corresponding middleware 1 in the first register
  layer1[1].// The corresponding middleware 2 in the first register
  (ctx, next) => {
    ctx.params = layer2.params // Set the value of the second register
    return next()
  },
  layer2[0]  // The corresponding middleware 1 in the second register
]
Copy the code

At the end of routes, koa-compose is called to merge the middleware array generated by Reduce, and the second optional parameter mentioned earlier in Koa-compose is used for the final callback after onion execution.


reporter

At this point, the mission of the KOA-Router is complete, and the route is registered and monitored. Read the source code for koA-Router and get confused:

  • What is clearly implemented in the code is not reflected in the documentation.
  • If this is not documented, why have an implementation in your code?

The two simplest pieces of evidence:

  1. You can modify itctx.routerPathTo implement theforwardFunctionality, but they don’t tell you in the documentation
  2. Can be achieved byrouter.register(path, ['GET', 'POST'])To quickly listen to multipleMETHOD, butregisterBe marked for@private

References:

  • koa-router | docs
  • path-to-regexp | docs

Location of the sample code in the repository: learning-Koa-Router