An overview of the
Recently, I was developing my own Web framework Bingo. I also checked some routing toolkits on the market, but some of them failed to meet my needs.
For example, I’d like to get some features of the Laravel framework:
-
Fast route lookup
-
Dynamic Routing Support
-
Middleware support
-
Routing Group Support
The fastest is Httprouter, which I modified a few months ago: Adapting HttprOuter to support middleware, but at the time it was coupled to the Bingo framework and middleware did not support interception, here I needed to pull it out and make a third-party package that could be referenced directly without relying on the Bingo framework
So I used HttprOuter as the base package and modified it to support the above features.
Warehouse address: Bingu-router
The usage has been very clear in the README of the project, so I will not repeat it here. If you have any questions or needs, please send me an issue
It is also recommended to read the readme. md article before reading this article, otherwise there may be some places to read it.
The transformation is mainly divided into two parts
- The first part is going to
httprouter
The routing treetree
On the mounthandle
Change the method to our custom structure
Httprouter can be seen in 5.2 Router request Routing
In simple terms, it is to construct a prefix tree for the paths of all interfaces, and put the paths with the same prefix in the branch of a tree, so that the search speed can be accelerated. Each leaf represents a route method found, and a method is mounted.
However, the prefix tree can only mount methods and cannot add any additional information, so the first step is to mount a custom structure to the prefix tree so that we can find mounted middleware, route prefixes, etc
-
The second part is to implement middleware functions. If you just iterate over an array of middleware, you can’t intercept some operations.
For example, if we want to implement a middleware to verify that the user is logged in, and the user who is not logged in will return an error message, then if we iterate through an array of middleware, the final route will still be executed
In order to achieve the interception function, I referred to the implementation principle of Pipeline function in Laravel, and realized a Pipeline object to achieve the above effect
Began to transform
1. Part 1
-
In our plan, we plan to implement routing group, middleware, routing prefix functions, so we need to customize the structure as follows:
/ / routing type Route struct { path string / / path targetMethod TargetHandle // The method to execute method string // The access type is get, post or whatever name string / / routing mount []*Route / / zi lu by middleware []MiddlewareHandle // Mounted middleware prefix string // Route prefix. This prefix is valid only for sub-routes } Copy the code
The targetMethod is the handle method originally mounted in the prefix tree. We need to change all handle methods mounted in the Node structure of the original tree.go file to Route.
Major changes, and there is nothing to pay special attention to, I will not repeat here, can see the tree. Go file
-
In the README, the Route registration operation uses the chain of responsibility mode. Each method returns a pointer to the current object, and the chain operation can be implemented by methods such as Get and Post
-
Implements the routing group function
Through routing group, we can set common prefixes and middleware for sub-routes. In Laravel, multiple routes form a group object, but here, I directly use the way of sub-routing to change the group object into a common route, and the route under the group object is the sub-route of the current route
Write a Mount() method to make way for adding child routes:
// Mount the child route, where only the route in the callback is placed func (r *Route) Mount(rr func(b *Builder)) *Route { builder := new(Builder) rr(builder) // Iterate over all child routes created under this route and place the route on the parent route for _, route := range builder.routes { r.mount = append(r.mount, route) } return r } Copy the code
The Builder contains an array of routes. In Builder mode, give the Builder a NewRoute method, and make each route created by this method under the Routes attribute of Builder:
func (b *Builder) NewRoute() *Route { r := NewRoute() b.routes = append(b.routes, r) return r } Copy the code
Just drop the pointer into the Builder at creation time
Httprouter’s Handle method can be used to inject the Route object into the Router.
-
Inject the route into the router
Httprouter: Get,Post, or any other method ends up calling router.handle (), passing in an access method, a path, and the corresponding method, which we’ve just changed to a route
So here we pass in the access method, path, and route object, and let middleware and route prefixes take effect at injection time
Write an injection method Mount:
Go var prefix []string // The prefix of the current route increases with each layer. Var middlewares map[string][]MiddlewareHandle Func (r *Router) Mount(routes... *Route) { prefix = []string{} middlewares = make(map[string][]MiddlewareHandle) for _, Func (r *Router) MountRoute(route * route) {// MountRoute(route * route) {// SetMiddlewares (currentPointer, route) // The current path is all prefix arrays joined together, P := getPrefix(currentPointer) + route.path Prefix = append(prefix, route.prefix) if route.method! = "" && p ! {r.handler (route.method, p, route) {r.handler (route.method, p, route)} If len(route.mount) > 0 {for _, subRoute := range route.mount {currentPointer += 1 R.ountroute (subRoute)}} else {if currentPointer > 0 {currentPointer -= 1 Func getPrefix(current int) string {if len(prefix) > current-1 && len(prefix)! = 0 {return strings.Join(prefix[:current], "")} return ""} Func setMiddlewares(current int, route * route) {key := "p" + strconv.Itoa(currentPointer) for _, V := range route.middleware {middlewares[key] = append(middlewares[key], v)} for I := 0; i < currentPointer; i++ { key = "p" + strconv.Itoa(i) if list, ok := middlewares[key]; ok { for _, v := range list { route.middleware = append(route.middleware, v) } } } } ```Copy the code
Define global variables first:
-
Prefix Records the route prefix of each layer. The key is the route layer number, and the value is the route prefix
-
Middlewares records each layer of routing middleware. The key identifies the number of routing layers, and the value is the entire set of middleware in that layer
-
CurrentPointer identifies the current routing layer and uses it to fetch data belonging to the current routing layer from the two variables above
Then, after each iteration, the corresponding prefix and middleware group are stored in global variables, recursively called, and then appropriate data is extracted. Finally, Handle method is executed to inject into the router
The above is just a brief introduction to how to make, specific can directly see the code, there is no difficulty.
2. Part II
We’re building servers that implement the ServeHttp method so that when a request comes in, it’s going to go to the method we’ve defined. The ServeHttp defined by the original HttprOuter can be seen here
The process is to take the current URL, find the leaf along the prefix tree, and execute it directly. We changed the leaf to the Route structure above, so that when it is found, it needs to execute its middleware first, and then its targetMethod method
In the middleware, we can’t just use a for loop to iterate through the execution because we can’t intercept the request, and we end up in targetMethod with no post-effect. How do we do that?
Laravel uses a method called Pipeline, or Pipeline, to pass each context sequentially through each middleware, without passing it down if it is intercepted
The specific idea can be seen here
The source code for my implementation is here
The following code is used:
Here’s what we expect:
// Build the pipe and execute the middleware to finally reach the route
new(Pipeline).Send(context).Through(route.middleware).Then(func(context *Context) {
route.targetMethod(context)
})
Copy the code
First establish a pipeline structure:
typeStruct {send *Context} struct {send *Context} struct {send *Context through []MiddlewareHandleCopy the code
The Send() and Through() methods inject content into them, but I won’t go into that here
The main Then methods are:
Func (p *Pipeline) Then(thenFunc (context * context)) {// execute in order // willthenVar m MiddlewareHandle m = func(c *Context, next func(c *Context)) {then(c)
next(c)
}
p.through = append(p.through, m)
p.Exec()
}
Copy the code
The then method wraps the final execution of the method as middleware, adding it to the end of the pipe, and then executes the Exec method to start sending objects through the pipe from scratch:
func (p *Pipeline) Exec(a) {
if len(p.through) > p.current {
m := p.through[p.current]
p.current += 1
m(p.send, func(c *Context) {
p.Exec()
})
}
}
Copy the code
Take the middleware to which the current pointer is pointing, move the current pointer to the next middleware, and execute the middleware you just fetched, and the callback passed in next, which recursively executes this logic, executes the next middleware,
This allows us to control whether the next() method is pre-positioned or post-positioned in our code
It’s not much code, but the implementation is interesting, thanks to Laravel
I just rewrote part of other people’s things, thanks to open source, benefit a lot, and hang up your own Web framework Bingo, beg star, welcome PR!