在 Go Echo Web 框架中設置 HTML 嵌套樣板
      更新 @ 2019-12-13: 由 Go 1.13 開始,請使用內置的 Go Module 來管理套件。
Echo 是 Golang 裡用於構建 RESTful API 的輕型又完整的 Web 框架。它速度很快並且包含了不少中介軟體來處理整個 HTTP 請求與回應過程。在 Echo 框架裡您可以使用於任何樣板引擎來渲染 HTML,為了簡單起見這篇文章將會利用 Go 標準函式庫所提供的 html/template 套件。而在本文的最後,您可以找到一個演示了嵌套樣板的 Echo 示例項目。
如果您已經了解 Echo 的基本操作原理,您可以直接地跳到使用嵌套樣板的章節。
一個基本的 Echo 項目設置
在 $GOPATH 下創建項目文件夾
完整的項目代碼已放在 GitLab 上作參考。現在請先創建項目文件夾 _$GOPATH/src/gitlab.com/ykyuen/golang-echo-template-example_。
創建 main.go
在新創建的文件夾中,我們先從 Echo 官方網站複製 hello world 示例並創建 main.go 。
main.go
package main
import (
  "net/http"
  "github.com/labstack/echo"
)
func main() {
  e := echo.New()
  e.GET("/", func(c echo.Context) error {
    return c.String(http.StatusOK, "Hello, World!")
  })
  e.Logger.Fatal(e.Start(":1323"))
}
使用 dep 下載 Echo 套件
如果已經安裝了 dep,只需運行 dep init。有關如何使用 dep,請參閱以下文章。
如果不想使用 dep,那麼您也可以執行 go github.com/labstack/echo 把 Echo 套件下載到 $GOPATH 裡。
執行 hello world
透過 go run main.go 指令啟動應用程序,然後可以通過瀏覽器或 curl 指令來訪問 http://localhost:1323。
      
      返回一個 JSON 回應
在現實生活中構建RESTful API時,客戶端通常希望接收的是一個 JSON 回應而不是一堆字串。試試在 main.go 中修改一些 Go 代碼。
main.go
package main
import (
  "net/http"
  "github.com/labstack/echo"
)
func main() {
  e := echo.New()
  e.GET("/", func(c echo.Context) error {
    return c.String(http.StatusOK, "Hello, World!")
  })
  e.GET("/json", func(c echo.Context) error {
    return c.JSONBlob(
      http.StatusOK,
      []byte(`{ "id": "1", "msg": "Hello, Boatswain!" }`),
    )
  })
  e.Logger.Fatal(e.Start(":1323"))
}
返回一個 HTML
與返回 JSON 物件類似,我們只需要在 return 語句中呼叫另一個方法。
main.go
package main
import (
  "net/http"
  "github.com/labstack/echo"
)
func main() {
  e := echo.New()
  e.GET("/", func(c echo.Context) error {
    return c.String(http.StatusOK, "Hello, World!")
  })
  e.GET("/json", func(c echo.Context) error {
    return c.JSONBlob(
      http.StatusOK,
      []byte(`{ "id": "1", "msg": "Hello, Boatswain!" }`),
    )
  })
  e.GET("/html", func(c echo.Context) error {
    return c.HTML(
      http.StatusOK,
      "<h1>Hello, Boatswain!</h1>",
    )
  })
  e.Logger.Fatal(e.Start(":1323"))
}
以上只是兩個簡單的例子,Echo 有一些更方便的方法來返回 JSON 和 HTML。有興趣的話可參閱文檔。
使用樣板引擎渲染 HTML
正如本文開首提到,我們可以在返回 HTTP 回應時使用樣板引擎來渲染 HTML,但在此之前,讓我們重新規劃這個示例項目的結構。
golang-echo-template-example/ ├── handler/ # folder of request handlers │ └── home_handler.go ├── vendor/ # dependencies managed by dep │ ├── github.com/* │ └── golang.org/* ├── view/ # folder of html templates │ └── home.html ├── Gopkg.lock # dep config file ├── Gopkg.toml # dep config file └── main.go # programme entrypoint
main.go
package main
import (
  "html/template""io"
  "github.com/labstack/echo"
  "gitlab.com/ykyuen/golang-echo-template-example/handler"
)
// Define the template registry struct
type TemplateRegistry struct {
  templates *template.Template
}
// Implement e.Renderer interface
func (t *TemplateRegistry) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
  return t.templates.ExecuteTemplate(w, name, data)
}
func main() {
  // Echo instance
  e := echo.New()
  // Instantiate a template registry and register all html files inside the view folder
  e.Renderer = &TemplateRegistry{
    templates: template.Must(template.ParseGlob("view/*.html")),
  }
  // Route => handler
  e.GET("/", handler.HomeHandler)
  // Start the Echo server
  e.Logger.Fatal(e.Start(":1323"))
}
在這個新 main.go 中,我們定義了一個名為 TemplateRegistry 的型別並實現了 Renderer 介面。Renderer 是一個包裝 Render() 函數的簡單接口。之後在 main() 函數中實例化一個 TemplateRegistry 並加入所需的樣板。
另一方面,我們定義 HomeHandler 以便將邏輯保存在單獨的文件中。
handler/home_handler.go
package handler
import (
  "net/http"
  "github.com/labstack/echo"
)
func HomeHandler(c echo.Context) error {
  // Please note the the second parameter "home.html" is the template name and should
  // be equal to the value stated in the {{ define }} statement in "view/home.html"
  return c.Render(http.StatusOK, "home.html", map[string]interface{}{
    "name": "HOME",
    "msg": "Hello, Boatswain!",
  })
}
當 c.Render() 被呼叫時,它會執行在 TemplateRegistry 實例中所配置的一個的樣板。這三個參數分別是
- HTTP 狀態碼
 - 樣板名稱
 - 以及可以在樣板中使用的 data 物件
 
