GoFrame 高級特性-HOOK事件回調(diào)

2022-04-15 11:17 更新

?ghttp.Server?提供了事件回調(diào)注冊功能,類似于其他框架的中間件功能,相比較于中間件,事件回調(diào)的特性更加簡單。

?ghttp.Server?支持用戶對于某一事件進(jìn)行自定義監(jiān)聽處理,按照?pattern?方式進(jìn)行綁定注冊(?pattern?格式與路由注冊一致)。支持多個方法對同一事件進(jìn)行監(jiān)聽,?ghttp.Server?將會按照路由優(yōu)先級及回調(diào)注冊順序進(jìn)行回調(diào)方法調(diào)用。同一事件時先注冊的?HOOK?回調(diào)函數(shù)優(yōu)先級越高。 相關(guān)方法如下:

func (s *Server) BindHookHandler(pattern string, hook string, handler HandlerFunc) error
func (s *Server) BindHookHandlerByMap(pattern string, hookmap map[string]HandlerFunc) error

當(dāng)然域名對象也支持事件回調(diào)注冊:

func (d *Domain) BindHookHandler(pattern string, hook string, handler HandlerFunc) error
func (d *Domain) BindHookHandlerByMap(pattern string, hookmap map[string]HandlerFunc) error

支持的?Hook?事件列表:

  • ?ghttp.HookBeforeServe ?

在進(jìn)入/初始化服務(wù)對象之前,該事件是最常用的事件,特別是針對于權(quán)限控制、跨域請求等處理。

  • ?ghttp.HookAfterServe ?

在完成服務(wù)執(zhí)行流程之后。

  • ?ghttp.HookBeforeOutput ?

向客戶端輸出返回內(nèi)容之前。

  • ?ghttp.HookAfterOutput ?

向客戶端輸出返回內(nèi)容之后。

具體調(diào)用時機(jī)請參考圖例所示。

回調(diào)優(yōu)先級

由于事件的綁定也是使用的路由規(guī)則,因此它的優(yōu)先級和 路由管理-路由規(guī)則 章節(jié)介紹的優(yōu)先級是一樣的。

但是事件調(diào)用時和路由注冊調(diào)用時的機(jī)制不一樣,同一個路由規(guī)則下允許綁定多個事件回調(diào)方法,該路由下的事件調(diào)用會按照優(yōu)先級進(jìn)行調(diào)用,假如優(yōu)先級相等的路由規(guī)則,將會按照事件注冊的順序進(jìn)行調(diào)用。

關(guān)于全局回調(diào)

