HTTP URL.Path Router - Rivet

jopen 10年前發布 | 15K 次閱讀 Rivet Web框架

Rivet 以 Go 語言實現的 HTTP 路由管理器。

路由是 WEB 開發中的核心功能之一。

Rivet 綜合實際需求提供了一個可擴展性極強的路由功能。

特性:
    ? 基于 Trie 路由匹配.
    ? 豐富的匹配規則, 靜態路由, 參數路由, 尾部通配, 可選尾斜線
    ? 支持參數路由匹配期轉換
    ? 支持注入 (Martini 注入變種), 方便維護上下文
    ? 支持過濾器
    ?深度解耦, 方便二次開發

深度解耦是 Rivet 的設計目標. Rivet 開放了各個核心環節, 從 Trie, Node, Filter, Params, Context 直至 Router 都可深度定制. hostrouter 就是基于 rivet.Trie 的二次開發實例.

簡潔

Rivet 使用常規風格.

示例: 復制到本地運行此代碼, 然后后點擊 這里

package mainimport (
    "io"
    "net/http"

    "github.com/typepress/rivet")// 常規風格 handlerfunc HelloWord(rw http.ResponseWriter, req *http.Request) {
    io.WriteString(rw, "Hello Word")}/**帶參數的 handler.params 是從 URL.Path 中提取到的參數.params 的另一種風格是 PathParams/Scene. 參見 Scene.*/func Hi(params rivet.Params, rw http.ResponseWriter) {
    io.WriteString(rw, "Hi "+params.Get("who")) // 提取參數 who}func main() {

    // 新建路由管理器
    mux := rivet.NewRouter(nil) // 下文解釋參數 nil

    // 注冊路由
    mux.Get("/", HelloWord)
    mux.Get("/:who", Hi) // 參數名設定為 "who"

    // rivet.Router 符合 http.Handler 接口
    http.ListenAndServe(":3000", mux) }

上例中 "/" 是無參數路由. "/:who" 是有參數路由, 參數名為 "who".

訪問 "/" 輸出:

Hello Word

訪問 "/Boy" 輸出:

Hi Boy

訪問 "/Girl" 輸出:

Hi Girl

訪問 "/news/sports" 會得到 404 NotFound 頁面.

以 api.github.com 真實路由為例:

mux.Get("/users/:user/events", Events)mux.Get("/users/:user/events/orgs/:org", Events)

因為都用 Events 函數作為 handler,  可以這樣寫:

