Implementation principles of koA Router

Two purposes of this paper

  1. Learn about path-to-regexp usage
  2. Koa-router source code parsing

path-to-regexp

Introduction to path-to-regexp usage.

How can it be used to match identification routes?

Imagine if we wanted to identify a route, what could we do?

The most intuitive is definitely the path string matching

'/string'= >'/string'
Copy the code

We can do some feedback when the route matches /string. Such as performing a callback, etc.

We can also take advantage of the regular matching feature

In this way, the matching mode can obviously operate in more diversified ways and more matching paths

For example, for path:

/^\/string\/.*? \/xixi$// => '/string/try/xixi'
Copy the code

Path-to-regexp is one such tool

Imagine if we were to parse a match for a path, we would need to write the regular expression ourselves. So as to achieve the matching effect.

Can I write it?

I’m sure we can, but it takes too long.

Path-to-regexp is an easy way to do this.

This section describes some apis of Path-to-Regexp

how to use it ???

The main API

const pathToRegexp = require('path-to-regexp') // pathToRegexp(path, keys? , options?) // pathToRegexp.parse(path) // pathToRegexp.compile(path)Copy the code
// pathToRegexp(path, keys? , options?) // path can be string/ string array/regular expression // keys found in the path // options are some matching rules such as full match separatorCopy the code

path-to-regexp api demo

// a demo if we want to implement a normal match for some key values eg: /user/:name how can we implement a regex like this to achieve full match in front and extract values with the regex group eg: /\/user\/((? ! / /). *?) / /? $/.exec('/user/zwkang'Finding a string that matches a re returns an array/returning a null pathToRegexp is what it does. Generate the required regular expression matches. There's some encapsulation, of course, but that's what it's all about.Copy the code
pathToRegexp('/user/:name').exec('/user/zwkang') path option ? PathToRegexp ('/:foo/:bar? ').exec('/test')
        pathToRegexp('/:foo/:bar? ').exec('/test/route'If you look closely, you can see that these words are almost identical to quantifiers in the regular expression. They can also match unnamed parameters. Keys are stored according to sequence subscripts Compile (Compile) uses Compile to pass a path and returns a function that can be filled to generate a value that matches path pathToregexp.compile ('/user/:id')({id: 123}) => "/user/123"Applicable to the string pathToRegexp. TokensToRegExp (tokens, keys? , options?) PathToRegexp. TokensToFunction (tokens) name can be seen on an array of tokens can be converted to a regular expressions will be tokens array into the compile method to generate functionsCopy the code

Go through the procedure

PathToRegexp = Return => regexp parse => path = Matching tokens=> keys token compile => path => Generatorfunction => value => full path string
Copy the code

koa-router

I don’t know if you’ve ever used a KOA-Router

Notic: Note the current maintenance permission change for koA-Router

The Router implementation is actually a rege-based access path matching.

If you are using KOA native code

Example:

Matching path /simple returns a body string with body {name:’zwkang’}

A simple example, e.g

Suppose we match the route using a simple middleware match ctx.url app.use(async (CTX, next) => {const url = ctx.urlif(/^\/simple$/i.test(url)) {
        ctx.body = {
            name: 'ZWkang'}}else {
        ctx.body = {
            errorCode: 404,
            message: 'NOT FOUND'
        }
        ctx.status = 404
    }
    returnAwait next()}) test code'use normal koa path', () => {
    it('use error path', (done) => {
        request(http.createServer(app.callback()))
        .get('/simple/s')
        .expect(404)
        .end(function (err, res) {
            if (err) return done(err);
            expect(res.body).to.be.an('object');
            expect(res.body).to.have.property('errorCode', 404).done(a); }); }) it('use right path', (done) => {
        request(http.createServer(app.callback()))
        .get('/simple')
        .expect(200)
        .end(function (err, res) {
            if (err) return done(err);
            expect(res.body).to.be.an('object');
            expect(res.body).to.have.property('name'.'ZWkang')
            done();
        });
    })
})
Copy the code

