Go Web 程式設計--超詳細的模板庫應用指南

KevinYan發表於2020-03-04

模板庫介紹

如果你有過Web程式設計的經驗,那麼或多或少都聽說過或者使用過模板。簡而言之,模板是可用於建立動態內容的文字檔案。例如,你有一個網站導航欄的模板,其中動態內容的一部分可能是根據當前使用者是否登入顯示登入還是退出按鈕。

Go提供了兩個模板庫text/templatehtml/template。這兩個模板庫的使用方式是相同的,但是html/template包在渲染頁面模板時會在後臺進行一些編碼以幫助防止造成程式碼注入(XSS 攻擊)。

因為兩個模板庫都使用相同的介面,因此本文中介紹的所有內容均可用於這兩個程式包,但是大多數時候我們都會使用html/template程式包來生成HTML程式碼段。

Go Web 程式設計系列的每篇文章的原始碼都打了對應版本的軟體包,供大家參考。公眾號中回覆gohttp07獲取本文原始碼

模板檔案的字尾名

模板檔案可以使用.html或任何其他副檔名。但是通常我們將使用.gohtml副檔名來命名模板檔案,因為編輯器通常使用它來表示你想要高亮Go HTML模板語法。 AtomSublime Text等編輯器都具有Go外掛,來預設識別此副檔名。

模板語法

我們先來建立一個簡單的模板檔案test.gohtml:

<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
        <title>Go Web</title>
    </head>
    <body>
        {{ . }}
    </body>
</html>

{{ 和 }} 中間的半形句號 . 它代表模板物件執行Execute(w, data)傳入模板的資料,它是頂級作用域範圍內的,根據傳入的資料不同渲染不同的內容。. 可以代表Go語言中的任何型別,如結構體、Map等。

在寫模板的時候,會經常用到.。比如{{.}}{{len .}}{{.Name}}{{$x.Name}}

{{ 和 }} 包裹的內容統稱為 action,分為兩種型別:

  • 資料求值(data evaluations)
  • 控制結構(control structures)

action求值的結果會直接複製到模板中,控制結構和我們寫Go程式差不多,也是條件語句、迴圈語句、變數、函式呼叫等等…模板中的 action 並不多,我們一個一個看。

註釋

{{/* comment */}}

裁剪空字元

注意裁剪的是替換內容前面或者後面的空字元,你可以理解成模板中{{前面或}}後面的空字元(包括換行符、製表符、空格等)。

// 裁剪 content 前後的空字元
{{- content -}}

// 裁剪 content 前面的空字元
{{- content }}

// 裁剪 content 後面的空字元
{{ content -}}

文字輸出

{{ pipeline }}

pipeline代表的資料會產生與呼叫 fmt.Print 函式類似的輸出,例如整數型別的 3 會轉換成字串 “3” 輸出。

條件語句

{{ if pipeline }} T1 {{ end }}

{{ if pipeline }} T1 {{ else }} T0 {{ end }}

{{ if pipeline }} T1 {{ else if pipeline }} T0 {{ end }}

// 上面的語法其實是下面的簡寫
{{ if pipeline }} T1 {{ else }}{{ if pipeline }} T0 { {end }}{{ end }}

{{ if pipeline }} T1 {{ else if pipeline }} T2 {{ else }} T0 {{ end }}

如果 pipeline 的值為空,不會輸出 T1,除此之外 T1 都會被輸出。

空值有false0nil空字串 ""(長度為 0 的字串)。

迴圈語句

{{ range pipeline }} T1 {{ end }}

// 這個 else 比較有意思,如果 pipeline 的長度為 0 則輸出 else 中的內容
{{ range pipeline }} T1 {{ else }} T0 {{ end }}

// 獲取容器的下標
{{ range $index, $value := pipeline }} T1 {{ end }}

迴圈語句中的pipeline 的值必須是陣列、切片、字典和通道中的一種,即可迭代型別的值,根據值的長度輸出多個 T1。

define

{{ define "name" }} T {{ end }}