view/home.html
{{define "home.html"}}
  <!DOCTYPE html>
  <html>
    <head>
      <title>Boatswain Blog | {{index . "name"}}</title>
    </head>
    <body>
      <h1>{{index . "msg"}}</h1>
    </body>
  </html>
{{end}}
如上面的 define 語句中所述,樣板名稱命名為 home.html,它可以從 c.Render() 的 data 物件讀取 name 和 msg 並放到 <title> 和 <h1> 元素中。
使用嵌套樣板
在以上的設置中,每個 HTML 樣板都有一整套 HTML 代碼,其中許多代碼都是重複的。若果使用嵌套樣板便可避免重複的代碼,可以更輕鬆地維護項目。
目前的 TemplateRegistry 中 templates 的欄位包含所有樣板文件。而在新設置中,我們將其設置為映射(map),每組鍵/值都是一套特定的 HTML 嵌套樣板文件。
我們在項目中添加了一些文件,新的項目結構如下。
golang-echo-template-example/ ├── handler/ # folder of request handlers │ ├── home_handler.go # handler for home page │ └── about_handler.go # handler for about page ├── vendor/ # dependencies managed by dep │ ├── github.com/* │ └── golang.org/* ├── view/ # folder of html templates │ ├── base.html # base layout template │ ├── home.html # home page template │ └── about.html # about page template ├── Gopkg.lock # dep config file ├── Gopkg.toml # dep config file └── main.go # programme entrypoint
main.go
package main
import (
  "errors""html/template""io"
  "github.com/labstack/echo"
  "gitlab.com/ykyuen/golang-echo-template-example/handler"
)
// Define the template registry struct
type TemplateRegistry struct {
  templates map[string]*template.Template
}
// Implement e.Renderer interface
func (t *TemplateRegistry) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
  tmpl, ok := t.templates[name]
  if !ok {
    err := errors.New("Template not found -> " + name)
    return err
  }
  return tmpl.ExecuteTemplate(w, "base.html", data)
}
func main() {
  // Echo instance
  e := echo.New()
  // Instantiate a template registry with an array of template set
  // Ref: https://gist.github.com/rand99/808e6e9702c00ce64803d94abff65678
  templates := make(map[string]*template.Template)
  templates["home.html"] = template.Must(template.ParseFiles("view/home.html", "view/base.html"))
  templates["about.html"] = template.Must(template.ParseFiles("view/about.html", "view/base.html"))
  e.Renderer = &TemplateRegistry{
    templates: templates,
  }
  // Route => handler
  e.GET("/", handler.HomeHandler)
  e.GET("/about", handler.AboutHandler)
// Start the Echo server
  e.Logger.Fatal(e.Start(":1323"))
}
我們添加了一個由 AboutHandler 處理的 /about 新路徑,從上面加亮顯示的行中可以看到 templates 映射包含不同 HTML 頁面的樣板集,Render() 將 name 參數作為樣板映射 儲存鍵,以便它可以執行正確的樣板集。
view/base.html
{{define "base.html"}}
  <!DOCTYPE html>
  <html>
    <head>
      <title>{{template "title" .}}</title>
    </head>
    <body>
      {{template "body" .}}
    </body>
  </html>
{{end}}
template 語句告訴樣板引擎它應該在樣板集中尋找 {{title}} 和 {{body}} 的定義,在 home.html 和 about.html 中可以找到。
view/about.html
{{define "title"}}
  Boatswain Blog | {{index . "name"}}
{{end}}
{{define "body"}}
  <h1>{{index . "msg"}}</h1>
  <h2>This is the about page.</h2>
{{end}}
以下就是 AboutHanlder,它與 HomeHandler 沒有甚麼大分別。
handler/about_handler.go
package handler
import (
  "net/http"
  "github.com/labstack/echo"
)
func AboutHandler(c echo.Context) error {
  // Please note the the second parameter "about.html" is the template name and should
  // be equal to one of the keys in the TemplateRegistry array defined in main.go
  return c.Render(http.StatusOK, "about.html", map[string]interface{}{
    "name": "About",
    "msg": "All about Boatswain!",
  })
}
總結
以上就是如何在 Echo 框架下利用 Go 標準函式庫所提供的 html/template 套件來實現嵌套樣板的基本示例。通過適當的設置,我們可以為 Echo 開發更加個性化和方便的嵌套樣板,甚至可以使用其它不同的樣板引擎。
完整的例子可以在此 gitlab.com 代碼庫上找到。
