在 Go Echo Web 框架中設置 HTML 嵌套樣板

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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/echoEcho 套件下載到 $GOPATH 裡。

執行 hello world

透過 go run main.go 指令啟動應用程序,然後可以通過瀏覽器或 curl 指令來訪問 http://localhost:1323。

啟動 Echo 服務器。

啟動 Echo 服務器。


嘗試發送請求。

嘗試發送請求。

返回一個 JSON 回應

在現實生活中構建RESTful API時,客戶端通常希望接收的是一個 JSON 回應而不是一堆字串。試試在 main.go 中修改一些 Go 代碼。

main.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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,但在此之前,讓我們重新規劃這個示例項目的結構。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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 實例中所配置的一個的樣板。這三個參數分別是

  1. HTTP 狀態碼
  2. 樣板名稱
  3. 以及可以在樣板中使用的 data 物件

view/home.html

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{{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 物件讀取 namemsg 並放到 <title><h1> 元素中。

使用嵌套樣板

在以上的設置中,每個 HTML 樣板都有一整套 HTML 代碼,其中許多代碼都是重複的。若果使用嵌套樣板便可避免重複的代碼,可以更輕鬆地維護項目。

目前的 TemplateRegistrytemplates 的欄位包含所有樣板文件。而在新設置中,我們將其設置為映射(map),每組鍵/值都是一套特定的 HTML 嵌套樣板文件。

我們在項目中添加了一些文件,新的項目結構如下。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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

下面的代碼參考了由 rand99 的創建的 gist

main.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
{{define "base.html"}}
  <!DOCTYPE html>
  <html>
    <head>
      <title>{{template "title" .}}</title>
    </head>
    <body>
      {{template "body" .}}
    </body>
  </html>
{{end}}

template 語句告訴樣板引擎它應該在樣板集中尋找 {{title}}{{body}} 的定義,在 home.htmlabout.html 中可以找到。

view/about.html

1
2
3
4
5
6
7
8
{{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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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 代碼庫上找到。