定義命名為 name 的模板。

template

{{ template "name" }}

{{ template "name" pipeline }}

第一種是直接執行名為name的模板,模板的全域性資料物件.設定為nil。第二種是點.設定為pipeline的值,並執行名為name的模板。

block

{{ block "name" pipeline }} T1 {{ end }}

block 的語義是如果有命名為 name 的模板,就引用過來執行,如果沒有命名為 name 的模板,就是執行自己定義的內容。換句話說,block可以認為是設定一個預設模板。

with

{{ with pipeline }} T1 {{ end }}

// 如果 pipeline 是空值則輸出 T0
{{ with pipeline }} T1 {{ else }} T0 {{ end }}

{{ with arg }}
    . // 此時 . 就是 arg
{{ end }}

with 建立一個新的上下文環境,在此環境中的 . 與外面的 . 無關。

對於第一種格式,當pipeline不為0值的時候,點.設定為pipeline運算的值,否則跳過。對於第二種格式,當pipeline為0值時,執行else語句塊,否則.設定為pipeline運算的值,並執行T1。

例如:

{{with .Person}}{{ .Name}}{{end}}

在這個 with 塊中.Name實際上引用的是全域性資料物件的.Person.Name

實踐練習:課程花名冊頁面

瞭解完模板語法後,接下來讓我們再http_demo專案中結合BootStrap建立一個簡單的模板,來展示伺服器如何把資料傳遞給模板、渲染HTML頁面,把頁面響應返回給客戶端。

我們建立一個用來展示大學物理課程的花名冊(授課老師和上課學生)

建立頁面模板

首先在我們的專案新增一個views目錄用於存放模板檔案,在建立三個模板檔案分別是:

layout.gohtml 用於存放頁面的整體佈局。

<html lang="en">
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="description" content="">
    <meta name="author" content="">

    <title>Bootstrap Template Page for Go Web Programming</title>

    <!-- Bootstrap core CSS -->
    <link href="//cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
</head>

<body>

{{ template "nav" .}}

<div class="container">
    {{template "content" .}}
</div> 

<script src="//cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
</body>
</html>

nav.gohtml是網頁頭部區域的頁面模板。

{{define "nav"}}
<nav class="navbar navbar-inverse navbar-fixed-top">
    <div class="container">
        <div class="navbar-header">
            <a class="navbar-brand" href="#">Person general infor</a>
        </div>
    </div>
</nav>

<div class="jumbotron">
    <div class="container">
        <h1>Hello, Professor {{.Teacher.Name}}</h1>
        <ul>
            <li>Name   : {{.Teacher.Name}}<p>
            <li>Subject : {{.Teacher.Subject}}
        </ul>
        <p><a class="btn btn-primary btn-lg" href="#" role="button">More &raquo;</a></p>
    </div>
</div>
{{end}}

content.gohtml是網頁主體內容部分的頁面模板。

{{define "content"}}
{{range .Students}}
<div class="row">
    <div class="col-md-4">
        <h2>Name</h2>
        <p>Name has the value of : {{.Name}} </p>
        <p><a class="btn btn-default" href="#" role="button">More &raquo;</a></p>
    </div>
    <div class="col-md-4">
        <h2>Id</h2>
        <p>Id has the value of : {{.Id}} </p>
        <p><a class="btn btn-default" href="#" role="button">More &raquo;</a></p>
    </div>
    <div class="col-md-4">
        <h2>Country</h2>
        <p>Country has the value of : {{.Country}} </p>
        <p><a class="btn btn-default" href="#" role="button">More &raquo;</a></p>
    </div>
</div>
{{end}}
{{end}}

layout.gohtml中我們引用了另外的兩個模板:

{{ template "nav" .}}
{{template "content" .}}

這樣不同的頁面變化的部分就只是content部分,針對不同的頁面我們只需要定義多個content模板,每次根據不同請求使用不同的content模板就行了。當然這裡的例子有點簡陋,大家理解意思就行了。

