Files
build-web-application-with-…/en/13.2.md
2016-09-27 16:12:42 -03:00

9.5 KiB
Raw Blame History

13.2 Customizing routers

HTTP routing

The HTTP routing component is responsible for mapping HTTP requests to a corresponding function or struct method. The router takes two key pieces of information from incoming requests:

-The user requested path (for example, /user/123,/article/123), and any query strings or parameters that come with it (for example, ?id=11) -The HTTP request method (GET, POST, PUT, and DELETE, PATCH, etc.)

The router then forwards the request to the handler function (controller layer) that has been registered with that particular HTTP method and path.

Default routing implementation

In section 3.4, we introduced Go's http package in detail, which included how to design and implement routing. Here, we take another look at an example that illustrates the routing process:

func fooHandler(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
}

http.Handle("/foo", fooHandler)

http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})

log.Fatal(http.ListenAndServe(":8080", nil))

The example above calls http's default mux called DefaultServeMux, implicitly specified by the nil parameter in the call to http.ListenAndServe. The http.Handle function takes two parameters: the first parameter is the resource you want users to access, specified by its URL path (which is stored in r.URL.Path) and the second argument binds a handler function with this path. The Router has two main jobs:

  • To add and store routing information
  • To forward requests to a handler function for processing

By default, Go routes are handled with http.Handle and http.HandleFunc types, registered by default through the underlying DefaultServeMux.Handle(pattern string, handler Handler) function. This function maps resource paths to handlers and stores them in a map[string]muxEntry map. This is the first job that we mentioned above.

When the application is running, the Go server listens to a port. When it receives a tcp connection, it uses a Handler to process it. As aforementioned, since the Handler in the example above is nil, the default router http.DefaultServeMux is used. Using the map of previously stored routes, DefaultServeMux.ServeHTTP will dispatch the request to the first handler with a matching path. This is the router's second job.

for k, v := range mux.m {
	if !pathMatch(k, path) {
		continue
	}
	if h == nil || len(k) > n {
		n = len(k)
		h = v.h
	}
}

Routing with Beego

At present, most Go web applications base their routing on http's default router, however this has several limitations:

-Does not support dynamic routes with parameters, such as the/user/:UID -Does not have good support for REST. The access methods cannot be restricted; for instance in the above example, when users access /foo, they can use the GET, POST, DELETE, and HEAD HTTP methods, among others.

  • In large apps, routing rules can become repetitive and cumbersome. Personally, I've developed simple web APIs composed of nearly thirty routing rules when in fact, these rules could have been further simplified using method structs.

The Beego framework's router is designed to overcome these limitations, taking the REST paradigm into consideration and simplifying the storing and forwarding of routes and requests.

Storing a routing

For the previously mentioned restriction point, we must first solve the arguments supporting the need to use regular, second and third point we passed a viable alternative to solve, REST method corresponds to a struct method to go, and then routed to the struct instead of a function, so that when the forward routing method can be performed according to different methods.

Based on the above ideas, we designed two data types controllerInfo( save path and the corresponding struct, here is a reflect.Type type ) and ControllerRegistor(routers are used to save user to add a slice of routing information, and the application of the framework beego information )

type controllerInfo struct {
	regex          *regexp.Regexp
	params         map[int]string
	controllerType reflect.Type
}

type ControllerRegistor struct {
	routers     []*controllerInfo
	Application *App
}

ControllerRegistor external interface function has

func(p *ControllerRegistor) Add(pattern string, c ControllerInterface)

Detailed implementation is as follows:

func (p *ControllerRegistor) Add(pattern string, c ControllerInterface) {
	parts := strings.Split(pattern, "/")

	j := 0
	params := make(map[int]string)
	for i, part := range parts {
		if strings.HasPrefix(part, ":") {
			expr := "([^/]+)"

			//a user may choose to override the defult expression
			// similar to expressjs: /user/:id([0-9]+)

			if index := strings.Index(part, "("); index != -1 {
				expr = part[index:]
				part = part[:index]
			}
			params[j] = part
			parts[i] = expr
			j++
		}
	}

	//recreate the url pattern, with parameters replaced
	//by regular expressions. then compile the regex

	pattern = strings.Join(parts, "/")
	regex, regexErr := regexp.Compile(pattern)
	if regexErr != nil {

		//TODO add error handling here to avoid panic
		panic(regexErr)
		return
	}

	//now create the Route
	t := reflect.Indirect(reflect.ValueOf(c)).Type()
	route := &controllerInfo{}
	route.regex = regex
	route.params = params
	route.controllerType = t

	p.routers = append(p.routers, route)

}

