函数
模板中可以使用函数作为参数。Go的模板引擎函数都是受限的:这些函数可以接受任意多个参数的输入,只能返回一个值,或返回一个值和一个错误。
使用模板函数映射,将一个函数映射为一个键(一般设置为函数名字)。
比如我们可以写这样一个函数,把日期时间类型的变量,格式化为指定的格式(“2006-01-02”)
func formatDate(t time.Time) string {
layout := "2006-01-02"
return t.Format(layout)
}
然后,我们定义一个函数Joeltemplate7(writer http.ResponseWriter, request *http.Request),在函数内创建一个变量名为 funcMap 的 FuncMap 结构。这个结构将名字 fdate 映射到 formatDate 函数。
接着,使用 template.New 函数创建一个模板,名为 ./JoelTemplate/sayHelloDate.html,再以串联的方式调用模板的 Funcs 方法,并将前面创建的 funcMap 传递给模板。
这两步使得 funcMap 和模板绑定完成。最后对模板文件进行语法分析,调用 ExecuteTemplate 方法,并将 ResponseWriter 以及当前时间传递给程序。
func Joeltemplate7(writer http.ResponseWriter, request *http.Request) {
funcMap := template.FuncMap{"fdate":formatDate}
t := template.New("./JoelTemplate/sayHelloDate.html").Funcs(funcMap)
t, _ = t.ParseFiles("./JoelTemplate/sayHelloDate.html")
t.ExecuteTemplate(writer,"sayHelloDate.html" ,time.Now())
}
模板文件 sayHelloDate.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Go Web Programming</title>
</head>
<body>
管道:{{. | fdate}}
<br>
普通:{{fdate .}}
</body>
</html>
在模板文件中,使用了2种显示方式,一种是管道、一种是普通函数的方式。2种方式执行结果是相同的。
上下文感知
go语言的模板引擎可以根据内容所处的上下文改变其显示的内容。可以正确的转义显示内容。比如:如果模板显示的是HTML格式的内容,那么模板将对其实施HTML转义;如果模板显示的是javascript格式的内容,那么模板将对其进行javascript转义。此外,模板还可以感知识别出内容中的URL或css样式。
代码示例
func Joeltemplate8(writer http.ResponseWriter, request *http.Request) {
t, _ := template.ParseFiles("./JoelTemplate/sayHelloContextAware.html")
content := `I asked:<li>"What's up?'"</li>`
t.Execute(writer, content)
}
模板文件sayHelloContextAware.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Go Web Programming</title>
</head>
<body>
<div>{{.}}</div>
<div><a href="/{{.}}">path</a> </div>
<div><a href="/?q={{.}}">query</a> </div>
<div><a onclick="f('{{.}}')">Onclick</a> </div>
</body>
</html>
这个content在模板中表现出的转义结果是不同的
<html lang="en"><head>
<meta charset="UTF-8">
<title>Go Web Programming</title>
</head>
<body>
<div>I asked:<li>"What's up?'"</li></div>
<div><a href="/I%20asked:%3cli%3e%22What%27s%20up?%27%22%3c/li%3e">path</a> </div>
<div><a href="/?q=I%20asked%3a%3cli%3e%22What%27s%20up%3f%27%22%3c%2fli%3e">query</a> </div>
<div><a onclick="f('I asked:\x3cli\x3e\x22What\x27s up?\x27\x22\x3c\/li\x3e')">Onclick</a> </div>
</body></html>
界面效果
对传递的值做一些改变,再比较一下运行结果
func Joeltemplate8(writer http.ResponseWriter, request *http.Request) {
t, _ := template.ParseFiles("./JoelTemplate/sayHelloContextAware.html")
content := []string{`I asked:<li>"What's up?'"</li>`,`完全是文字`,`http://www.baidu.com/`,`333`,`alert("Love Moon")`}
t.Execute(writer, content)
}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Go Web Programming</title>
</head>
<body>
{{range .}}
<div>{{.}}</div>
<div><a href="{{.}}">path</a></div>
<div><a href="/?q={{.}}">query</a></div>
<div><buton onclick="alert('{{.}}')">Onclick</buton></div>
<hr>
{{end}}
</body>
</html>
<html lang="en"><head>
<meta charset="UTF-8">
<title>Go Web Programming</title>
</head>
<body>
<div>I asked:<li>"What's up?'"</li></div>
<div><a href="#ZgotmplZ">path</a></div>
<div><a href="/?q=I%20asked%3a%3cli%3e%22What%27s%20up%3f%27%22%3c%2fli%3e">query</a></div>
<div><buton onclick="alert('I asked:\x3cli\x3e\x22What\x27s up?\x27\x22\x3c\/li\x3e')">Onclick</buton></div>
<hr>
<div>完全是文字</div>
<div><a href="%e5%ae%8c%e5%85%a8%e6%98%af%e6%96%87%e5%ad%97">path</a></div>
<div><a href="/?q=%e5%ae%8c%e5%85%a8%e6%98%af%e6%96%87%e5%ad%97">query</a></div>
<div><buton onclick="alert('完全是文字')">Onclick</buton></div>
<hr>
<div>http://www.baidu.com/</div>
<div><a href="http://www.baidu.com/">path</a></div>
<div><a href="/?q=http%3a%2f%2fwww.baidu.com%2f">query</a></div>
<div><buton onclick="alert('http:\/\/www.baidu.com\/')">Onclick</buton></div>
<hr>
<div>333</div>
<div><a href="333">path</a></div>
<div><a href="/?q=333">query</a></div>
<div><buton onclick="alert('333')">Onclick</buton></div>
<hr>
<div>alert("Love Moon")</div>
<div><a href="alert%28%22Love%20Moon%22%29">path</a></div>
<div><a href="/?q=alert%28%22Love%20Moon%22%29">query</a></div>
<div><buton onclick="alert('alert(\x22Love Moon\x22)')">Onclick</buton></div>
<hr>
</body></html>
这个上下文感知,能够让你以原有的形式,在各种上下文中转义显示出来。更可以来防御XSS(cross-site scripting,跨站脚本)攻击。
防御XSS攻击
XSS攻击是由于服务器将攻击者存储的数据原原本本的显示给其他用户所致的。例如一些网站带有用户回复功能,而回复的内容带有<script>标签的代码。界面展示这些回复内容的时候,即使对 <script> 标签也是原原本本的显示给用户,这使得用户在毫不知情的情况下,被执行了 javascript 脚本。这样的脚本往往是恶意代码。
预防这样的情况,常见的方法就是在显示或者存储用户传入的数据之前,对数据进行转义。
给一个XSS攻击的代码例子
form表单模板文件sayHelloForm.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Go Web Programming</title>
</head>
<body>
<form action="/xss/" method="post">
Comment:<input name="comment" type="text">
<hr>
<button id="submit" type="submit">Submit</button>
</form>
</body>
</html>
表单页面模板加载的代码
func JoelFormSend(writer http.ResponseWriter, request *http.Request) {
t, _ := template.ParseFiles("./JoelTemplate/sayHelloForm.html")
t.Execute(writer, nil)
}
显示数据的页面模板sayHelloXSS.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Go Web Programming</title>
</head>
<body>
<div>{{.}}</div>
</body>
</html>
接收数据绑定模板的代码
func Joeltemplate9(writer http.ResponseWriter, request *http.Request) {
vlu:= request.FormValue("comment")
t, _ := template.ParseFiles("./JoelTemplate/sayHelloXSS.html")
t.Execute(writer, vlu)
}
我们在界面输入 <script>alert('hah')</script>
提交后显示结果为下图:
不对HTML转义的方法
在某些情况下,我们需要按照用户原始输入来输出,这个时候,可以在Execute的时候,使用 template.HTML来告诉处理器,不对HTML进行转义。
func Joeltemplate9(writer http.ResponseWriter, request *http.Request) {
vlu:= request.FormValue("comment")
writer.Header().Set("X-XSS-Protection", "0")
t, _ := template.ParseFiles("./JoelTemplate/sayHelloXSS.html")
t.Execute(writer, template.HTML(vlu))
}
这几行代码中, writer.Header().Set("X-XSS-Protection", "0")是针对一些本来就具有防跨站脚本攻击功能的浏览器,进行关闭内置XSS防御功能。从而实现原始输出。
我们现在可以借助这个方法来看XSS攻击效果。
模板嵌套
模板嵌套是构建丰富页面形态的主要手段了。通常,在一个页面里会有很多个布局部分组成,这些布局中有些是在多个页面甚至是整个站点中都被反复使用的。如果每个页面的这些部分都是独立写的,当这些公共部分需要修改的时候,就会需要改动所有使用到这些内容的页面。这个时候,使用模板嵌套就可以很好的解决这个问题了。
模板,允许把每一个可以逻辑上可独立存在的布局单独编写成一个模板文件,并且让多个其他模板嵌套调用。
先看个简单的例子
我们知道一个页面一般由头部、菜单、主体、底部组成,我们可以称它们为head、menu、body、foot。我们构建一个index页面模板,再分别构建head、menu、body、foot模板。并且在index中嵌套其他的这几个模板。
tmplIndex.html
{{ define "index2" }}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Go Web Programming{{.Title}}</title>
</head>
<body>
index自己的内容
{{ template "head" .Head}}
{{ template "menus" .Menu}}
{{ template "body" .Body }}
{{ template "foot" .Foot}}
</body>
</html>
{{end}}
tmplPubHead.html
{{define "head"}}
<div>头部</div>
<div>Banner</div>
{{end}}
tmplPubMenus.html
{{define "menus"}}
<div>主菜单</div>
<div>子菜单</div>
{{end}}
tmplPubBody.html
{{define "body"}}
<div>主体1</div>
<div>主体2{{.}}</div>
<div>主体3</div>
{{end}}
tmplPubFoot.html
{{define "foot"}}
<div>底部</div>
{{end}}
在模板调用加载的handler函数中,需要注意,所有需要加载的模板都必须写进代码里。
这里为了给各个模板传递指定的值进去,绑定的参数为map[string]string格式。
//嵌套模板
func processQtHandler(writer http.ResponseWriter, request *http.Request) {
t, _ := template.ParseFiles("./JoelTempWeb/tmplIndex.html","./JoelTempWeb/tmplPubHead.html","./JoelTempWeb/tmplPubMenus.html","./JoelTempWeb/tmplPubBody.html","./JoelTempWeb/tmplPubFoot.html",)
t.ExecuteTemplate(writer, "index2", map[string]string{"Title":"This is Title","Head":"This is Head","Menu":"This is menu", "Body":"This is Body", "Foot":"This is Foot"})
}
在main中添加此函数的监听路径,我们可以看到以下运行结果。
如果看不清楚的话,我们来修改一下模板
tmplIndex.html
{{ define "index2" }}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{.Title}}</title>
</head>
<body>
当前页面是index页
{{ template "head" .Head}}
{{ template "menus" .Menu}}
{{ template "body" .Body }}
{{ template "foot" .Foot}}
</body>
</html>
{{end}}
tmplPubHead.html
{{define "head"}}
<div>{{.}}</div>
{{end}}
tmplPubMenus.html
{{define "menus"}}
<div>{{.}}</div>
{{end}}
tmplPubBody.html
{{define "body"}}
<div>{{.}}</div>
{{end}}
tmplPubFoot.html
{{define "foot"}}
<div>{{.}}</div>
{{end}}
这次看清楚了吗?在index页面里,{{ template "head" .Head}}的 "head" 对应的是 tmplPubHead.html 中的 {{define "head"}} ,.Head 对应的是 map[string]string 中的 “Head”。界面对应位置显示的就是 Head 健的 值 “This is Head”。其他布局也是同样。
由此,我们可以知道,在 handler 中把页面所需要加载的数据,都获取到,并且按照模板布局分别给入绑定就可以了。
那么如果,是临时要在index中增加一个动作,来显示一些已经存在的数据,是不是必须要再添加一个模板文件,并且修改go代码中加载模板的那部分呢?
这里,模板给我们一个便利的方法,如果是要显示已经存在的数据(例如Title),只需要在index中使用 block 直接定义一个模板就可以了。
下面我们在index中增加以下模板
{{ block "fly" .Title }}
<div>自由的飞{{.}}</div>
{{end}}
增加这些代码后的index模板文件变为这样
{{ define "index2" }}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>{{.Title}}</title>
</head>
<body>
当前页面是index页
{{ template "head" .Head}}
{{ template "menus" .Menu}}
{{ template "body" .Body }}
{{ template "foot" .Foot}}
{{ block "fly" .Title }}
<div>自由的飞{{.}}</div>
{{end}}
</body>
</html>
{{end}}
然后我们不需要修改go代码,直接运行看看结果
动态切换模板
当我们需要根据业务随时切换模板的时候,最好还是能够让程序自动执行。这里可以采用两个相同的 define 来实现这一目标。
首先构建两个用来切换的模板
tmplDtRed.html
{{define "content"}}
<h1 style="color: #f00;">Hello Moon</h1>
{{end}}
tmplDtBlue.html
{{define "content"}}
<h1 style="color: #00f;">Hello Moon</h1>
{{end}}
然后构建一个调用他们的主模板
tmplDt.html
{{define "Dt"}}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Go Web Programming</title>
</head>
<body>
{{template "content"}}
</body>
</html>
{{end}}
两个模板的 define 都是 "content",但是模板文件名不同。
在go代码中,我们采用随机的方式,来判断应该调用哪个嵌套模板
//动态切换模板
func processDtHandler(writer http.ResponseWriter, request *http.Request) {
rand.Seed(time.Now().Unix())
var t *template.Template //创建一个模板
if rand.Intn(10) > 5 {
t, _ = template.ParseFiles("./JoelTempWeb/tmplDt.html","./JoelTempWeb/tmplDtRed.html",)
}else {
t, _ = template.ParseFiles("./JoelTempWeb/tmplDt.html","./JoelTempWeb/tmplDtBlue.html",)
}
t.ExecuteTemplate(writer, "Dt", "")
}
我们根据 rand.Intn(10) > 5 的值来随机的使用嵌套模板。
运行程序后,多刷新几次,就会发现页面文字的颜色会发生变换。
当然实际项目中,是很少 rand.Intn(10) > 5 这样实现的,应该是你具体的业务逻辑判断,来决定采用哪个模板。