注意模板名稱後面的.,我們把layout.gohtml的全域性資料物件傳給了另外兩個模板這樣,在子模板裡也能訪問傳給模板的資料了。如果頁面模板中使用的資料欄位和迴圈語句有點疑惑可以先不用管,繼續往下看,等看過傳給頁面模板的資料後自然就理解了。

建立響應頁面請求的Handler

接下來建立一個伺服頁面請求的Handler

package handler

import (
    "fmt"
    "html/template"
    "net/http"
)

type Teacher struct {
    Name    string
    Subject string
}
type Student struct {
    Id      int
    Name    string
    Country string
}

type Rooster struct {
    Teacher Teacher
    Students []Student
}

func ShowIndexView(response http.ResponseWriter, request *http.Request) {

    teacher := Teacher{
        Name:    "Alex",
        Subject: "Physics",
    }
    students := []Student{
        {Id: 1001, Name: "Peter", Country: "China"},
        {Id: 1002, Name: "Jeniffer", Country: "Sweden"},
    }
    rooster := Rooster{
        Teacher:  teacher,
        Students: students,
    }

    tmpl, err := template.ParseFiles("./views/layout.gohtml", 
                                   "./views/nav.gohtml", 
                                   "./views/content.gohtml")

    if err != nil {
        fmt.Println("Error " +  err.Error())
    }
    tmpl.Execute(response, rooster)
}

使用template.ParseFiles載入這個頁面要使用的全部三個模板(如果載入少了,訪問頁面時會發生panic),然後使用模板物件的 Execute方法把我們儲存了花名冊資訊的資料物件傳給模板: tmpl.Execute(response, rooster) 渲染頁面並寫到響應裡去(http.ResponseWriter物件)。

註冊頁面路由

處理程式寫完後,為其註冊路由,在我們專案的路由模組新增如下路由:

package router

import (
    "example.com/http_demo/middleware"
    "github.com/gorilla/mux"
    "example.com/http_demo/handler"
)

func RegisterRoutes(r *mux.Router) {
    r.Use(middleware.Logging())
...

  viewRouter := r.PathPrefix("/view").Subrouter()
  viewRouter.HandleFunc("/index", handler.ShowIndexView)
}

訪問頁面

現在所有步驟都完成了,重啟我們的伺服器後就可以訪問到新寫的頁面了。

如果是在本地電腦裡,用Ctrl+C結束伺服器程式後再次執行go run main.go。如果是使用我們之前文章裡的Docker開發環境的話(公眾號回覆:go-docker 獲取Docker環境的安裝指南)需要在docker-compose.yml所在的目錄裡用docker-compose restart重啟服務。

開啟瀏覽器輸入http://localhost:8000/view/index就能訪問到我們剛才寫的頁面了。

l

總結

今天的文章講解了Go模板最常使用的幾個功能的使用方法,使用html/template模板庫結合BootStrap做頁面模板,還是比較簡單的BootStrap幫我們解決了很多前端的樣式問題。模板庫還有很多更高階的用法,比如在模板中呼叫函式、定義變數等功能,可以看下文末給出的參考連結瞭解這些內容。在前後端分離架構流行的今天我覺得作為用Go開發的後端工程師瞭解文章中列出的這些功能就夠了。

今天的例子中是通過 CDN 引用的BootStrap靜態資源,到目前我們的伺服器還無法伺服靜態資源,這個我們下篇文章再講。公眾號回覆gohttp07即可獲取今天文章中示例程式碼的下載連結。如果覺得我的文章有收穫,請幫忙分享給更多人。

參考

Go 語言標準庫 text/template 包深入淺出

An Introduction to Templates in Go

前文回顧

深入學習用Go編寫HTTP伺服器

設定HTTP伺服器的路由

Go Web程式設計–應用ORM

Go Web程式設計–深入學習解析HTTP請求

本作品採用《CC 協議》,轉載必須註明作者和本文連結

公眾號:網管叨bi叨 | Golang、PHP、Laravel、Docker等學習經驗分享

相關文章