Implementation principles of koA Router
Two purposes of this paper
- Learn about path-to-regexp usage
- 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
- 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
- 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.
- Initializes the Layer instance
- Register it with the Router instance.
We know when we call the Router instance.
There are usually two steps to using middleware
- use(router.routes())
- 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.
- You should be more comfortable with koa Middleware.
- I believe that you should have some understanding of the koA-Router source code architecture concrete method implementation.
- 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