Static routing

Above we achieve the realization of dynamic routing, Go the http package supported by default static file handler FileServer, because we have implemented a custom router, then the static files also need to set their own, beego static folder path stored in the global variable StaticDir, StaticDir is a map type to achieve the following:

func (app *App) SetStaticPath(url string, path string) *App {
	StaticDir[url] = path
	return app
}

Applications can use the static route is set to achieve the following manner:

beego.SetStaticPath("/img", "/static/img")

Forwarding route

Forwarding route in the routing is based ControllerRegistor forwarding information, detailed achieve the following code shows:

// AutoRoute
func (p *ControllerRegistor) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	defer func() {
		if err := recover(); err != nil {
			if !RecoverPanic {
				// go back to panic
				panic(err)
			} else {
				Critical("Handler crashed with error", err)
				for i := 1; ; i += 1 {
					_, file, line, ok := runtime.Caller(i)
					if !ok {
						break
					}
					Critical(file, line)
				}
			}
		}
	}()
	var started bool
	for prefix, staticDir := range StaticDir {
		if strings.HasPrefix(r.URL.Path, prefix) {
			file := staticDir + r.URL.Path[len(prefix):]
			http.ServeFile(w, r, file)
			started = true
			return
		}
	}
	requestPath := r.URL.Path

	//find a matching Route
	for _, route := range p.routers {

		//check if Route pattern matches url
		if !route.regex.MatchString(requestPath) {
			continue
		}

		//get submatches (params)
		matches := route.regex.FindStringSubmatch(requestPath)

		//double check that the Route matches the URL pattern.
		if len(matches[0]) != len(requestPath) {
			continue
		}

		params := make(map[string]string)
		if len(route.params) > 0 {
			//add url parameters to the query param map
			values := r.URL.Query()
			for i, match := range matches[1:] {
				values.Add(route.params[i], match)
				params[route.params[i]] = match
			}

			//reassemble query params and add to RawQuery
			r.URL.RawQuery = url.Values(values).Encode() + "&" + r.URL.RawQuery
			//r.URL.RawQuery = url.Values(values).Encode()
		}
		//Invoke the request handler
		vc := reflect.New(route.controllerType)
		init := vc.MethodByName("Init")
		in := make([]reflect.Value, 2)
		ct := &Context{ResponseWriter: w, Request: r, Params: params}
		in[0] = reflect.ValueOf(ct)
		in[1] = reflect.ValueOf(route.controllerType.Name())
		init.Call(in)
		in = make([]reflect.Value, 0)
		method := vc.MethodByName("Prepare")
		method.Call(in)
		if r.Method == "GET" {
			method = vc.MethodByName("Get")
			method.Call(in)
		} else if r.Method == "POST" {
			method = vc.MethodByName("Post")
			method.Call(in)
		} else if r.Method == "HEAD" {
			method = vc.MethodByName("Head")
			method.Call(in)
		} else if r.Method == "DELETE" {
			method = vc.MethodByName("Delete")
			method.Call(in)
		} else if r.Method == "PUT" {
			method = vc.MethodByName("Put")
			method.Call(in)
		} else if r.Method == "PATCH" {
			method = vc.MethodByName("Patch")
			method.Call(in)
		} else if r.Method == "OPTIONS" {
			method = vc.MethodByName("Options")
			method.Call(in)
		}
		if AutoRender {
			method = vc.MethodByName("Render")
			method.Call(in)
		}
		method = vc.MethodByName("Finish")
		method.Call(in)
		started = true
		break
	}

	//if no matches to url, throw a not found exception
	if started == false {
		http.NotFound(w, r)
	}
}

Getting started

After the design is based on the routing can solve the previously mentioned three restriction point, using a method as follows:

The basic use of a registered route:

beego.BeeApp.RegisterController("/", &controllers.MainController{})

Parameter registration:

beego.BeeApp.RegisterController("/:param", &controllers.UserController{})

Are then matched:

beego.BeeApp.RegisterController("/users/:uid([0-9]+)", &controllers.UserController{})