Above our own IMPLEMENTATION of THE URL pattern is such, a single match, if multiple matches, even matching parameters, need to consider the re writing.

Disadvantages, relatively single, set method relatively simple, weak function

If we use the KOA-Router

// A simple usage it('simple use should work', (done) => {
    router.get('/simple', (ctx, next) => {
        ctx.body = {
            path: 'simple'
        }
    })
    app.use(router.routes()).use(router.allowedMethods());
    request(http.createServer(app.callback()))
      .get('/simple')
      .expect(200)
      .end(function (err, res) {
        if (err) return done(err);
        expect(res.body).to.be.an('object');
        expect(res.body).to.have.property('path'.'simple');
        done(a); }); })Copy the code

App.callback ()

Some point explanations of the test code above

Callback is the operating mechanism for KOA. What does a method represent? Represents its setup process

Our usual Listen method is actually the only one that calls http.createserver (app.callback())


Let’s see what the Koa-Router does

Front knowledge

From the simple example above, we can see that understanding the KOA operation mechanism, internal middleware processing pattern.

Start with the demo

The instance methods invoked when koA is called include

router.allowedMethods ===> router.routes ===> router.get

Consider that since it is a KOA, use call, then we can be sure that it is a standard KOA middleware pattern

The function returned is similar to

Async (CTX, next) => {// process routing logic // process business logic}Copy the code

The opening comments of the source code describe the basic usage

We can refine it a little bit

Router.verb () specifies the corresponding function based on the HTTP method

For example, the router. The get (). The post (). The put ()

The.all method supports all HTTP methods

If the route matches, ctx._matchedRoute can obtain the path. If it is a named route, you can obtain the route name ctX. _matchedRouteName

Querystring (? xxxx)

Named functions are allowed

Routes can be quickly located at development time

 * router.get('user'.'/users/:id', (ctx, next) => { * // ... *}); * * router.url('user', 3); * / / = >"/users/3"
Copy the code

Multiple routes are allowed

 * router.get(
 *   '/users/:id',
 *   (ctx, next) = >{*return User.findOne(ctx.params.id).then(function(user) { * ctx.user = user; * next(); *}); * *}ctx= >{*console.log(ctx.user);
 *     // => { id: 17, name: "Alex" }*} *);Copy the code

Nested routines are allowed by

 * var forums = new Router();
 * var posts = new Router();
 *
 * posts.get('/', (ctx, next) => {... }); * posts.get('/:pid', (ctx, next) => {... }); * forums.use('/forums/:fid/posts', posts.routes(), posts.allowedMethods());
 *
 * // responds to "/forums/123/posts" and "/forums/123/posts/123"
 * app.use(forums.routes());
Copy the code

Route prefix matching is allowed

 var router = new Router({
    prefix: '/users'
 });
 
 router.get('/',...). ;// responds to "/users"
 router.get('/:id',...). ;// responds to "/users/:id"
Copy the code

Capture named parameters to add to ctx.params

 router.get('/:category/:title', (ctx, next) => {
    console.log(ctx.params);
    // => { category: 'programming', title: 'how-to-node' }
 });
Copy the code

Code holistic analysis

There are some clever points in the code design

  1. The separation of responsibilities, the upper Router does HTTP layer method status and related processing of routers middlewares. Low-level layer. js focuses on routing path processing
  2. The design of the middlerware

Start with the Layer file.

layer.js

As mentioned earlier, this file is mainly used to handle operations on the Path-to-regexp library

There are only 300 lines or so in the file. There are few methods.

Layer constructor

function Layer(path, methods, middleware, opts) {
  this.opts = opts || {};
  this.name = this.opts.name || null; // Name the route
  this.methods = []; // Allow method
  // [{ name: 'bar', prefix: '/', delimiter: '/', optional: false, repeat: false, pattern: '[^\\/]+?' }]
  this.paramNames = [];
  this.stack = Array.isArray(middleware) ? middleware : [middleware]; // Middleware heap
  // Initialize parameters
  // tips: forEach the second argument can pass this
  // forEach push array we can use the array [L-1] to determine the end element
  // The push method returns the number of elements pushed by the array

  // The external method argument is passed inside
  methods.forEach(function(method) {
    var l = this.methods.push(method.toUpperCase());
    // If a GET request is supported, HEAD requests are supported
    if (this.methods[l- 1= = ='GET') {
      this.methods.unshift('HEAD'); }},this);

  // ensure middleware is a function
  // Make sure each 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);
  / / path
  this.path = path;
  // Use pathToRegExp to generate regular expressions for paths
  // The array associated with params falls back into our this.paramnames
  // this.regexp generates an array for cutting
  this.regexp = pathToRegExp(path, this.paramNames, this.opts);

  debug('defined route %s %s'.this.methods, this.opts.prefix + this.path);
};
Copy the code

We can focus on inputs and outputs.

Enter: PATH, Methods, Middleware, OPts

Output: Object properties include (OPTS, name, methods, paramNames, Stack, PATH, regEXP)

As we mentioned earlier, layer processes the route path to determine whether it matches, and the link library path-to-regexp is important.

The stack should match the middleware passed in. Stack is an array, so we can see that our path can have multiple routes.

Let’s focus on that

What encapsulation does the Koa-Router give us, based on path-to-regexp combined with the middleware it needs

Prototype chain mount methods are

params

// Get the route parameter key-value pairs
Layer.prototype.params = function (path, captures, existingParams) {
  var params = existingParams || {};

  for (var len = captures.length, i=0; i<len; i++) {
    if (this.paramNames[i]) { // Get the corresponding capture group
      var c = captures[i]; // Get parameter values
      params[this.paramNames[i].name] = c ? safeDecodeURIComponent(c) : c; 
      // Populate the key-value pairs}}// Returns the parameter key-value pair object
  return params;
};
Copy the code

When the constructor is initialized, we generate this.regexp by passing this.paramNames to fill out the param it parsed according to path

Input: path, capture group, existing parameter group Output: a parameter key-value pair object

The treatment is very common. Because params corresponds to captures. So you can cycle directly.

match

// Check whether a match is found
Layer.prototype.match = function (path) {
  return this.regexp.test(path);
};
Copy the code

The first thing to look at is the input and return values

Input: the path

Output: Boolean matching or not

We can see that this.regexp is the property value, proving that we have the ability to change this. Regexp at any time to affect the return value of this function

captures

// Returns the parameter value
Layer.prototype.captures = function (path) {
  if (this.opts.ignoreCaptures) return []; // Ignore capture and return null

  // match returns an array of matching results
  // We can see from the re that the generated re is a full match.
  /** * eg: * var test = [] * pathToRegExp('/:id/name/(.*?) ', test) * * /^\/((? : [^ \] / +)? )\/name\/((? :. *?) (?) : \ /? ) = $)? $/i * * '/xixi/name/ashdjhk'.match(/^\/((? : [^ \] / +)? )\/name\/((? :. *?) (?) : \ /? ) = $)? $/i) * * ["/xixi/name/ashdjhk", "xixi", "ashdjhk"] */

  return path.match(this.regexp).slice(1); // [value, value .....]
};
Copy the code

Enter: path Path

Output: Capture array of groups

Returns the entire capture group contents

url

Layer.prototype.url = function(params, options) {
  var args = params;
  console.log(this);
  var url = this.path.replace(/\(\.\*\)/g."");
  var toPath = pathToRegExp.compile(url); //
  var replaced;

  if (typeofparams ! ="object") {
    args = Array.prototype.slice.call(arguments);
    if (typeof args[args.length - 1] = ="object") {
      options = args[args.length - 1];
      args = args.slice(0, args.length - 1); }}var tokens = pathToRegExp.parse(url);
  var replace = {};

  if (args instanceof Array) {
    for (var len = tokens.length, i = 0, j = 0; i < len; i++) {
      if(tokens[i].name) replace[tokens[i].name] = args[j++]; }}else if (tokens.some(token= > token.name)) {
    replace = params; // replace = params
  } else {
    options = params; // options = params
  }

  replaced = toPath(replace); // Replace by default is the default key-value pair passed in // followed by the full URL

  if (options && options.query) {
    // Whether query exists
    var replaced = new uri(replaced); //
    replaced.search(options.query); // Add route query
    return replaced.toString();
  }

  return replaced; // Return the URL string
};

Copy the code

Url method of the Layer instance

In fact, an example is /name/:id

After parsing, we get a params object {id: XXX}

Can we deduce the actual URL from /name/:id and params objects?

This URL method provides just that capability.

param

Layer.prototype.param = function(param, fn) {
  var stack = this.stack;
  var params = this.paramNames;
  var middleware = function(ctx, next) {
    return fn.call(this, ctx.params[param], ctx, next);
  };
  middleware.param = param;

  var names = params.map(function(p) {
    return String(p.name);
  });
  var x = names.indexOf(param); / / get the index

  if (x > - 1) {
    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

      // Two strategies
      // 1. The param processor is always first, and the current fn.param does not exist. Insert into [a,b] mid => [a,b]
      // 2. [mid, a, b] mid2 => [mid, a, b
      // Before normal middleware
      // Ensure that the params are sorted in order
      if(! fn.param || names.indexOf(fn.param) > x) {// Inject middleware currently
        stack.splice(i, 0, middleware);
        return true; // Stop some iterations.}}); }return this;
};
Copy the code

This method adds processors for a single param to the current stack

It’s actually doing an operation on the stack of the layer

setPrefix

Layer.prototype.setPrefix = function(prefix) {
  // Calling setPrefix resets some constructs of layer
  if (this.path) {
    this.path = prefix + this.path;
    this.paramNames = [];
    this.regexp = pathToRegExp(this.path, this.paramNames, this.opts);
  }

  return this;
};
Copy the code

Prefixes the current path and resets some current instance attributes

safeDecodeURIComponent

function safeDecodeURIComponent(text) {
  try {
    return decodeURIComponent(text);
  } catch (e) {
    returntext; }}Copy the code

Ensure safeDecodeURIComponent does not throw any errors

Layer.

The stack of the layer stores the actual middleware[s].

The main function is to design for pathToRegexp. Provides the ability to implement calls to the upper-level Router.


Router

The Router is primarily responsive to the upper KOA framework (CTX, Status, etc.) and links to the lower layer instances.

Router constructor

function Router(opts) {
    / / new automatically
  if(! (this instanceof Router)) {
    return new Router(opts);
  }

  this.opts = opts || {};
  // methods are used to validate allowedmethods
  this.methods = this.opts.methods || [
    "HEAD"."OPTIONS"."GET"."PUT"."PATCH"."POST"."DELETE"
  ]; // Initialize the HTTP method

  this.params = {}; // Parameter key-value pairs
  this.stack = []; // Store route instances
}
Copy the code
methods.forEach(function(method) {
  // Append all HTTP method methods to the prototype
  Router.prototype[method] = function(name, path, middleware) {
    var middleware;
    // Compatible parameters
    // Allow path to be a string or regular expression
    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 with the current instance
    // Basically a generic method to set up Install Middleware. (mark. tag: function)
    this.register(path, [method], middleware, {
      name: name
    });
    // chain call
    return this;
  };
});

Copy the code

Register the Router prototype

HTTP method: router.prototype. get = XXX

It’s easier and more accurate to use when we use examples

router.get(‘name’, path, cb)

You can obviously have more than one middleware. Get (name, path, cb)

Notice that the main thing here is calling another method

Notic: register method. And the input of this method, we can pay attention to. Much like the Layer instance initializes the input parameter.

With a little bit of confusion we can go into the Register method.

The register method


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

  var router = this;
  var stack = this.stack;
  if (Array.isArray(path)) {
    path.forEach(function(p) {
      router.register.call(router, p, methods, middleware, opts);
    });
    return this;
  }

  var route = new Layer(path, methods, middleware, {
    end: opts.end === false ? opts.end : true.// It needs to be explicitly declared as end
    name: opts.name, // The name of the route
    sensitive: opts.sensitive || this.opts.sensitive || false.// add I to case-sensitive re
    strict: opts.strict || this.opts.strict || false.// Non-capture grouping plus (? :)
    prefix: opts.prefix || this.opts.prefix || "".// Prefix characters
    ignoreCaptures: opts.ignoreCaptures || false // Use ignore capture for layer
  });

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

  // add parameter middleware
  // Add parameter middleware
  Object.keys(this.params).forEach(function(param) {
    route.param(param, this.params[param]);
  }, this);
  // Stack pushes a single Layer instance
  stack.push(route);

  return route;
};

Copy the code

We can see that the whole register method is designed to register a single path.

Call the register method on forEach for multipathing. This notation is not uncommon in KOA-Router implementations.

Looking at the Register method, our suspicions are confirmed, as most of the incoming arguments are used to initialize layer instances.

After initializing the Layer instance, we place it on the stack under the Router instance.

According to some OPTs then processing judgment. Not much is probably harmless.

So we know how to use register.

  1. Initializes the Layer instance
  2. Register it with the Router instance.

We know when we call the Router instance.

There are usually two steps to using middleware

  1. use(router.routes())
  2. use(router.allowedMethods())

We know that a minimalist form of middleware invocation is always

app.use(async (ctx, next) => {
    await next()
})
Copy the code

We don’t care about koA-body or KoA-Router

Passing app.use is always one

async (ctx, next) => {
    await next()
}
Copy the code

Such functions are compatible with koA middleware requirements.

With that in mind

We can find out in the Routes method.

Routes prototype method

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

  var dispatch = function dispatch(ctx, next) {
    debug("%s %s", ctx.method, ctx.path);
    // Get the path
    var path = router.opts.routerPath || ctx.routerPath || ctx.path;
    // matched is already handled to get the Layer object payload
    var matched = router.match(path, ctx.method);
    var layerChain, layer, i;
    // Consider multiple router instances
    if (ctx.matched) {
      // Since matched is always an array
      // Apply is similar to concat
      ctx.matched.push.apply(ctx.matched, matched.path);
    } else {
      // Match the path
      ctx.matched = matched.path;
    }
    // Current route
    ctx.router = router;
    // If there is a matching route
    if(! matched.route)return next();
    // Layer where methods and paths match
    var matchedLayers = matched.pathAndMethod;
    // Last layer
    var mostSpecificLayer = matchedLayers[matchedLayers.length - 1];
    //
    ctx._matchedRoute = mostSpecificLayer.path;

    // If the layer has a name
    if (mostSpecificLayer.name) {
      ctx._matchedRouteName = mostSpecificLayer.name;
    }
    // Compose operation for matched layer

    // Update capture params routerName

    // For example, we use multiple routes.
    // => ctx.capture, ctx.params, ctx.routerName => layer Stack[s]
    // => ctx.capture, ctx.params, ctx.routerName => next layer Stack[s]
    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();
      });
      returnmemo.concat(layer.stack); } []);return compose(layerChain)(ctx, next);
  };

  dispatch.router = this;

  return dispatch;
};
Copy the code

We know that the essence of route matching is that the actual route matches the defined path.

So the middleware generated by routes is actually considering this matching processing.

We can see from the return value

=> Dispatch method.

This dispacth approach is essentially the minimalist approach we talked about earlier.

function dispatch(ctx, next) {}
Copy the code

It’s almost the same.

We know that stack currently stores multiple Layer instances.

And based on the path matching, we know that

A back-end path, which can simply be classified as an HTTP method, matches the path definition.

For example: / name / : id

This time comes a request /name/3

Is it a match? (params = {id: 3})

But what if the request method is get? /name/:id is a post.

In this case, although the path matches, the actual match is not complete.

Prototype method match

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];

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

    if (layer.match(path)) {
      // If the paths match
      matched.path.push(layer);
      // matched medium press layer

      if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) {
        // Check method
        matched.pathAndMethod.push(layer);
        // Press layer in both path and method
        if (layer.methods.length) matched.route = true;
        // Prove that there is no supported method. If route is true, middleware processing is skipped}}}return matched;
};
Copy the code

Look at the match method.

Determine the layaer in the stack.

In the matched object returned

Path property: Only the path matches.

The pathAndMethod attribute: Only HTTP methods match the path.

Route attribute: method length that requires layer is not 0(there are defined methods).


So in dispatch we first

ctx.matched = matched.path

Get the layer of path matching

The actual middleware handles layer with HTTP methods and path matching

In this case. In fact, middleware is just an array

It can be stacked multidimensional or one-dimensional.

If a route matches

Ctx. _matchedRoute represents its path.

Here ctx._matchedroute is the last layer of the method and path matching array.

I’m sure I’ll take the last one and you know why. Multiple paths, except for the current one, always return the last one in the next middleware process.

Finally, the matching layers are combined

For example, if you have multiple layers, you also have multiple stacks

// For example, we use multiple routes. // => ctx.capture, ctx.params, ctx.routerName => layer Stack[?s] // => ctx.capture, ctx.params, ctx.routerName => next layer Stack[?s]Copy the code

The running order is equivalent to flattening the stack of multiple Layer instances and adding CTX properties before each layer instance for use.

Finally, use the flattened array together with compose.

Notice that middleware is just a bunch of arrays.

But using the CTX property before each layer instance is a good idea.

Operations on middleware such as prefix. Is the constant adjustment of the internal stack position properties.

AllowedMethods method

Router.prototype.allowedMethods = function(options) {
  options = options || {};
  var implemented = this.methods;
  // Return a middleware for app.use registration.
  return function allowedMethods(ctx, next) {
    return next().then(function() {
      var allowed = {};
      // Check whether ctx.status is 404
      console.log(ctx.matched, ctx.method, implemented);

      if(! ctx.status || ctx.status ===404) {
        // routes method generated ctx.matched
        // Is the filtered layer matching group
        ctx.matched.forEach(function(route) {
          route.methods.forEach(function(method) {
            allowed[method] = method;
          });
        });

        var allowedArr = Object.keys(allowed);
        // Implement route matching
        if(! ~implemented.indexOf(ctx.method)) {// Bit operator ~(-1) === 0! 0 == true
          // the options argument throws an error if it is true
          // This can handle the upper intermediate price
          // The default is to raise an HttpError
          if (options.throw) {
            var 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 {
            // Otherwise run out of 501
            // 501=> The server does not implement the method
            ctx.status = 501;
            ctx.set("Allow", allowedArr.join(","));
          }
          // If allowed
        } else if (allowedArr.length) {
          // Perform operations on the options request.
          // Options requests are similar to GET requests, but only headers without a body.
          // query
          if (ctx.method === "OPTIONS") {
            ctx.status = 200;
            ctx.body = "";
            ctx.set("Allow", allowedArr.join(","));
          } else if(! allowed[ctx.method]) {// if the method is allowed
            if (options.throw) {
              var notAllowedThrowable;
              if (typeof options.methodNotAllowed === "function") {
                notAllowedThrowable = options.methodNotAllowed(); // set whatever the user returns from their function
              } else {
                notAllowedThrowable = new HttpError.MethodNotAllowed();
              }
              throw notAllowedThrowable;
            } else {
              // The 405 method is not allowed
              ctx.status = 405;
              ctx.set("Allow", allowedArr.join(",")); }}}}}); }; };Copy the code

This method basically adds these state controls 404 405 501 to our routing middleware by default.

We can also unify the processing in high level middleware as well.

The bitwise operator +indexOf is also a common usage.


The full text summary

At this point the entire KOA-Router source code is basically resolved.

Although the source of the Router has many methods not written in this article, but most of them are to provide layer instance method connections to the upper layer, welcome to github link from the source view.

In general can absorb the point may be quite a lot.

If you read the whole thing.

  1. You should be more comfortable with koa Middleware.
  2. I believe that you should have some understanding of the koA-Router source code architecture concrete method implementation.
  3. Learn how to read source code, build test cases, and understand input and output.

My blog is zwkang.com

Source address (annotated parsing version) KoA-Router branch