Golang html/template包
Template 模板引擎 将模板和数据进行渲染 输出格式化后的字符程序
golang提供 两个 Template 标准库
text/template $GOROOT/src/text/template
html/template $GOROOT/src/html/template
html/template格式化html字符
go 模板引擎三步创建模板对象
加载模板字串
执行渲染模板//把加载的字符和数据进行格式化
go提供 标准库html/template 处理模板的接口
├── main.go
└── templates
├── index.html
└── layout.html
layout.html文件
<!DOCTYPE html><html><head><meta charset="utf-8"><title>layout</title></head>
<body><h3>This is layout</h3>template data: {{ . }}</body></html>
ParseFiles 加载模板 该方法 返回模板对象和错误 用模板对象执行模板 注入数据对象
go 提供 模板标签 称之为 *action* 是 action
func templateHandler(w http.ResponseWriter, r *http.Request){
t, _ :=template.ParseFiles("templates/layout.html")
fmt.Println(t.Name())
t.Execute(w, "Hello world")
}
打印t模板对象的Name方法 每一个模板 都有一个名字
如果不显示指定这个名字 go会把文件名(包括扩展名当成名字)
本例则是layout.html 访问之后可以看见返回的html字串
curl -i http://127.0.0.1:8000/
go 解析模板文件 也可解析模板字串 标准的处理 新建-加载-执行三部曲
func templateHandler(w http.ResponseWriter, r *http.Request){
tmpl := `<!DOCTYPE html><html><head><meta charset="utf-8"> <title>Go Web Programming</title></head>
<body>{{ . }}</body></html>`
t := template.New("layout.html")
t, _ = t.Parse(tmpl)
fmt.Println(t.Name())
t.Execute(w, "Hello World")
}
开发最终页面 可能是多个模板文件的嵌套
go的ParseFiles支持加载多个模板文件 不过模板对象的名字则是第一个模板文件的文件名
func templateHandler(w http.ResponseWriter, r *http.Request){
t, _ :=template.ParseFiles("templates/layout.html", "templates/index.html")
fmt.Println(t.Name())
t.Execute(w, "Hello world")
}ParseGlob 方法 通过glob通配符加载模板
模板命名与嵌套
模板对象有名字 创建模板对象时 显示命名可让go自动命名 涉及到嵌套模板的时候 如何命名模板
go提供 ExecuteTemplate 方法 用于执行指定名字的模板 如加载 layout.html 模板 可以指定layout.html
func templateHandler(w http.ResponseWriter, r *http.Request){
t, _ :=template.ParseFiles("templates/layout.html")
fmt.Println(t.Name())
t.ExecuteTemplate(w, "layout", "Hello world")
}修改layout.html文件
{{ define "layout" }}<!DOCTYPE html><html><head><meta charset="utf-8"><title>layout</title></head>
<body><h3>This is layout</h3>template data: {{ . }}</body></html>{{ end }}
模板文件中 使用 define 这个action给模板文件命名 虽然 ParseFiles 方法返回的模板对象 t 名字还是layout.html
但 ExecuteTemplate 执行的模板 是html文件中定义的 layout
通过 define 定义模板 可通过 template action 引入模板 修改 layout.html 和 index.html
{{ define "layout" }}<!DOCTYPE html><html><head><meta charset="utf-8"><title>layout</title></head>
<body><h3>This is layout</h3>template data: {{ . }}{{ template "index" }}</body></html>{{ end }}
index.html 模板
{{ define "index" }}<div style="background: yellow">this is index.html</div>{{ end }}
用 ParseFiles 加载需要渲染的模板文件
func templateHandler(w http.ResponseWriter, r *http.Request){
t, _ :=template.ParseFiles("templates/layout.html", "templates/index.html")
t.ExecuteTemplate(w, "layout", "Hello world")
} 访问看到 index 被 layout 模板include了 curl http://127.0.0.1:8000/
单文件嵌套
创建模板对象 和 加载多个模板文件 执行模板文件时 需要指定 base模板 layoutbase模板中 include 其他命名的模板 无论点 . define template 花括号包裹的东西都是go的action(模板标签)
Action
action是go模板 用于动态执行 逻辑和展示数据的形式
条件语句 迭代 封装
模板条件判断
{{ if arg }}some content{{ end }}{{ if arg }}some content{{ else }}other content{{ end }}
arg 是基本数据结构 也可以是表达式
if-end包裹的内容为条件为真的时候展示 与if语句一样 模板也可以有else语句
func templateHandler(w http.ResponseWriter, r *http.Request){
t, _ :=template.ParseFiles("templates/layout.html")
rand.Seed(time.Now().Unix())
t.ExecuteTemplate(w, "layout", rand.Intn(10) > 5)
}
{{ define "layout" }}<!DOCTYPE html><html><head><meta charset="utf-8"><title>layout</title></head><body>
<h3>This is layout</h3>template data: {{ . }}{{ if . }} Number is greater than 5!{{ else }} Number is 5 or less!{{ end }}</body></html>{{ end }}
当. 值为true 显示if 逻辑 否则显示else的逻辑
模板迭代
数组 切片或 map 使用迭代的action与go的迭代类似 使用range处理
func templateHandler(w http.ResponseWriter, r *http.Request) {
t := template.Must(template.ParseFiles("templates/layout.html"))
daysOfWeek := []string{"Mon", "Tue", "Wed", "Ths", "Fri", "Sat", "Sun"}
t.ExecuteTemplate(w, "layout", daysOfWeek)
}
{{ define "layout" }}
<!DOCTYPE html><html><head><meta charset="utf-8"><title>layout</title></head>
<body><h3>This is layout</h3>template data: {{ . }}{{ range . }}<li>{{ . }}</li>{{ end }}</body></html>{{ end }}
输出li列表 迭代的时候 还可以使用$设置循环变量
{{ range $key, $value := . }} <li>key: {{ $key }}, value: {{ $value }}</li>{{ else }}empty{{ end }}
和迭代切片很像 rang也可以使用else语句
func templateHandler(w http.ResponseWriter, r *http.Request) {
t := template.Must(template.ParseFiles("templates/layout.html"))
daysOfWeek := []string{}
t.ExecuteTemplate(w, "layout", daysOfWeek)
}
{{ range . }}<li>{{ . }}</li>{{ else }}empty{{ end }}
当range的结构为空的时候 则会执行else分支的逻辑
模板with封装
go模板 with语句 是创建一个封闭的作用域 在其范围内 可以使用.action 而与外面的.无关 只与with的参数有关{{ with arg }}此时的点 . 就是arg{{ end }}
{{ define "layout" }}<!DOCTYPE html><html><head><meta charset="utf-8"><title>layout</title></head>
<body><h3>This is layout</h3>template data: {{ . }} {{ with "world"}} Now the dot is set to {{ . }} {{ end }}</body></html>{{ end }}
访问结果curl http://127.0.0.1:8000/
template data: [Mon Tue Wed Ths Fri Sat Sun]
Now the dot is set to world
with语句的.与其外面的.是两个不相关的对象 with语句也可以有else
else中的.则和with外面的.一样 毕竟只有with语句内才有封闭的上下文
{{ with ""}}Now the dot is set to {{ . }}{{ else }}{{ . }}{{ end }}
访问效果curl http://127.0.0.1:8000/
template data: [Mon Tue Wed Ths Fri Sat Sun]
[Mon Tue Wed Ths Fri Sat Sun]
模板引用
func templateHandler(w http.ResponseWriter, r *http.Request) {t := template.Must(template.ParseFiles("templates/layout.html", "templates/index.html"))
daysOfWeek := []string{"Mon", "Tue", "Wed", "Ths", "Fri", "Sat", "Sun"}
t.ExecuteTemplate(w, "layout", daysOfWeek)
}
layout中引用了 index模板
{{ define "layout" }}<!DOCTYPE html><html><head><meta charset="utf-8"><title>layout</title></head><body>
<h3>This is layout</h3>layout template data: ({{ . }}){{ template "index" }}</body></html>{{ end }}
index.html 内容打印 .
{{ define "index" }}<div style="background: yellow">this is index.html ({{ . }})</div>{{ end }}
访问的效果 index.html 中的点并没有数据
curl http://127.0.0.1:8000/
<h3>This is layout</h3>
layout template data: ([Mon Tue Wed Ths Fri Sat Sun])
<div style="background: yellow">this is index.html ()</div>
修改引用语句{{ template "index" . }} 把参数传给子模板 就能看见index.html模板 有数据
<div style="background: yellow">this is index.html ([Mon Tue Wed Ths Fri Sat Sun])</div
参数 变量和管道
模板的参数可以是go中的基本数据类型 如字串 数字 布尔值 数组切片或者一个结构体 在模板中设置变量可以使用 $variable := value在range迭代的过程使用了设置变量的方式
go 模板的管道函数 通过定义函数过滤器 实现模板的一些简单格式化处理 并且通过管道哲学 这样的处理方式可以连成一起
{{ p1 | p2 | p3 }} 如 模板内置了函数格式化输出 {{ 12.3456 | printf "%.2f" }}
模板函数
管道符可以成为模板中的过滤器 除了内建的函数 能够自定义函数可以扩展模板的功能go模板提供了自定义模板函数的功能
想要创建一个定义函数需要两步
创建 FuncMap 类型的map key是模板函数的名字 value是其函数的定义
将 FuncMap 注入到模板中
func templateHandler(w http.ResponseWriter, r *http.Request) {
funcMap := template.FuncMap{"fdate": formDate}
t := template.New("layout").Funcs(funcMap)
t = template.Must(t.ParseFiles("templates/layout.html", "templates/index.html"))
t.ExecuteTemplate(w, "layout", time.Now())
}在模板中使用{{ . | fdate }} 也可以不用管道过滤器 而是使用正常的函数调用形式 {{ fdate . }}
函数的注入 必须要在 parseFiles 之前 因为解析模板的时候 需要先把函数编译注入
模板智能上下文
go 根据上下文显示模板的内容 如字符的转义 根据所显示的上下文环境而智能变化 比如同样的html标签 在Js和html环境中 其转义的内容是不一样func templateHandler(w http.ResponseWriter, r *http.Request){
t, _ :=template.ParseFiles("templates/layout.html")
content := `I asked: <i>What's up?</i>`
t.ExecuteTemplate(w, "layout", content)
}模板文件
{{ define "layout" }}<!DOCTYPE html><html><head><meta charset="utf-8"><title>layout</title></head>
<body><h3>This is layout</h3>layout template data: ({{ . }})
<div><a href="/{{ . }}">Path</a></div>
<div><a href="/?q={{ . }}">Query</a></div>
<div><a onclick="f('{{ . }}')">Onclick</a></div>
</body></html>{{ end }}
访问结果
layout template data: (I asked: <i>What's up?</i>)
<div><a href="/I%20asked:%20%3ci%3eWhat%27s%20up?%3c/i%3e">Path</a></div>
<div><a href="/?q=I%20asked%3a%20%3ci%3eWhat%27s%20up%3f%3c%2fi%3e">Query</a></div>
<div><a onclick="f('I asked: \x3ci\x3eWhat\x27s up?\x3c\/i\x3e')">Onclick</a></div>
模板安全
go自动处理html标签的转义 对web安全具有重要作用 避免了一些XSS攻击XSS安全
安全是一个很大的话题 XSS安全包含很多内容
XSS主要分为三种
通过提交待script标签的内容执行js
layout.html加一个表单
<form action="/" method="post">Comment: <input name="comment" type="text"><hr/><button id="submit">Submit</button></form>
go的表单 处理函数
func templateHandler(w http.ResponseWriter, r *http.Request){
t, _ :=template.ParseFiles("templates/layout.html")
t.ExecuteTemplate(w, "layout", r.FormValue("comment"))
}
提交一段js 到go在表达处理的时候 自动做了xss过滤
如果不想转义标签 需要使用template.HTML方法包裹
func templateHandler(w http.ResponseWriter, r *http.Request){
w.Header().Set("X-XSS-Protection", "0")
t, _ :=template.ParseFiles("templates/layout.html")
t.ExecuteTemplate(w, "layout", template.HTML(r.FormValue("comment")))
}
模板自定义函数
golang 模板template自定义函数用法package main
import (
"html/template"
"net/http"
"time"
)
type User struct {
Username, Password string
RegTime time.Time
}
//函数名字大写 不然模板 无法调用
func ShowTime(t time.Time, format string) string {
return t.Format(format)
}
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
//template.New 参数名字要和 ParseFiles 函数的文件名要相同 不然报错 "" is an incomplete template
tmpl := template.New("demo.html")
tmpl = tmpl.Funcs(template.FuncMap{"showtime": ShowTime})
tmpl, _ = tmpl.ParseFiles("demo.html")
//mpl, _ = tmpl.Parse(`<p>{{.Username}}|{{.Password}}|{{.RegTime.Format "2006-01-02 15:04:05"}}</p>
//<p>{{.Username}}|{{.Password}}|{{showtime .RegTime "2006-01-02 15:04:05"}}</p>
//`)
// tmpl = template.Must(tmpl.ParseFiles("templates/demo.html")) // 这样写 这个template.Must到底干嘛的呢 就是省略了一个err
user := User{"username", "password", time.Now()}
if err := tmpl.ExecuteTemplate(w, "demo.html", user); nil != err {
http.Error(w, err.Error(), http.StatusBadRequest)
}
})
http.ListenAndServe(":8082", nil)
}
<h1>Func</h1>
<p>{{.Username}}|{{.Password}}|{{.RegTime.Format "2006-01-02 15:04:05"}}</p>
<p>{{.Username}}|{{.Password}}|{{showtime .RegTime "2006-01-02 15:04:05"}}</p>
Must 函数
// Must is a helper that wraps a call to a function returning (*Template, error)// and panics if the error is non-nil. It is intended for use in variable initializations
// such as var t = template.Must(template.New("name").Parse("html"))
func Must(t *Template, err error) *Template {
if err != nil {
panic(err)
}
return t
}用于验证模板 如大括号 注释和变量的匹配
package main
import (
"fmt"
"text/template"
)
func main() {
t1 := template.New("first")
template.Must(t1.Parse(" some static text "))
fmt.Println("第一个解析OK")
t2 := template.New("second")
template.Must(t2.Parse("some static text {{ .Name }}"))
fmt.Println("第二个解析OK")
fmt.Println("下一个解析失败")
t3 := template.New("check parse error with Must")
template.Must(t3.Parse(" some static text {{ .Name }"))//panic: template: check parse error with Must:1: unexpected "}" in operand
}
从字符串载入模板
tplStr := `{{ .Name }} {{ .Age }}`tpl := template.Must(template.New("tplName").Parse(tplStr))
tpl.Execute(os.Stdout, map[string]interface{}{Name: "big_cat", Age: 29})
从文件载入模板
模板文件 每个模板文件显式 定义模板名称 {{ define "tplName" }} 模板对象名 与 模板名不一致 无法解析用 {{ define "tplName" }} 定义模板名
用 {{ template "tplName" . }} 引入其他模板
用 . 访问当前数据域 比如 range 里使用 . 访问 循环项的数据域
用 $. 访问绝对顶层数据域
views/header.html
{{ define "header" }}<!doctype html><head><meta charset="UTF-8"><title>{{ .PageTitle }}</title></head>{{ end }}
views/footer.html
{{ define "footer" }}</html>{{ end }}
views/index/index.html
{{ define "index/index" }} {{}}{{ template "header" . }}
<body><div>hello, {{ .Name }}, age {{ .Age }}</div></body> {{ template "footer" . }}{{ end }}
views/news/index.html
{{ define "news/index" }}
{{ template "header" . }}
<body>{{}}
{{ $pageTitle := "news title" }}
{{ $pageTitleLen := len $pageTitle }}
{{}}
{{ if gt $pageTitleLen 4 }}<h4>{{ $pageTitle }}</h4>{{ end }}
{{ $c1 := gt 4 3}}
{{ $c2 := lt 2 3 }}
{{}}
{{ if and $c1 $c2 }}<h4>1 == 1 3 > 2 4 < 5</h4>{{ end }}
<div><ul>
{{ range .List }}
{{ $title := .Title }}{{}}
<li>{{ $title }} - {{ .CreatedAt.Format "2006-01-02 15:04:05" }} - Author {{ $.Author }}</li>
{{end}}
</ul>
{{}}
{{ with .Total }}<div>总数:{{ . }}</div>{{ end }}
</div>
</body>
{{ template "footer" . }}
{{ end }}
template.ParseFiles
手动定义 要载入的模板 解析后制定需要渲染的模板名 news/index// 从模板文件构建
tpl := template.Must(
template.ParseFiles("views/index/index.html","views/news/index.html","views/header.html","views/footer.html",),
)// render template with tplName index
_ = tpl.ExecuteTemplate(os.Stdout,"index/index",map[string]interface{}{PageTitle: "首页",Name: "cat",Age: 29, },)
// render template with tplName index
_ = tpl.ExecuteTemplate(os.Stdout,"news/index",map[string]interface{}{"PageTitle": "新闻","List": []struct {
Title string
CreatedAt time.Time
}{{Title: "golang views/template example", CreatedAt: time.Now()},
{Title: "be honest, i don't very like this raw engine", CreatedAt: time.Now()},
},
"Total": 1,
"Author": "big_cat",
},
)
template.ParseGlob
手动 指定每一个模板文件 在一些场景下难免难以满足需求 可以使用通配符正则匹配载入正则不包含文件夹 否则会因文件夹被作为视图载入无法解析而报错 可以设定多个模式串 如下载入了一级目录和二级目录的视图文件
// 从模板文件构建 tpl := template.Must(template.ParseGlob("views*.html"))
Web服务器
package mainimport (
"html/template"
"log"
"net/http"
"time"
)
var (
htmlTplEngine *template.Template
htmlTplEngineErr error
)
func init() {
// 初始化模板引擎 并加载各层级的模板文件
// views*.html 来加载 view 下的各子目录中的模板文件
htmlTplEngine = template.New("htmlTplEngine")// 模板根目录下的模板文件 一些公共文件
_, htmlTplEngineErr = htmlTplEngine.ParseGlob("views*.html")
if nil != htmlTplEngineErr {
log.Panic(htmlTplEngineErr.Error())
}
}
func IndexHandler(w http.ResponseWriter, r *http.Request) {
_ = htmlTplEngine.ExecuteTemplate(w,"index/index",
map[string]interface{}{"PageTitle": "首页", "Name": "sqrt_cat", "Age": 25},
)
}// news
func NewsHandler(w http.ResponseWriter, r *http.Request) {
_ = htmlTplEngine.ExecuteTemplate(w,"news/index", map[string]interface{}{
"PageTitle": "新闻",
"List": []struct {
Title string
CreatedAt time.Time
}{{Title: "this is golang views/template example", CreatedAt: time.Now()},
{Title: "to be honest, i don't very like this raw engine", CreatedAt: time.Now()},
},
"Total": 1,
"Author": "big_cat",
},
)
}
func main() {
http.HandleFunc("/", IndexHandler)
http.HandleFunc("/index", IndexHandler)
http.HandleFunc("/news", NewsHandler)
serverErr := http.ListenAndServe(":8085", nil)
if nil != serverErr {
log.Panic(serverErr.Error())
}
}
板对象有名字 如果没有定义名字 会用第一个被载入的视图文件 baseName 做名
template.ParseFiles/template.ParseGlob 生成模板对象时 没指定模板对象名 则用第一个被载入的文件 如 index/index.html 的 baseName 即 index.html 做默认名
如果 tplObj.Execute 方法执行渲染 会查找名为 index.html 的模板 所以常用 tplObj.ExecuteTemplate 自己指定要渲染的模板名
尊贵的董事大人
英文标题不为空时 视为本栏投稿
需要关键字 描述 英文标题