我們往往使用綁定?/*?這樣的?HOOK?路由來實現(xiàn)全局的回調(diào)處理,這樣是可以的。但是?HOOK?執(zhí)行的優(yōu)先是最低的,路由注冊的越精確,優(yōu)先級越高,越模糊的路由優(yōu)先級越低,?/*?就屬于最模糊的路由。

為降低不同的模塊耦合性,所有的路由往往不是在同一個地方進(jìn)行注冊。例如用戶模塊注冊的?HOOK?(?/user/*?),它將會被優(yōu)先調(diào)用隨后才可能是全局的?HOOK?;如果僅僅依靠注冊順序來控制優(yōu)先級,在模塊多路由多的時候優(yōu)先級便很難管理。

業(yè)務(wù)函數(shù)調(diào)用順序

建議 相同的業(yè)務(wù)(同一業(yè)務(wù)模塊) 的多個處理函數(shù)(例如: A、B、C)放到同一個HOOK回調(diào)函數(shù)中進(jìn)行處理,在注冊的回調(diào)函數(shù)中自行管理業(yè)務(wù)處理函數(shù)的調(diào)用順序(函數(shù)調(diào)用順序: A-B-C)。

雖然同樣的需求,注冊多個相同?HOOK?的回調(diào)函數(shù)也可以實現(xiàn),功能上不會有問題,但從設(shè)計的角度來講,內(nèi)聚性降低了,不便于業(yè)務(wù)函數(shù)管理。

ExitHook方法

當(dāng)路由匹配到多個?HOOK?方法時,默認(rèn)是按照路由匹配優(yōu)先級順序執(zhí)行?HOOK?方法。當(dāng)在?HOOK?方法中調(diào)用?Request.ExitHook?方法后,后續(xù)的?HOOK?方法將不會被繼續(xù)執(zhí)行,作用類似?HOOK?方法覆蓋。

接口鑒權(quán)控制

事件回調(diào)注冊比較常見的應(yīng)用是在對調(diào)用的接口進(jìn)行鑒權(quán)控制/權(quán)限控制。該操作需要綁定?ghttp.HookBeforeServe?事件,在該事件中會對所有匹配的接口請求(例如綁定?/*?事件回調(diào)路由)服務(wù)執(zhí)行前進(jìn)行回調(diào)處理。當(dāng)鑒權(quán)不通過時,需要調(diào)用?r.ExitAll()?方法退出后續(xù)的服務(wù)執(zhí)行(包括后續(xù)的事件回調(diào)執(zhí)行)。

此外,在權(quán)限校驗的事件回調(diào)函數(shù)中執(zhí)行?r.Redirect*?方法,又沒有調(diào)用?r.ExitAll()?方法退出業(yè)務(wù)執(zhí)行,往往會產(chǎn)生?http multiple response writeheader calls?錯誤提示。因為?r.Redirect*?方法會往返回的?header?中寫入?Location?頭;而隨后的業(yè)務(wù)服務(wù)接口往往會往?header?寫入?Content-Type/Content-Length?頭,這兩者有沖突造成的。

中間件與事件回調(diào)

中間件(?Middleware?)與事件回調(diào)(?HOOK?)是?GF?框架的兩大流程控制特性,兩者都可用于控制請求流程,并且也都支持綁定特定的路由規(guī)則。但兩者區(qū)別也是非常明顯的。

  1. 首先,中間件側(cè)重于應(yīng)用級的流程控制,而事件回調(diào)側(cè)重于服務(wù)級流程控制;也就是說中間件的作用域僅限于應(yīng)用,而事件回調(diào)的“權(quán)限”更強(qiáng)大,屬于?Server?級別,并可處理靜態(tài)文件的請求回調(diào)。
  2. 其次,中間件設(shè)計采用了“洋蔥”設(shè)計模型;而事件回調(diào)采用的是特定事件的鉤子觸發(fā)設(shè)計。
  3. 最后,中間件相對來說靈活性更高,也是比較推薦的流程控制方式;而事件回調(diào)比較簡單,但靈活性較差。

Request.URL與Request.Router

?Request.Router?是匹配到的路由對象,包含路由注冊信息,一般來說開發(fā)者不會用到。 ?Request.URL?是底層請求的?URL?對象(繼承自標(biāo)準(zhǔn)庫?http.Request?),包含請求的?URL?地址信息,特別是?Request.URL.Path?表示請求的?URI?地址。

因此,假如在服務(wù)回調(diào)函數(shù)中使用的話,?Request.Router?是有值的,因為只有匹配到了路由才會調(diào)用服務(wù)回調(diào)方法。但是在事件回調(diào)函數(shù)中,該對象可能為?nil?(表示沒有匹配到服務(wù)回調(diào)函數(shù)路由)。特別是在使用事件回調(diào)對請求接口鑒權(quán)的時候,應(yīng)當(dāng)使用?Request.URL?對象獲取請求的?URL?信息,而不是?Request.Router?。

靜態(tài)文件事件

如果僅僅是提供?API?接口服務(wù)(包括前置靜態(tài)文件服務(wù)代理如?nginx?),不涉及到靜態(tài)文件服務(wù),那么可以忽略該小節(jié)。

需要注意的是,事件回調(diào)同樣能夠匹配到符合路由規(guī)則的靜態(tài)文件訪問(靜態(tài)文件特性在?gf?框架中是默認(rèn)開啟的,我們可以使用?WebServer?相關(guān)配置來手動關(guān)閉。

例如,我們注冊了一個?/*?的全局匹配事件回調(diào)路由,那么?/static/js/index.js?或者?/upload/images/thumb.jpg?等等靜態(tài)文件訪問也會被匹配到,會進(jìn)入到注冊的事件回調(diào)函數(shù)中進(jìn)行處理。

我們可以在事件回調(diào)函數(shù)中使用?Request.IsFileRequest()?方法來判斷該請求是否是靜態(tài)文件請求,如果業(yè)務(wù)邏輯不需要靜態(tài)文件的請求事件回調(diào),那么在事件回調(diào)函數(shù)中直接忽略即可,以便進(jìn)行選擇性地處理。

事件回調(diào)示例

示例1,基本使用

package main

import (
	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/net/ghttp"
	"github.com/gogf/gf/v2/os/glog"
)

func main() {
	// 基本事件回調(diào)使用
	p := "/:name/info/{uid}"
	s := g.Server()
	s.BindHookHandlerByMap(p, map[string]ghttp.HandlerFunc{
		ghttp.HookBeforeServe:  func(r *ghttp.Request) { glog.Println(ghttp.HookBeforeServe) },
		ghttp.HookAfterServe:   func(r *ghttp.Request) { glog.Println(ghttp.HookAfterServe) },
		ghttp.HookBeforeOutput: func(r *ghttp.Request) { glog.Println(ghttp.HookBeforeOutput) },
		ghttp.HookAfterOutput:  func(r *ghttp.Request) { glog.Println(ghttp.HookAfterOutput) },
	})
	s.BindHandler(p, func(r *ghttp.Request) {
		r.Response.Write("用戶:", r.Get("name"), ", uid:", r.Get("uid"))
	})
	s.SetPort(8199)
	s.Run()
}

當(dāng)訪問 http://127.0.0.1:8199/john/info/10000 時,運行?WebServer?進(jìn)程的終端將會按照事件的執(zhí)行流程打印出對應(yīng)的事件名稱。

示例2,相同事件注冊

package main

import (
	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/net/ghttp"
)

// 優(yōu)先調(diào)用的HOOK
func beforeServeHook1(r *ghttp.Request) {
	r.SetParam("name", "GoFrame")
	r.Response.Writeln("set name")
}

// 隨后調(diào)用的HOOK
func beforeServeHook2(r *ghttp.Request) {
	r.SetParam("site", "https://goframe.org")
	r.Response.Writeln("set site")
}

// 允許對同一個路由同一個事件注冊多個回調(diào)函數(shù),按照注冊順序進(jìn)行優(yōu)先級調(diào)用。
// 為便于在路由表中對比查看優(yōu)先級,這里講HOOK回調(diào)函數(shù)單獨定義為了兩個函數(shù)。
func main() {
	s := g.Server()
	s.BindHandler("/", func(r *ghttp.Request) {
		r.Response.Writeln(r.Get("name"))
		r.Response.Writeln(r.Get("site"))
	})
	s.BindHookHandler("/", ghttp.HookBeforeServe, beforeServeHook1)
	s.BindHookHandler("/", ghttp.HookBeforeServe, beforeServeHook2)
	s.SetPort(8199)
	s.Run()
}

執(zhí)行后,終端輸出的路由表信息如下:

SERVER  | ADDRESS | DOMAIN  | METHOD | P | ROUTE |        HANDLER        |    MIDDLEWARE
|---------|---------|---------|--------|---|-------|-----------------------|-------------------|
  default |  :8199  | default | ALL    | 1 | /     | main.main.func1       |
|---------|---------|---------|--------|---|-------|-----------------------|-------------------|
  default |  :8199  | default | ALL    | 2 | /     | main.beforeServeHook1 | HOOK_BEFORE_SERVE
|---------|---------|---------|--------|---|-------|-----------------------|-------------------|
  default |  :8199  | default | ALL    | 1 | /     | main.beforeServeHook2 | HOOK_BEFORE_SERVE
|---------|---------|---------|--------|---|-------|-----------------------|-------------------|

手動訪問 http://127.0.0.1:8199/ 后,頁面輸出內(nèi)容為:

set name
set site
GoFrame
https://goframe.org

示例3,改變業(yè)務(wù)邏輯

package main

import (
	"fmt"
	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/net/ghttp"
)

func main() {
	s := g.Server()
	// 多事件回調(diào)示例,事件1
	pattern1 := "/:name/info"
	s.BindHookHandlerByMap(pattern1, map[string]ghttp.HandlerFunc{
		ghttp.HookBeforeServe: func(r *ghttp.Request) {
			r.SetParam("uid", 1000)
		},
	})
	s.BindHandler(pattern1, func(r *ghttp.Request) {
		r.Response.Write("用戶:", r.Get("name"), ", uid:", r.Get("uid"))
	})

	// 多事件回調(diào)示例,事件2
	pattern2 := "/{object}/list/{page}.java"
	s.BindHookHandlerByMap(pattern2, map[string]ghttp.HandlerFunc{
		ghttp.HookBeforeOutput: func(r *ghttp.Request) {
			r.Response.SetBuffer([]byte(
				fmt.Sprintf("通過事件修改輸出內(nèi)容, object:%s, page:%s", r.Get("object"), r.GetRouterString("page"))),
			)
		},
	})
	s.BindHandler(pattern2, func(r *ghttp.Request) {
		r.Response.Write(r.Router.Uri)
	})
	s.SetPort(8199)
	s.Run()
}

通過事件1設(shè)置了訪問?/:name/info?路由規(guī)則時的?GET?參數(shù);通過事件2,改變了當(dāng)訪問的路徑匹配路由?/{object}/list/{page}.java?時的輸出結(jié)果。執(zhí)行之后,訪問以下?URL?查看效果:

示例4,回調(diào)執(zhí)行優(yōu)先級

package main

import (
	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/net/ghttp"
)

func main() {
	s := g.Server()
	s.BindHandler("/priority/show", func(r *ghttp.Request) {
		r.Response.Writeln("priority service")
	})

	s.BindHookHandlerByMap("/priority/:name", map[string]ghttp.HandlerFunc{
		ghttp.HookBeforeServe: func(r *ghttp.Request) {
			r.Response.Writeln("/priority/:name")
		},
	})
	s.BindHookHandlerByMap("/priority/*any", map[string]ghttp.HandlerFunc{
		ghttp.HookBeforeServe: func(r *ghttp.Request) {
			r.Response.Writeln("/priority/*any")
		},
	})
	s.BindHookHandlerByMap("/priority/show", map[string]ghttp.HandlerFunc{
		ghttp.HookBeforeServe: func(r *ghttp.Request) {
			r.Response.Writeln("/priority/show")
		},
	})
	s.SetPort(8199)
	s.Run()
}

在這個示例中,我們往注冊了3個路由規(guī)則的事件回調(diào),并且都能夠匹配到路由注冊的地址?/priority/show?,這樣我們便可以通過訪問這個地址來看看路由執(zhí)行的順序是怎么樣的。

執(zhí)行后我們訪問 http://127.0.0.1:8199/priority/show ,隨后我們看到頁面輸出以下信息:

/priority/show
/priority/:name
/priority/*any
priority service

示例5,允許跨域請求

?HOOK?和中間件都能實現(xiàn)跨域請求處理,我們這里使用?HOOK?來實現(xiàn)簡單的跨域處理。 首先我們來看一個簡單的接口示例:

package main

import (
    "github.com/gogf/gf/v2/frame/g"
    "github.com/gogf/gf/v2/net/ghttp"
)

func Order(r *ghttp.Request) {
    r.Response.Write("GET")
}

func main() {
    s := g.Server()
    s.Group("/api.v1", func(group *ghttp.RouterGroup) {
        group.GET("/order", Order)
    })
    s.SetPort(8199)
    s.Run()
}

接口地址是 http://localhost:8199/api.v1/order ,當(dāng)然這個接口是不允許跨域的。我們打開一個不同的域名,例如:百度首頁(正好用了?jQuery?,方便調(diào)試),然后按?F12?打開開發(fā)者面板,在?console?下執(zhí)行以下?AJAX?請求:

$.get("http://localhost:8199/api.v1/order", function(result){
    console.log(result)
});

結(jié)果如下:


返回了不允許跨域的錯誤,接著我們修改一下測試代碼,如下:

package main

import (
	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/net/ghttp"
)

func Order(r *ghttp.Request) {
	r.Response.Write("GET")
}

func main() {
	s := g.Server()
	s.Group("/api.v1", func(group *ghttp.RouterGroup) {
		group.Hook("/*any", ghttp.HookBeforeServe, func(r *ghttp.Request) {
			r.Response.CORSDefault()
		})
		group.GET("/order", Order)
	})
	s.SetPort(8199)
	s.Run()
}

我們增加了針對于路由?/api.v1/*any?的綁定事件?ghttp.HookBeforeServe?,該事件將會在所有服務(wù)執(zhí)行之前調(diào)用,該事件的回調(diào)方法中,我們通過調(diào)用?CORSDefault?方法使用默認(rèn)的跨域設(shè)置允許跨域請求。該綁定的事件路由規(guī)則使用了模糊匹配規(guī)則,表示所有?/api.v1?開頭的接口地址都允許跨域請求。

返回剛才的百度首頁,再次執(zhí)行請求?AJAX?請求,這次便成功了: 



以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號