cJavaSwingandroidiosjstitlejavanodeshellgolanggolangRD
Koa.jsNodejsJSTJconodejs4.0nodejs10koa.jsconodejs10co

什么是洋葱模型呢?

nextcontextdom

有人说,不就是递归吗,这有什么?确实,从原理角度来看确实是递归但是又有很多不同:

nextnextcontext
golang
koagolang
koagolang

话不多说先看看执行效果

package main

import (
	"fmt"

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

func main() {
	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)
	if err != nil {
		fmt.Println(err)
	}
}
Durationtest1test/:var/p
    $ curl localhost:8080/test/test/p?c=Hello World

    test1
    test map[var:test]
    test1
    2020-12-17 15:04:28 +0800 CST /test/test/p?c=Hello%20World Request cost:  0.040756 ms
test/:var/ptest1next

洋葱模型的核心代码

koa
func compose(ctx *Context, handlers []Handler) func(error) {
	currentPoint := int32(0) // 记录当前执行到第几个函数
	var next func(err error)    // 缓存退栈函数,
	var cbMax = len(handlers)    // 记录最大执行次数

        // 整个框架最核心的代码,退栈执行函数
	next = func(err error) {
		_ctx := ctx    // 闭包缓存当前执行上下文指针
		_router := handlers    // 缓存需要执行的函数
		bFound := false     // 当前轮训是否匹配到已执行函数。保证每次只执行一个函数

		for int(currentPoint) < cbMax && bFound == false {
			bFound = true
                        // 取出当前需要执行的函数
			currRouterHandler := _router[currentPoint] 
			atomic.AddInt32(¤tPoint, 1)
                        // 执行,并将退栈执行函数传入。
			currRouterHandler(err, _ctx, next) 
		}
	}
        // 返回退栈函数
	return next
}
compose
Handler
// 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)

	if req.Body != nil {
		body, err = ioutil.ReadAll(req.Body)
	}
        // 创建一个新的执行上下文
	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
        // 将符合规则的中间件进行压栈
	for _, middleware := range app.middlewares {
		if ok := compare(middleware.path, ctx.Path, false); ok {
			routerHandler = append(routerHandler, middleware.handler)
		}
	}
        // 将符合规则的路由函数进行压栈
	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...)
		}
	}
        // 开始执行因为需要保障每一个中间件和路由函数访问同一个执行上下文,所以传入的是指针。
	fb := compose(ctx, routerHandler)
	fb(err)
}
compose

总结

通过洋葱模型,使我们可以更容易的监控整个路由请求的全链路,并且可以在中间件做更多的事情。比如统计响应耗时:

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")
}

可以看到,在洋葱模型的支持下,我们可以将整个中间件逻辑写在一起即可。而next就是具体的路由逻辑,我们并不需要关心它具体做了什么,只需要关心我们自己需要做什么。这样可以将同一个业务逻辑的代码聚合到一起,更容易维护,不是么?

最后