koa router implementation principle
Two purposes of this article
- Understanding the use of path-to-regexp
- koa-router source parsing
path-to-regexp
Introduction to path-to-regexp usage.
How can it be used to match identified routes?
Think about what we can do if we want to identify routes?
The most intuitive is definitely a path string match
'/string' => '/string'
We can do some feedback when the routes match/string exactly.For example, execute a callback, and so on.
We can also take advantage of the regular matching feature
In this way, the sub-matching pattern is obviously more versatile and has more matching paths
For example, for path:
/^\/string\/.*?\/xixi$ // => '/string/try/xixi'
path-to-regexp is one such tool
Consider that if we want to parse and match paths, we need to write our own regular expressions.This achieves the matching effect.
Can I write?
Sure, but it's too time consuming.
path-to-regexp can help us do this easily.
Introduction to some api of path-to-regexp
how to use it ???
Primary api
const pathToRegexp = require('path-to-regexp') // pathToRegexp(path, keys?, options?) // pathToRegexp.parse(path) // pathToRegexp.compile(path)
// pathToRegexp(path, keys?, options?) // path can be a string/string array/regular expression // Keys holds the array of keys found in the path // options are populations of some matching rules such as whether they are fully matched splitters, etc.
path-to-regexp api demo
//a demo If we want to match some key values properly eg: /user/:name How do we achieve this The front is a perfect match, and the back is a regular grouping to extract values eg: /\/user\/((?!\/).*?)\/?$/.exec('/user/zwkang') Find matching regular string returns an array/no value returns a null This is what pathToRegexp does.Generate the required regular expression matches.There are some packaging operations, of course, but that's what it's all about.
pathToRegexp('/user/:name').exec('/user/zwkang') path option ? Indicates availability or absence pathToRegexp('/:foo/:bar?').exec('/test') pathToRegexp('/:foo/:bar?').exec('/test/route') *Representatives can come anywhere +for one or more Look closely and you can see that these words are almost identical to the quantifiers in the regular You can also store keys by sequence subscripts when matching unnamed parameters Regular expressions are also supported parse method Generate a matching tokens array for path That is the keys array above Method applies to string type Compile method Passing in a path with compile returns a function that can be populated to generate a value that matches the path pathToRegexp.compile('/user/:id')({id: 123}) => "/user/123" Applicable to strings pathToRegexp.tokensToRegExp(tokens, keys?, options?) pathToRegexp.tokensToFunction(tokens) You can see by name A regular expression that converts an array of tokens A function that converts a tokens array into a compile method
Steps for using one stroke
pathToRegexp =Return=> regexp parse =analysis=> path =matching tokens=> keys token compile => path => generator function => value => full path string
koa-router
I don't know if you've ever used koa-router
notic: Notice the current koa-router maintenance permission change issue
The router implementation is also essentially a Regular-based access path matching.
If koa native code is used
Example:
Match Path/simple returns a body string with body {name:'zwkang'}
A simple example, such as
Suppose we match routes using a simple middleware match ctx.url app.use(async (ctx, next) => { const url = ctx.url if(/^\/simple$/i.test(url)) { ctx.body = { name: 'ZWkang' } } else { ctx.body = { errorCode: 404, message: 'NOT FOUND' } ctx.status = 404 } return await next() }) //Test Code describe('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(); }); }) 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(); }); }) })
The above pattern of url implementation by ourselves is the same. Single matching, if multiple matching, even matching parameters, requires regular writing to be considered.
Disadvantages, simpler, simpler setting method, weak function
If we use koa-router
// A simple use 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(); }); })
Off-topic: app.callback()
Some explanations of the test code above
callback is koa's operating mechanism.What does the method represent?Represents the process of its setup
And our common listen method is actually the only step that calls http.createServer(app.callback())
Let's see what this koa-router actually does
Pre-knowledge
As we can see from the simple examples above, we can understand the operating mechanism of koa and the internal middleware processing mode.
Start with demo for analysis
Instance methods called when koa is called include
router.allowedMethods ===> router.routes ===> router.get
Consider that because it's a koa, use call, we can be sure it's a standard koa middleware pattern
The function returned is similar to
async (ctx, next) => { // Processing routing logic // Processing business logic }
The opening comments of the source code give us some basic usage
We can make a simple extraction
router.verb() specifies the corresponding function based on the http method
For example, router.get().post().put()
The.All method supports all http methods
When a route matches, ctx._matchedRoute can get the route here, and if it is named, it can get the route name ctx._matchedRouteName here
querystring(?xxxx) is not considered when requesting matches
Allow use of named functions
Quickly locate routes during development
* router.get('user', '/users/:id', (ctx, next) => { * // ... * }); * * router.url('user', 3); * // => "/users/3"
Allow multiple routes
* 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" } * } * );
Allow Nested Routing
* 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());
Allow routing prefix matching
var router = new Router({ prefix: '/users' }); router.get('/', ...); // responds to "/users" router.get('/:id', ...); // responds to "/users/:id"
Capture named parameters to add to ctx.params
router.get('/:category/:title', (ctx, next) => { console.log(ctx.params); // => { category: 'programming', title: 'how-to-node' } });
Code Integration Analysis
Some clever coding
- Separation of responsibilities, upper-level routers do http-level method status es, and routers middlewares-related processing.Low-level Layer.js focuses on routing path processing
- middlerware design
You might want to start with a layer file.
layer.js
As mentioned earlier, this file is primarily used to handle operations on the path-to-regexp Library
There are only about 300 lines in the file, so the direct intercept method is explained in detail.
layer constructor
function Layer(path, methods, middleware, opts) { this.opts = opts || {}; this.name = this.opts.name || null; // Named Route this.methods = []; // Allow methods // [{ name: 'bar', prefix: '/', delimiter: '/', optional: false, repeat: false, pattern: '[^\\/]+?' }] this.paramNames = []; this.stack = Array.isArray(middleware) ? middleware : [middleware]; // middleware heap // Initialization parameters // Tips: the second parameter of forEach can pass this // The forEach push array can be used later to determine the end element using the array [l-1] // The push method returns the number of elements after the array push // External method parameter passed in methods.forEach(function(method) { var l = this.methods.push(method.toUpperCase()); // If a GET request supports a HEAD request if (this.methods[l-1] === 'GET') { this.methods.unshift('HEAD'); } }, this); // ensure middleware is a function // Ensure that 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); // Route this.path = path; // Generating regular expressions for paths using pathToRegExp // Arrays associated with params fall back into our this.paramNames // this.regexp An array generated for cutting this.regexp = pathToRegExp(path, this.paramNames, this.opts); debug('defined route %s %s', this.methods, this.opts.prefix + this.path); };
We can focus on input and output.
Input: path, methods, middleware, opts
Output: Object properties include (opts, name, methods, paramNames, stack, path, regexp)
We've said before that layer s are processed to determine matches based on route path s, which is important for the connection library path-to-regexp.
The stack should match the middleware passed in.The stack is an array, so you can see that our path's corresponding route s allow more than one.
Let's focus next
What encapsulation does koa-router handle for us, based on path-to-regexp combining middleware with its own needs
The mount methods on the prototype chain are
params
// Get Routing 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; // Fill in key-value pairs } } // Returns a parameter key-value pair object return params; };
When the constructor is initialized, we generate this.regexp by passing in this.paramNames to fill out the param parsed from the path
Input: Path, Capture Group, Existing Parameter Group
Output: A parameter key-value pair object
Processing is very common.Because params correspond to captures in location.So you can cycle directly.
match
// Determine whether to match Layer.prototype.match = function (path) { return this.regexp.test(path); };
The first thing to look at is the input and return values
Input: path
Output: whether a boolean matches
We can see that this.regexp is an attribute value, proving that we have the ability to change this.regexp at any time to affect the return value of this function
captures
// Return parameter value Layer.prototype.captures = function (path) { if (this.opts.ignoreCaptures) return []; // Ignore capture and return null // match returns an array of matching results // From the regular rule, the result is a perfect 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 .....] };
Input: path path path
Output: Capture array
Return the entire capture group content
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 (typeof params != "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); // By default, replace is the default incoming key-value pair//match followed by the full url if (options && options.query) { // Is there query var replaced = new uri(replaced); // replaced.search(options.query); //Add query routing query return replaced.toString(); } return replaced; // Return URL String };
url method of layer instance
In fact, an example is/name/:id
After parsing, we get a params object of {id: xxx}
Can we reverse the actual url based on / name/:id and params objects?
This url method provides this 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 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 at the top, and the current fn.param does not exist.Insert [a, b] mid => [mid, a, b] // 2. [mid, a, b] mid2 => [mid, mid2, a, b] guarantees the order of params // Ensure before normal Middleware // Ensure that the order is params if (!fn.param || names.indexOf(fn.param) > x) { // Injecting middleware at the current time stack.splice(i, 0, middleware); return true; // Stop some iteration. } }); } return this; };
The purpose of this method is to add a processor for a single param to the current stack
It's actually an operation on the layer's stack
setPrefix
Layer.prototype.setPrefix = function(prefix) { // Calling setPrefix is equivalent to resetting some of the layer's constructs if (this.path) { this.path = prefix + this.path; this.paramNames = []; this.regexp = pathToRegExp(this.path, this.paramNames, this.opts); } return this; };
Prefix the current path and reset some of the current instance properties
safeDecodeURIComponent
function safeDecodeURIComponent(text) { try { return decodeURIComponent(text); } catch (e) { return text; } }
Ensure that safeDecodeURIComponent does not throw any errors
Layer concludes.
The stack of layer s mainly stores the actual middleware[s].
The main function is to design for pathToRegexp.
Provides the ability to make calls to higher-level routers.
Router
Router mainly responds to the upper koa framework (ctx, status, etc.) and links the lower layer instances.
Router Constructor
function Router(opts) { // Automatic new if (!(this instanceof Router)) { return new Router(opts); } this.opts = opts || {}; // The method is used to check the allowedMethod that follows this.methods = this.opts.methods || [ "HEAD", "OPTIONS", "GET", "PUT", "PATCH", "POST", "DELETE" ]; // Initialize http method this.params = {}; // Parameter key-value pairs this.stack = []; // Store Routing Instances }
methods.forEach(function(method) { // Attach all http method methods to the prototype Router.prototype[method] = function(name, path, middleware) { var middleware; // Compatibility 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 // It's mainly a way to set up a generic install middleware.(mark. tag: function) this.register(path, [method], middleware, { name: name }); // call chaining return this; }; });
Register Router prototype
Methods for HTTP methods, such as Router.prototype.get = xxx
It's easier and more accurate when we use instances
router.get('name', path, cb)
Middlewares can obviously be multiple here.For example, router.get(name, path, cb)
We can notice that the main thing here is to call another method
notic:
register method.And we can keep an eye on this method.Much like Layer instance initialization.
With doubt, we can enter the register method.
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, // Need to be explicitly declared as end name: opts.name, // The name of the route sensitive: opts.sensitive || this.opts.sensitive || false, // Case Sensitive Regular Plus i strict: opts.strict || this.opts.strict || false, // Non-capture grouping plus (?:) prefix: opts.prefix || this.opts.prefix || "", // Prefix character 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); // Current Router instance stack push single layer instance stack.push(route); return route; };
We can see that the entire register method is designed to register a single path.
Call the register method on forEach for multipaths.This is not uncommon in koa-router implementations.
Looking at the register method, our doubts are confirmed, and it turns out that most of the input is used to initialize the layer instance.
After initializing the layer instance, we place it in the stack under the router instance.
Processing judgment based on some opts.Not much is harmless and elegant.
That way we know how to use register s.
- Initialize layer instance
- Register it with the router instance.
We know when we call the router instance.
To use middleware, we often need to complete two steps
- use(router.routes())
- use(router.allowedMethods())
We know that a minimalist form of middleware call is always
app.use(async (ctx, next) => { await next() })
Whether we are koa-body or koa-router
Incoming app.use is always one
async (ctx, next) => { await next() }
Such a function meets the requirements of koa middleware.
With this in mind
We can explore the routes approach.
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 Path var path = router.opts.routerPath || ctx.routerPath || ctx.path; // matched has already been processed to get layer object hosting var matched = router.match(path, ctx.method); var layerChain, layer, i; // Consider multiple router instances if (ctx.matched) { // Because matched is always an array // apply here is similar to concat ctx.matched.push.apply(ctx.matched, matched.path); } else { // Matched Path ctx.matched = matched.path; } // Current Route ctx.router = router; // If a matching route exists if (!matched.route) return next(); // layer whose method and path match var matchedLayers = matched.pathAndMethod; // Last layer var mostSpecificLayer = matchedLayers[matchedLayers.length - 1]; // ctx._matchedRoute = mostSpecificLayer.path; // If a layer has a name if (mostSpecificLayer.name) { ctx._matchedRouteName = mostSpecificLayer.name; } // Matching layer s perform compose operations // update capture params routerName, etc. // For example, if 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(); }); return memo.concat(layer.stack); }, []); return compose(layerChain)(ctx, next); }; dispatch.router = this; return dispatch; };
We know that the essence of route matching is that the actual route matches the defined route.
So the middleware generated by routes is actually thinking about doing this matching.
From the return value we can see that
=> dispatch method.
This dispacth method is actually the simplest way we've said before.
function dispatch(ctx, next) {}
It can be said that there is little difference.
We know that stack currently stores multiple layer instances.
And based on the matching of paths, we can see that
A back-end path, which can be simply divided into http methods, matches the path definition.
For example: /name/:id
A request comes at this time/name/3
Is it a match?(params = {id: 3})
But what if the request method is a get? Define this / name/:id if it is a post.
At this point, although the paths match, they do not match exactly.
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 path matches matched.path.push(layer); // Press layer into matched if (layer.methods.length === 0 || ~layer.methods.indexOf(method)) { // Check Method matched.pathAndMethod.push(layer); // layer is pressed into both paths and methods if (layer.methods.length) matched.route = true; // Prove that there are no supported methods.Skip middleware processing after route is true } } } return matched; };
Take a look at this match method.
Judge layaer in stack.
In the returned matched object
Path property: Simply path matching.
Path AndMethod property: Just http method matches path.
route property: The length of the method requiring a layer is not zero (there is a defined method).)
So in dispatch we start with
ctx.matched = matched.path
Get a path-matching layer
The actual middleware handles layer with http method and path matching
In this case.In fact, the so-called middleware is an array
It may be stacked in a multidimensional or one-dimensional manner.
If a route matches
ctx._matchedRoute represents its path.
Here ctx._matchedRoute is the last layer of the method and path matching array.
Believe that the last one you know why.Multiple paths, except for the current process, always return to the last one in the next middleware process.
Finally, the layer s that match are grouped together
For example, if there are multiple layers, the layers also have multiple stack s
// For example, if we use multiple routes. // => ctx.capture, ctx.params, ctx.routerName => layer Stack[?s] // => ctx.capture, ctx.params, ctx.routerName => next layer Stack[?s]
The run order will be as shown above
This is equivalent to flattening stack s for multiple layer instances and adding a ctx attribute to use it before each layer instance.
Finally use compose to bring this flattened array together.
In fact, here we can notice that the so-called middleware is just an array.
But it's a good idea to use the ctx attribute before each layer instance here.
Operations on middleware, such as prefix, etc.Is the constant adjustment of the internal stack position property.
allowedMethods method
Router.prototype.allowedMethods = function(options) { options = options || {}; var implemented = this.methods; // Returns a middleware for app.use registration. return function allowedMethods(ctx, next) { return next().then(function() { var allowed = {}; // Judge ctx.status or status code to 404 console.log(ctx.matched, ctx.method, implemented); if (!ctx.status || ctx.status === 404) { // ctx.matched generated by routes method // Is the filtered layer matching group ctx.matched.forEach(function(route) { route.methods.forEach(function(method) { allowed[method] = method; }); }); var allowedArr = Object.keys(allowed); // Route Matching Implemented if (!~implemented.indexOf(ctx.method)) { // Bit Operator ~(-1) === 0! 0 == true // options parameter throw throw throw throws an error if true // This allows the upper middle price to be handled // The default is to throw 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 501 // 501=>Server unimplemented method ctx.status = 501; ctx.set("Allow", allowedArr.join(", ")); } // If permitted } else if (allowedArr.length) { // Operate on options requests. // options requests are similar to get requests, but requests have no body but only a head. // Common Terms Query Operation if (ctx.method === "OPTIONS") { ctx.status = 200; ctx.body = ""; ctx.set("Allow", allowedArr.join(", ")); } else if (!allowed[ctx.method]) { // If methods are 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 { // 405 Method Not Allowed ctx.status = 405; ctx.set("Allow", allowedArr.join(", ")); } } } } }); }; };
This method adds these state controls by default to our routing middleware 404 405 501.
We can also do this in high-level middleware.
It is also common to use the bitwise operator + indexOf.
Full text summary
So far, the koa-router source code has been basically parsed.
Although there are many ways to source Router that are not written out in this article, most of them are method connections that provide layer instances to the upper level. Welcome to the github link to view it from the source.
Overall, there may be a lot of points that can be absorbed.
If you have read the whole article.
- I'm sure you should be more comfortable with koa middleware.
- I'm sure you have a good idea of how to implement the source architecture of koa-router.
- Learn how to read the source code, build test cases, and learn about input and participating output.