func Events(params rivet.Params, rw http.ResponseWriter) {
    user := params.Get("owner")
    if user == "github" {
        // 用戶 github 很受歡迎, 需要特別處理
        // do something
        return 
    }

    // 因為兩個路由 path 都用 Events 處理, 可根據參數進行區分
    org := params.Get("org")
    if org != "" {
        // 對應 "/users/:user/events/orgs/:org" 的處理
        return
    }

    // 對應 "/users/:user/events" 的處理}

事實上 api.github.com 路由很多, 分開用不同的 handler 處理才是好方法:

mux.Get("/users/:user/events", userEvents)mux.Get("/users/:user/events/orgs/:org", userOrgEvents)

提示: 如果 Params 類型不適合您, 請看 Scene 部分.

貪心匹配

通常 Router 庫都能支持靜態路由, 參數路由, 可選尾部斜線等, Rivet 也同樣支持, 而且做的更好. 下面這些路由并存, 同樣能正確匹配:

"/",
"/**",
"/hi",
"/hi/**",
"/hi/path/to",
"/hi/:name/to",
"/:name",
"/:name/path/?",
"/:name/path/to",
"/:name/path/**",
"/:name/**",

當 URL.Path 為

"/xx/zzz/yyy"

"/:name/** 會被匹配, 它的層級比較深, 這符合貪心匹配原則. 使用者有可能困惑, 因為 "/**""/:name/**" 都可以匹配 "/xx/zzz/yyy". 記住貪心匹配原則, 否則避免這種用法即可. 后文會詳細介紹路由風格.

注入

rivet.Context 支持注入(Injector), 有三個關鍵方法:

    // MapTo 以 t 為 key 把變量 v 關聯到 context. 相同 t 值只保留一個.
    MapTo(v interface{}, t uint)

    // Get 以類型標識 t 為 key, 返回關聯到 context 的變量.
    Get(t uint) interface{}

    // Map 自動提取 v 的類型標識作為 t, 調用 MaptTo. 通常使用 Map.
    Map(v interface{})

現實中會有一些需求, 比如服務器對不同用戶在相同 URL.Path 下有不同響應, 也就是用戶角色控制. 使用注入后會很簡單.

// 用戶角色控制示意, 簡單的定義為 stringtype Role string/**在 handler 函數中加上 rivet.Context 參數即可用注入標記用戶角色,*/func UserRole(c rivet.Context) {
    // Context.Request() 返回 *http.Request
    req := c.Request()

    // 通常根據 session 確定用戶角色.
    session := req.Cookie("session").Value

    /**    這里只是示意代碼, 現實中的邏輯更復雜.    用注入函數 Map, 把用戶角色關聯到上下文.    */
    switch session {
    default: // 游客
        c.Map(Role(""))

    case "admin": // 管理員
        c.Map(Role("admin"))

    case "signOn": // 已經登錄
        c.Map(Role("signOn"))
    }}/**DelComments 刪除評論, role 參數由前面的 UserRole 注入上下文.*/func DelComments(role Role, params rivet.Params, rw http.ResponseWriter) {
    if role == "" {
        // 拒絕游客
        rw.WriteHeader(http.StatusForbidden)
        return
    }

    if role == "admin" {
        // 允許 admin
        // do delete
        return
    }

    // 其他角色,需要更多的判斷
    // do something}func main() {
    // ...
    //注冊路由:
    mux.Get("/del/comments/:id", UserRole, DelComments)
    // ...}

這個例子中, "/del/comments/:id" 被匹配后, 先執行 UserRole, 把用戶角色關聯到 Context, 因為 UserRole 沒有對 http.ResponseWriter 進行寫操作, DelComments 會被執行. Rivet 負責傳遞 DelComments 需要的參數 UserRole 等. DelComments 獲得 role 變量進行相應的處理, 完成角色控制.

提示: 如果 Rivet 發現 ResponseWriter 寫入任何內容, 認為響應已經完成, 不再執行后續 handler

定制

事實上, 上例中的 UserRole 很多地方都要用, 每次注冊路由都帶上 UserRole 很不方便. 通常在路由匹配之前執行 UserRole. 可以這樣用:

// 定義自己的 rivet.Context 生成器func MyRiveter(rw http.ResponseWriter, req *http.Request) rivet.Context {
    c := new(rivet.NewContext(rw, req))
    // 先執行角色控制
    UserRole(c)
    return c}func main() {

    // 使用 MyRiveter
    mux := rivet.NewRouter(MyRiveter)

    mux.Get("/del/comments/:id", DelComments)

    http.ListenAndServe(":3000", mux)}

方法也很多, 這只是最簡單的一種.

提示: 善用 Filter 可真正起到濾器請求的作用.

深度解耦

解耦使應用能切入到路由執行流程中的每一個環節, 達到高度定制. Rivet 在不失性能的前提下, 對解耦做了很多努力. 了解 Rivet 的類型和接口有助于深度定制路由流程.

  • Params 保存 URL.Path 中的參數

  • Filter 檢查/轉換 URL.Path 參數, 亦可過濾請求.

  • Node 保存 handler, 每個 Node 都擁唯一 id.

  • Trie 匹配 URL.Path, 調用 Filter, 調用 Params 生成器. 匹配到的 Trie.id 和 Node.id 是對應的.

  • Context 維護上下文, 處理 handler. 內置 Rivet 實現了它.

  • Router 路由管理器, 把上述對象聯系起來, 完成路由功能.

他們是如何解耦:

Params 無其它依賴, 有 PathParams 風格可選. 自定義 ParamsReceiver 定制.

Filter 接口無其它依賴. 自定義 FilterBuilder 定制.

Node 接口依賴 Context. 自定義 NodeBuilder 定制. 可以建立獨立的 Context.

Trie 是路由匹配的核心, 依賴 Filter, ParamsReceiver. 它們都可定制.

Context 接口依賴 ParamsReceiver, 這只是個函數, 最終也是無依賴的. Context 用了注入, 可能您的應用并不需要注入, 不用它即可.

Rivet 是內置的 Context 實現, 是個 struct, 可以擴展.

提示: 注入是透明的, 不使用不產生開銷, 使用了開銷也不高.

Router 依賴上述所有. 了解函數類型 NodeBuilder 和 Riveter 定制自己的 Node, Context.

定制使用大概分兩類:

底層: 直接使用 Trie, 構建自己的 Node, ParamsReceiver, Context, Router.
      需要了解 TypeIdOf, NewContext, NewNode, ParamsFunc, FilterFunc.
擴展: 使用 Router, 自定義 Context 生成器, 或者擴展 Rivet.

提示: 底層定制 Trie 需要 FilterBuilder, 如果 Path 參數無類型. 直接用 nil 替代, Trie 可以正常工作.

下文展示擴展定制方法.

自定義 Context 生成器:

// 自定義 Context 生成器, 實現真正的 http.Flusherfunc MyRiveter(rw http.ResponseWriter, req *http.Request) rivet.Context {

    // 構建自己的 http.Flusher
    rw = MyResponseWriterFlusher(rw) 
    c := new(rivet.NewContext(rw, req)) // 依舊使用 rivet.Rivet
    return c}

