From my work experience, I can see that I am destined to be a non-serious and non-mainstream front-end engineer. From the beginning, I wrote network applications in C language, and developed Swing with Java. Finally, I caught up with the mobile wave and worked on Android for one year, but I passed the ios development engineer certification. Then, I kept the title of front-end engineer hanging and wrote Java, Node, and shell. Now the company, it turns out, everybody’s writing golang, and that’s not the right tendon. Began to study golang, as the saying goes, know yourself and know your enemy, win every battle, only to understand the RD weapon, in order to better (tear) cooperation.

Koa.js is a very well known framework for Nodejs technology, created by TJ, in which the Onion model and CO in Nodejs4.0 – Nodejs10 solve a lot of problems. Although I don’t like koa.js. But I also love the Onion model, CO and its middleware mechanisms. Although after nodeJS10 + co eggs have no use.

What is the Onion model?

The request comes in, layer by layer, through the middleware to execute the next function into the next middleware that you set up, and can be passed down through the context object, and when it reaches the last middleware, it goes back up to where it started. Is it a bit like a dom event capture bubble? That’s true.

Some people say, isn’t it just recursion? What’s the big deal? Yes, it is recursive from a principle point of view but there are many differences:

  • Because we need to go through the third parameternextPerforms the next callback execution, and executes thenextAfter the next callback of the call is executed, the rest of the code can continue to execute, and the function of the next call can be executed anywhere. So this implementation is not just recursive. Because it’s not the same function, and it’s in a different location.
  • Passing parameters and references, we need to ensure that the entire execution process, the various intermediate functions are executedcontextPoints to the same context object.

In order to realize the above two functions, first of all, a language must have one basic capability — closure. Golang can theoretically implement the Onion model because it has closure. Is it actually possible? And it works.

koaOne that implements the Onion model and middleware mechanismsgolangThe framework

I’ll just call it KOA. Most of golang’s frameworks currently support middleware, but middleware only supports sequential execution, not the Onion model. So. This is a bit of a challenge.

Without further ado, let’s see how it works

package main

import (
	"fmt"

	"github.com/ryouaki/koa"
	"github.com/ryouaki/koa/example/plugin"
)

func main(a) {
	app := koa.New()

	app.Use(plugin.Duration)
	app.Use("/".func(err error, ctx *koa.Context, next koa.NextCb) {
		fmt.Println("test1")
		next(err)
		fmt.Println("test1")
	})

	app.Get("/test/:var/p".func(err error, ctx *koa.Context, next koa.NextCb) {
		fmt.Println("test", ctx.Params)
		ctx.SetData("test", ctx.Query["c"] [0])
		next(nil)},func(err error, ctx *koa.Context, next koa.NextCb) {
		ctx.Write([]byte(ctx.GetData("test"). (string)))
	})

	err := app.Run(8080)
	iferr ! =nil {
		fmt.Println(err)
	}
}
Copy the code

This code includes middleware Duration to output the request time, test1 middleware to observe the onion model execution order, and test/:var/p as our test interface

    $ curl localhost:8080/test/test/p? c=Hello World test1test map[var:test]
    test1
    2020-12-17 15:04:28 +0800 CST /test/test/p? C =Hello%20World Request cost: 0.040756 msCopy the code

From the log we can see that our test/:var/p interface function is executed as if nested in test1 middleware next. So how does that work? Isn’t it fun?

The core code of the Onion model

The entire KOA core code is less than 300 lines, of which only 20 lines are the core code that implements the Onion model:

func compose(ctx *Context, handlers []Handler) func(error) {
	currentPoint := int32(0) // Record the number of functions currently executed
	var next func(err error)// Cache unstack function,
	var cbMax = len(handlers)    // Record the maximum number of executions

        // The core code of the entire framework, unstack the execution function
	next = func(err error) {
		_ctx := ctx    // Closure caches the current execution context pointer
		_router := handlers    // Cache the functions that need to be executed
		bFound := false     // Whether the current rotation matches the executed function. Ensure that only one function is executed at a time

		for int(currentPoint) < cbMax && bFound == false {
			bFound = true
                        // Retrieve the current function that needs to be executedCurrRouterHandler: = _router [currentPoint] atomic. AddInt32 (problem tPoint,1)
                        // execute, and pass in the unstack execution function.
			currRouterHandler(err, _ctx, next) 
		}
	}
        // returns the unstack function
	return next
}
Copy the code

Explain how to use itcomposeImplementing the Onion model

In order to ensure the execution order of each middleware and ensure the consistency of execution context, we need to stack all handlers that comply with the request routing rules.

// ServeHTTP interface func
func (app *Application) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	var err error = nil
	var body []uint8 = nil

	method := strings.ToLower(req.Method)

	ifreq.Body ! =nil {
		body, err = ioutil.ReadAll(req.Body)
	}
        Create a new execution context
	ctx := &Context{
		Header:   req.Header,
		Res:      w,
		Req:      req,
		URL:      req.RequestURI,
		Path:     req.URL.Path,
		Query:    formatQuery(req.URL.Query()),
		Body:     body,
		Method:   method,
		Status:   200,
		IsFinish: false,
		data:     make(map[string]interface{})},var routerHandler []Handler
        // Push the middleware that meets the rule
	for _, middleware := range app.middlewares {
		if ok := compare(middleware.path, ctx.Path, false); ok {
			routerHandler = append(routerHandler, middleware.handler)
		}
	}
        // Push the routing function that matches the rule
	for _, router := range app.route[ctx.Method] {
		if ok := compare(router.path, ctx.Path, true); ok {
			ctx.MatchURL = router.path
			ctx.Params = formatParams(router.path, ctx.Path)
			routerHandler = append(routerHandler, router.handler...) }}Since we need to ensure that every middleware and routing function accesses the same execution context, we pass in Pointers.
	fb := compose(ctx, routerHandler)
	fb(err)
}
Copy the code

The pressed function is pushed into compose for unstack execution. It’s that simple.

conclusion

With the Onion model, we can more easily monitor the entire link of routing requests and do more in the middleware. For example, statistical response time:

package plugin

import (
	"fmt"
	"time"

	"github.com/ryouaki/koa"
)

// Duration func
func Duration(err error, ctx *koa.Context, next koa.NextCb) {
	startTime := time.Now()
	next(nil)
	d := time.Now().Sub(startTime)
	fmt.Println(time.Date(startTime.Year(),
		startTime.Month(),
		startTime.Day(),
		startTime.Hour(),
		startTime.Minute(),
		startTime.Second(), 0, time.Local),
		ctx.URL,
		"Request cost: ".float64(d)/float64(time.Millisecond), "ms")}Copy the code

As you can see, with the support of the Onion model, you can write the entire middleware logic together. Next is the specific routing logic, and we don’t need to care what it does, just what we need to do. This brings together code from the same business logic and makes it easier to maintain, right?

The last

Find a wave of follow and star project addresses