rivet 內置的 ResponseWriteFakeFlusher 是個偽 http.Flusher, 只是有個 Flush() 方法, 沒有真的實現 http.Flusher 功能. 如果您需要真正的 Flusher 需要自己實現.

實現自己的 Context 很容易, 善用 Next 和 Invoke 方法即可.

舉例:

/**擴展 Context, 實現 Before Handler.*/type MyContext struct {
    rivet.Context
    beforeIsRun true}/**MyContext 生成器使用:    reivt.Router(MyRiveter)*/func MyRiveter(res http.ResponseWriter, req *http.Request) rivet.Context {
    c := new(MyContext)
    c.Context = rivet.NewContext(res, req)
    return c}func (c *MyContext) Next() {
    if !beforeIsRun {
        // 執行 Before Handler
        // do something
        beforeIsRun = true
    }
    c.Context.Next()}// 觀察者模式func Observer(c rivet.Context) {
    defer func() {
        if err := recover(); err != nil {
            // 捕獲 panic
            // do something
            return
        }
        // 其他操作, 比如寫日志, 統計執行時間等等
        // do something
    }()
    c.Next()}/**調用 Context.Invoke, 插入執行另外的 handler.MyInvoke 插入執行 SendStaticFile, 這和直接調用 SendStaticFile 不同.這樣的 SendStaticFile 可以使用上下文關聯變量, 就像上文講的角色控制.而 MyInvoke 不必關心 SendStaticFile 所需要的參數, 那可以由別的代碼負責.*/func MyInvoke(c rivet.Context) {
    c.Invoke(SendStaticFile)}/**發送靜態文件, 參數 root 是前期代碼關聯好的.現實中簡單的改寫 req.URL.Path, 無需 root 參數也是可行的.*/func SendStaticFile(root http.Dir, rw http.ResponseWriter, req *http.Request) {
    // send ...}

路由風格

Rivet 對路由 pattern 支持豐富.

示例:

"/news/:cat"

可匹配:

"/news/sprots"
"/news/health"

示例:

"/news/:cat/:id"

可匹配:

"/news/sprots/9527"
"/news/health/1024"

上面的路由只有參數名, 數據類型都是 string. Rivet 還支持帶類型的 pattern.

示例:

"/news/:cat/:id uint"

":id uint" 表示參數名是 "id", 數據要符合 "uint" 的要求.

"uint" 是內置的 Filter class, 參見 FilterClass, 您可以注冊新的 class.

示例: 可選尾斜線

"/news/?"

可匹配:

"/news"
"/news/"

提示: "/?" 只能在尾部出現.

除了可選尾斜線, 路由風格可歸納為:

"/path/to/prefix:pattern/:pattern/:"

其中 "path", "to","prefix" 是占位符, 表示固定字符, 稱為定值. ":pattern" 格式為:

:name class arg1 arg2 argN

    以 ":" 開始, 以 " " 作為分隔符.
    第一段是參數名, 第二段是類型名, 后續為參數.

    示例: ":cat string 6"

    cat
        為參數名, 如果省略只驗證不提取參數, 形如 ": string 6"
    string
        為類型名, 可以自定義 class 注冊到 FilterClass 變量.
    6
        為長度參數, 可以設置一個限制長度參數. 例如
        ":name string 5"
        ":name uint 9"
        ":name hex 32"

:name class
    提取參數, 以 "name" 為 key, 根據 class 對值進行合法性檢查.

:name
    提取參數, 不對值進行合法檢查, 值不能為空.
    如果允許空值要使用 ":name *". "*" 是個 class, 允許空值.

:
    不提取參數, 不檢查值, 允許空值, 等同于 ": *".
::
    只能用于模式尾部. 提取參數, 不檢查值, 允許空值, 參數名為 "*".
    例如:
        "/path/to/::"
    可匹配:
        "/path/to/",          "*" 為參數名, 值為 "".
        "/path/to/paths",     "*" 為參數名, 值為 "paths".
        "/path/to/path/path", "*" 為參數名, 值為 "path/path".
*
    "*" 可替代 ":" 作為開始定界符, 某些情況 "*" 更符合習慣, 如:
    "/path/to*"
    "/path/to/**"

提示: 含有 class 才會生成 Filter, 否則被優化處理

您也許注意到, 這里沒有正則, 自定義 Filter 怎么執行由定制者控制, 包括正則.

提示: 正則中不能含有 "/".

Scene

路由風格支持帶類型的參數, Filter 檢查時可能會對參數進行類型轉換, interface{} 方便保存轉換后的結果, 后續代碼無需再次檢查轉換, 所以 Params 定義成這樣:

type Params map[string]interface{}

一些應用場景無轉換需求, 只需要簡單定義:

type PathParams map[string]string

是的, 這種場景也很普遍. Scene 就是為此準備的 Context.

Scene 的使用很簡單:

package mainimport (
    "io"
    "net/http"

    "github.com/typepress/rivet")/**params 類型為 PathParams, 是從 URL.Path 中提取到的參數.PathParams 和 Scene 配套使用.*/func Hi(params rivet.PathParams, rw http.ResponseWriter) {
    io.WriteString(rw, "Hi "+params["who"])}func main() {

    // 傳遞 NewScene, 采用 PathParams 風格
    mux := rivet.NewRouter(rivet.NewScene)

    mux.Get("/:who", Hi) // 參數名設定為 "who"

    http.ListenAndServe(":3000", mux) }

提示: PathParams 和 NewScene 配套使用. 事實上 Context 采用 All-In-One 的設計方式, 具體實現不必未完成所有接口, 使用方法配套即可.

Acknowledgements

Inspiration from Julien Schmidt's httprouter, about Trie struct.

Trie 算法和結構靈感來自 Julien Schmidt's httprouter.

項目主頁:http://www.baiduhome.net/lib/view/home/1409928506932

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!