Go语言编程笔记15:模版引擎
2022-08-03 09:57:03
236
{{single.collect_count}}

Go语言编程笔记15:模版引擎

image-20211108153040805

图源:wallpapercave.com

除去一些作为API使用或者其他特殊用途的Web应用,大多数Web应用都是以网站的形式对外提供服务,所以自然的,返回的HTTP响应内容也都是以HTML页面为主。在[Go语言编程笔记12:web基础](Go语言编程笔记12:web基础 - 魔芋红茶’s blog (icexmoon.xyz))中我提到过,在Web技术发展的过程中,因为对交互的需要,Web诞生了一种SSI技术,即在服务端通过编程语言来“动态”生成HTML页面并返回给客户端。这种技术进一步发展,最后的结果就是我们现在经常在Web开发中会提到的模版引擎

模版引擎

所谓的模版引擎,其功能相当明确和单一:就是负责将服务端根据请求生成的响应数据,“渲染”到指定的HTML模版文件上,最后生成将要返回给客户端的最终HTML页面。

这个过程可以用下图表示:

image-20211226152238560

虽然模版引擎的功能简单且易于理解,但模版引擎真正要掌握的“模版语法”并不简单,且各种不同的模版引擎之间在语法上可能千差万别。

从功能上,模版引擎可以分为两类:

  • 无逻辑模版引擎
  • 嵌入逻辑的模版引擎

前者的“模版语法”相对简单,仅仅实现简单的内容替换。后者的“模版语法”就复杂一些,看起来更像是小型的编程语言,但功能也更强大,可以使用编程语言的“流程控制”,比如条件语句或循环语句。就实用性而言,后者显然应用的更广泛,所以一般Web框架会集成一个嵌入逻辑的模版引擎。

Go语言提供两个标准库:text/templatehtml/template作为可选的模版引擎。前者适用与一般的文本处理,后者专门用于Web应用的模版引擎来处理HTML内容。

html/template

template包将模版抽象为一个结构体:

type Template struct {escapeErr errortext *template.TemplateTree *parse.Tree*nameSpace // common to all associated templates}

我们可以使用New函数创建模版:

t := template.New("index.html")

这里传入的index.html是模版名称,如果没有给模版起名,默认使用模版文件的文件名作为其名称。

后边会说明如何通过“定义动作”给模版起名。

在“真正”使用模版之前,需要先解析模版。具体有两种方式,一种是指定一个或多个模版文件来解析:

t.ParseFiles("index.html")

另一种是指定一个包含模版文件内容的字符串:

content := `<!DOCTYPE html><html><head></head><body><h1>{{ . }}</h1><h1>index page.</h1></body></html>`t.Parse(content)

最后“执行模版”就可以调用模版引擎将给定的数据“渲染”到模版中,生成HTML页面并写入响应报文:

msg := "hello world!"t.Execute(rw, msg)

现在看一个完整的例子:

package mainimport ("html/template""net/http")func index(rw http.ResponseWriter, r *http.Request) {t := template.New("index.html")t.ParseFiles("index.html")msg := "hello world!"t.Execute(rw, msg)}func main() {http.HandleFunc("/index", index)http.ListenAndServe(":8080", http.DefaultServeMux)}

模版文件index.html

<!DOCTYPE html><html><head></head><body><h1>{{ . }}</h1><h1>index page.</h1></body></html>

需要注意,使用VSC编写上面这段代码时,自动导入包的工具会自动帮你导入text/template包,如果要使用html/template包,需要手动修改相应的import语句。

除了上边这种“按部就班”的方式,还可以用一种更简单的方式使用模版:

...func index(rw http.ResponseWriter, r *http.Request) {t, err := template.ParseFiles("index.html")if err != nil {log.Fatal(err)}msg := "hello world!"t.Execute(rw, msg)}...

这里的template.ParseFiles函数可以直接返回一个解析好的模版。

要注意的是,解析模版的过程中很容易出错(比如模版语法写错等),所以相应的解析函数会返回一个错误标识,我们需要处理可能存在的错误。除了上面示例中这种一般性的错误处理方法外,template包提供一种简便的方式:

...func index(rw http.ResponseWriter, r *http.Request) {t := template.Must(template.ParseFiles("index.html"))msg := "hello world!"t.Execute(rw, msg)}...

其实这里的template.Must函数内容很简单:

func Must(t *Template, err error) *Template {if err != nil {panic(err)}return t}

Must函数的参数正好和ParseFiles的返回值类型一致,所以嵌套Must函数到ParseFiles函数就可以实现“模版解析失败时程序中断”,这样就可以不用写额外的错误处理程序。

其实类似Must函数的做法在Go中相当常见,Go语言习惯将类似的“如果不成功就中断程序运行”的函数用MustXXX来进行命名。

动作

模版中的{{ . }}这样的特殊语法就是相应模版引擎定义的“模版语法”,也可以叫做“模版标签”,在Go中常将其称作“动作”(action),所以下面我也会使用动作来称呼。

模版引擎正是通过解析模版中定义的动作,再结合给定的数据来“渲染”模版。

最简单的动作语句是{{ . }},它代表当前模版绑定的最初始的数据,也就是对应到执行模版时传入的那个数据:

t.Execute(rw, msg)

在这个示例中就是msg这个变量。

如果模版绑定的数据是一个复合结构,比如结构体,还可以访问其属性:

...func index(rw http.ResponseWriter, r *http.Request) {t := template.Must(template.ParseFiles("index.html"))p := Person{Name: "icexmoon",Age:19,}t.Execute(rw, p)}...

index.html模版:

<!DOCTYPE html><html><head></head><body><h1>name:{{ .Name }}</h1><h1>age:{{ .Age }}</h1></body></html>

当然如果动作只能简单地替换数据,模版引擎的作用就不是很大,所以template还支持一些更复杂的动作:

  • 条件动作
  • 循环动作
  • 替换动作
  • 包含动作

循环动作

循环是最长使用的动作,因为服务端查询到的结果往往是批量的结构化的数据,在HTML中往往要用<table>之类的组件进行展示,如果模版引擎支持循环,前端的工作量就可以减少很多。

循环动作的语法是:

{ range param }html sentence{ end }

这里的param被称作“动作参数”,指代循环语句的“迭代目标”。动作参数可以是变量、常量、结构体、甚至一个函数。

我们来看一个用页面展示学生成绩的示例:

...func index(rw http.ResponseWriter, r *http.Request) {t := template.Must(template.ParseFiles("index.html"))scores := make(map[string][]float64)scores["Li lei"] = []float64{55, 60, 80}scores["Han Meimei"] = []float64{90, 60, 33}scores["Jack Chen"] = []float64{80, 100, 95}t.Execute(rw, scores)}...

html模版使用表格展示成绩:

<!DOCTYPE html><html><head></head><body><table><tr><th>姓名</th><th>语文</th><th>数学</th><th>英语</th></tr>{{ range $key,$val := . }}<tr><td>{{ $key }}</td>{{ range $val }}<td>{{ . }}</td>{{ end }}</tr>{{ end }}</table></body></html>

效果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kYAnI77f-1640598382178)(https://image2.icexmoon.xyz/image/image-20211226194053782.png)]

这里学生成绩scores是一个类似“二维数组”的结构,所以在模版中需要使用两层循环来进行遍历。

这里{{ range $key,$val := . }}语句中的$key$val模版变量,也就是在模版中定义的变量。利用这种特性可以获取作为scores索引的学生姓名。

循环动作还有一种“变种”形式:

{{ range param }}html sentence.{{ else }}html sentence.{{ end }}

如果循环语句中的param参数为空,就会直接执行else中的部分,而不会执行循环语句本身。

我们来看改进后的成绩显示模版:

...{{ range $key,$val := . }}<tr><td>{{ $key }}</td>{{ range $val }}<td>{{ . }}</td>{{ end }}</tr>{{ else }}<tr><td colspan="4">没有可显示的成绩</td></tr>{{ end }}...

现在如果服务端给该模版绑定一个空数据:

...func index(rw http.ResponseWriter, r *http.Request) {t := template.Must(template.ParseFiles("index.html"))t.Execute(rw, nil)}...

页面就会显示:

image-20211226201213078

判断动作

判断动作也比较常用,我们可以利用判断动作来实现根据内容不同,显示不同的HTML内容。

判断动作的基本语法是:

{{ if param }}html sentence.{{ else }}html sentence.{{ end }}

这里修改一下上边的成绩单,假如我们需要用不同的颜色标记成绩:

...func index(rw http.ResponseWriter, r *http.Request) {t := template.New("index.html")fm := template.FuncMap{"score_pass": func(score float64) bool { return score > 60 }}t.Funcs(fm)t.ParseFiles("index.html")scores := make(map[string][]float64)scores["Li lei"] = []float64{55, 60, 80}scores["Han Meimei"] = []float64{90, 60, 33}scores["Jack Chen"] = []float64{80, 100, 95}t.Execute(rw, scores)}...

index.html模版:

<!DOCTYPE html><html><head></head><body><table><tr><th>姓名</th><th>语文</th><th>数学</th><th>英语</th></tr>{{ range $key,$val := . }}<tr><td>{{ $key }}</td>{{ range $val }}{{if score_pass . }}<td style="background-color: greenyellow;">{{ . }}</td>{{ else }}<td style="background-color: red;">{{ . }}</td>{{ end }}{{ end }}</tr>{{ end }}</table></body></html>

这里的{{ if score_pass }}实际上是定义了一个模版函数,关于模版函数的内容我们在后边说明。

这里利用模版函数来对成绩进行筛选,对及格的成绩用绿色背景色,不及格的成绩用红色背景色,最后的效果是:

image-20211226200410851

替换动作

使用替换动作可以将当前“块”中的绑定数据“临时替换”。

原书中称为“设置动作”,但我觉得替换动作这个名称更贴切。

替换动作的基本语法是:

{{ with param }}html sentence.{{ end }}

我们可以利用替换动作给成绩表添加一个“表头”:

 <table><tr>{{ with "成绩汇总表" }}<th colspan="4">{{ . }}</th>{{ end }}</tr><tr><th>姓名</th><th>语文</th><th>数学</th><th>英语</th></tr>{{ range $key,$val := . }}...{{ end }}</table>

原本模版中{{ . }}代表服务端绑定到模版的scores变量,包含所有的成绩信息。但是在{{ with "成绩汇总表" }}这个替换动作中,{{ . }}的内容变成了with中指定的参数。并且这种替换效果只会发生在with块中,并不会影响到之后的模版。

替换动作也存在一种变种形式:

{{ with param }}html sentence.{{ else }}html sentence.{{ end }}

和循环动作的变种形式类似,会在param为空的时候直接执行else语句。

这里依然使用成绩表标题作为示例:

...<tr>{{ with "" }}<th colspan="4">{{ . }}</th>{{ else }}<th colspan="4">成绩汇总表</th>{{ end }}</tr>...

因为示例中的替换动作参数是空字符串,所以最终依然会正常显示表头。

当然这里的这个示例并不是很合适,显得完全没有必要,只是用于说明替换动作的用法。

包含动作

使用包含动作可以让一个模板包含另一个模板。

包含动作的基本语义:

{{ temlate t_name }}

t_name是要包含的模版名称。如果没有人为给模版指定名称,模版的默认名称是所在的模版文件的名称。

下面我们使用包含动作给示例页面添加上头部和尾部,这也是绝大多数网站很常见的做法。因为头部和尾部几乎所有网站的页面都会使用相同素材,所以使用模版可以提高HTML代码复用。

<!DOCTYPE html><html><head>{{ template "header.html" }}</head><body>...</body><footer>{{ template "footer.html" }}</footer></html>

header.html内容:

<h1>Welcome to my homepage</h1><p>My name is icexmoon</p>

footer.html内容:

<h2>All copyright by icexmoon.xyz</h2>

此外还需要修改服务端代码,加载相应的模版文件:

...func index(rw http.ResponseWriter, r *http.Request) {...t.ParseFiles("index.html", "header.html", "footer.html")...}...

最终呈现的效果:

image-20211227113353907

ParseFiles方法可以接受不止一个模版文件作为参数,此时模版是一个包含了多个模版文件的“模版集”。

模版集在执行并生成最终HTML文件时,需要确定以哪个模版文件作为“骨架”(或者说入口模版文件),默认情况下执行t.Execute会选择模版集的第一个模版文件。如果要使用指定模版文件,则需要:

...func index(rw http.ResponseWriter, r *http.Request) {t.ParseFiles("index.html", "header.html", "footer.html")...t.ExecuteTemplate(rw, "index.html", scores)}...

通过ExecuteTemplate方法可以在执行模版时使用指定的模版作为入口模版。

管道

html/template模版支持一种被称作“管道”的语法,熟悉Linux下Shell的应该不陌生,实际上两者的概念和写法都几乎没有区别。

管道可以和任意的动作结合使用,比如和条件动作:

{{ if param1|param2|param3 }}html sentence.{{ end }}

这样做就会先执行param1|param2|param3,具体方式是将param1的结果代入param2,再将param2的结果代入param3,最后再执行if判断。

这里以之前添加的判断成绩是否及格的判断动作作为示例:

...{{if score_pass . }}<td style="background-color: greenyellow;">{{ . }}</td>{{ else }}<td style="background-color: red;">{{ . }}</td>{{ end }}...

原本的条件动作中参数是score_pass .score_pass是一个函数,用于判断成绩是否及格,.表示当前遍历到的成绩。可以使用管道来替换这种写法:

...{{ if . | score_pass }}<td style="background-color: greenyellow;">{{ . }}</td>{{ else }}<td style="background-color: red;">{{ . }}</td>{{ end }}...

.|score_pass意味着先执行.,结果是当前的成绩,然后将其作为参数传递给score_pass函数,执行后得到一个bool值再用于条件动作进行判断。

虽然管道写法比直接调用函数的方式更繁琐,但管道有其存在的意义:对于某些需要多个函数进行“级联”调用复杂处理,可以用管道实现,比如. | func1 | func2 | func3

函数

模版中可以使用自定义函数,这点在之前示例中已经有过展示,现在详细说明如何做到这一点。

将函数绑定到模版的关键代码是:

...t := template.New("index.html")fm := template.FuncMap{"score_pass": func(score float64) bool { return score > 60 }}t.Funcs(fm)...

这其中template.FuncMap是一个映射:

type FuncMap map[string]interface{}

其键就是模版中调用函数时的函数名,值是函数变量(这里直接使用了匿名函数)。

构造好FuncMap映射后,使用template.Funcs方法将其绑定到模版实例即可。

需要注意的是,绑定函数的工作需要在解析模版之前完成,否则模版中是无法识别到对应名称的函数的。

这也不难理解,解析模版本来就是为了识别模版中的语法来构建对绑定变量和函数的映射关系。

在模版中调用函数就简单了:

{{ if score_pass . }}

当然也可以使用之前介绍的管道方式调用。

模版中除了可以使用自定义函数以外,还可以使用一些定义好的内建函数,比如:

...{{ if . | score_pass }}<td style="background-color: greenyellow;">{{ printf "%.2f" . }}</td>{{ else }}<td style="background-color: red;">{{ printf "%.2f" . }}</td>{{ end }}...

这里的printf实际上就是fmt.Sprintf函数的别名。

遗憾的是我并没有在fmt包或template包的文档中找到相关的内建函数列表或说明,如果有人找到了,麻烦告知一下。

最后要说明的是,模版使用函数是有限制的,即绑定到模版的函数的返回值只能是一个或者包含错误信息的两个。之所以会有这种限制,大概是考虑到超过两个返回值会给管道带来一些麻烦。

上下文感知

模版中的语法可以“感知”所处的上下文环境,并因环境的不同做出相应的改变,这种特点被称作上下文感知

上下文感知的最大用途是对嵌入模版的文本进行“转义”。

来看一个简单的例子:

...<body><h1>{{ . }}</h1><a href="{{ . }}"></a><button onclick="alert('{{ . }}')">click</button></body>...

这里使用模版语法{{ . }}嵌入文本的地方分别是HTML的h1标签、锚点的链接属性、按钮的点击事件(实际上就是js代码)。

我们通过后台代码给该模版绑定一个字符串:

...func index(rw http.ResponseWriter, r *http.Request) {t := template.New("index.html")t.ParseFiles("index.html", "header.html", "footer.html")t.ExecuteTemplate(rw, "index.html", "<Hello world!>")}...

此时用浏览器访问:

image-20211227151910819

如果点击按钮,看到的弹窗内容也是正常的<Hello world!>

似乎这没有什么,但如果你查看页面HTML源码,或者使用curl工具查看响应报文:

❯ curl -i 127.0.0.1:8080/indexHTTP/1.1 200 OKDate: Mon, 27 Dec 2021 07:17:58 GMTContent-Length: 201Content-Type: text/html; charset=utf-8<!DOCTYPE html><html><body><h1>&lt;Hello world!></h1><a href="%3cHello%20world!%3e"></a><button onclick="alert('\u003cHello world!\u003e')">click</button></body></html>

就会发现三处模版语言替换的文本使用了三种不同的编码方式进行了转义。这其中锚点的链接属性转义我们应当很熟悉,就是URL编码,h1标签位置的编码方式是HTML对特殊字符的编码,按钮点击事件内是js对特殊字符的编码。

最妙的是我们使用模版嵌入文本时并没有告诉模版应当进行何种转义,但模版“自动”以正确的方式完成了转义,并且页面可以正常运行,这就是上下文感知的功劳。

可能你会困惑,这么做的意义何在。答案是可以防范某些形式的恶意代码攻击,比如XSS。

防御XSS攻击

XSS(cross site scripting)全称跨站脚本攻击。实际上是通过一些漏洞(比如网站的数据提交功能),将恶意js代码提交到网站,从而让网站在加载页面时加载相应的代码,以达成某些攻击者的目的。

下面我们看一个简单的XSS攻击示例。

这里有一个很简单的Web应用,有两个页面,一个页面有一个textarea用于用户输入信息,然后通过按钮提交:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GXyjEK3n-1640598382205)(https://image2.icexmoon.xyz/image/image-20211227155215409.png)]

然后会跳转到另一个页面显示刚才填充的内容:

image-20211227155242200

前后端的代码如下:

submit.html

<!DOCTYPE html><html><body><form action="/submit" method="post"><textarea name="comment" rows="10" cols="30"></textarea><br/><input type="submit" value="submit"/></form></body></html>

show.html

<html><body>comment:{{ . }}<br/><button onclick="location.href='/index'">back</button></body></html>

main.go

package mainimport ("html/template""net/http")func index(rw http.ResponseWriter, r *http.Request) {t := template.New("submit.html")t.ParseFiles("submit.html")t.Execute(rw, nil)}func submit(rw http.ResponseWriter, r *http.Request) {comment := r.PostFormValue("comment")t := template.Must(template.ParseFiles("show.html"))t.Execute(rw, comment)}func main() {http.HandleFunc("/index", index)http.HandleFunc("/submit", submit)http.ListenAndServe(":8080", http.DefaultServeMux)}

正常情况下这个应用可以正常运行,并不会有什么问题,但如果有心人输入的内容并非普通文本,而是js代码:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fcQarmdr-1640598382210)(https://image2.icexmoon.xyz/image/image-20211227155623405.png)]

就可能会在加载内容的页面加载相应的代码,从而产生一些安全问题,这就是XSS攻击。

幸运的是因为之前说的上下文感知功能,使用template库并不会产生这样的漏洞。/submit页面显示的内容都经过了合理转义,不会让可能存在的恶意代码运行:

<html><body>comment:<script>alert('hacked!');</script><br/><button onclick="location.href='/index'">back</button></body></html>

不使用转义

在某些情况下可能开发者并不希望模版在替换文本时自动转义,此时可以:

...func submit(rw http.ResponseWriter, r *http.Request) {comment := r.PostFormValue("comment")t := template.Must(template.ParseFiles("show.html"))t.Execute(rw, template.HTML(comment))}...

其实template.HTML是一个template包定义的具名类型:

type HTML string

所以template.HTML(comment)实际上并不是函数调用,而是类型转换。将普通的字符串转换为HTML类型后,模版就不会对其进行转义。

此时再尝试提交js代码:

<script>alert('hacked!');</script>

就可能在跳转到/show页面时加载XSS攻击的代码:

image-20211227160612419

之所以是可能,是因为不同的浏览器对此的处理方式不同,某些浏览器提供了“自主”的XSS防御,这种功能可以通过响应报文头X-XSS-Protection开启或关闭:

...func submit(rw http.ResponseWriter, r *http.Request) {comment := r.PostFormValue("comment")t := template.Must(template.ParseFiles("show.html"))rw.Header().Set("X-XSS-Protection", "1")t.Execute(rw, template.HTML(comment))}...

1表示开启0表示关闭,更多关于X-XSS-Protection报文头的内容请阅读[这里](X-XSS-Protection (Headers) - HTTP 中文开发手册 - 开发者手册 - 云+社区 - 腾讯云 (tencent.com))。

实际上现代浏览器提供了更为安全的CSP机制(Content-Security-Policy)用于防范XSS攻击。要添加该功能可以通过给HTML添加meta标签:

<meta http-equiv="Content-Security-Policy" content="default-src 'self'; img-src https://*; child-src 'none';">

或者在响应报文中加入Content-Security-Policy报文头。

更多的CSP内容可以阅读[这里](Content-Security-Policy - HTTP | MDN (mozilla.org))。

嵌套模版

在前边介绍包含动作的时候鉴定单介绍过如何用一个模版加载另一个模版。其实更常见的使用模版的方式是给模版进行命名,具体需要使用定义动作:

{{ define t_name }}template content.{{ end }}

有了定义动作,我们可以在一个模版文件中定义多个模版:

{{ define "html" }}<!DOCTYPE html><html><header>{{ template "header" }}</header><body><h1>This is index page.</h1></body><footer>{{ template "footer" }}</footer></html>{{ end }}{{ define "header" }}<h1>Welcome to my homepage</h1><p>My name is icexmoon</p>{{ end }}{{ define "footer" }}<h2>All copyright by icexmoon.xyz</h2>{{ end }}

加载模版:

...func index(rw http.ResponseWriter, r *http.Request) {t := template.Must(template.ParseFiles("index.html"))t.ExecuteTemplate(rw, "html", nil)}...

但显然这样做并不利于代码维护,所以正确的做法是将代码拆分:

index.html

{{ define "html" }}<!DOCTYPE html><html>{{ template "header" }}<body><h1>This is index page.</h1></body>{{ template "footer" }}</html>{{ end }}

header.html

<header>{{ define "header" }}<h1>Welcome to my homepage</h1><p>My name is icexmoon</p>{{ end }}</header>

footer.html

<footer>{{ define "footer" }}<h2>All copyright by icexmoon.xyz</h2>{{ end }}</footer>

加载模版:

...func index(rw http.ResponseWriter, r *http.Request) {t := template.Must(template.ParseFiles("index.html", "footer.html", "header.html"))t.ExecuteTemplate(rw, "html", nil)}...

我们甚至可以给“子模版”传值:

header.html

<header>{{ define "header" }}<h1>Welcome to my homepage</h1><p>My name is icexmoon, Today is {{ . }}</p>{{ end }}</header>

index.html:

{{ define "html" }}<!DOCTYPE html><html>{{ template "header" . }}<body><h1>This is index page.</h1></body>{{ template "footer" }}</html>{{ end }}

main.go

...func index(rw http.ResponseWriter, r *http.Request) {today := time.Now().Format("2006-01-02")t := template.Must(template.ParseFiles("index.html", "footer.html", "header.html"))t.ExecuteTemplate(rw, "html", today)}...

使用模版将可以重复使用的HTML部分分离的另外一个好处是,可以在服务端根据需要加载不同的模版。

比如我们要实现一个工作日和周末加载不同header的页面。

package mainimport ("html/template""net/http""time")func addHeaderAndFooterFiles(tFiles []string) []string {//工作日使用普通头尾,双休使用节日专用头尾switch time.Now().Weekday() {case time.Saturday:case time.Sunday:tFiles = append(tFiles, "header_week.html", "footer_week.html")default:tFiles = append(tFiles, "header.html", "footer.html")}return tFiles}func index(rw http.ResponseWriter, r *http.Request) {today := time.Now().Format("2006-01-02")tFiles := []string{"index.html"}tFiles = addHeaderAndFooterFiles(tFiles)t := template.Must(template.ParseFiles(tFiles...))t.ExecuteTemplate(rw, "html", today)}func main() {http.HandleFunc("/index", index)http.ListenAndServe(":8080", nil)}

现在我们就可以为周末和工作日设计两套不同的头部和尾部模版了,具体的前端代码这里不再展示,可以查看我的Github仓库。

使用块动作定义默认模版

Go1.6加入了一个新的“块动作”:

{{ block arg }}html sentence.{{ end }}

使用它可以定义一个默认的模版,比如:

{{ define "html" }}<!DOCTYPE html><html><head>{{ block "header" . }}<h1>This is a default header.</h1>{{ end }}</head><body><h1>{{ . }}</h1><h1>index page.</h1></body></html>{{ end }}

服务端定义两个不同的处理器,一个处理器加载header.html,一个不加载:

package mainimport ("html/template""net/http")func index(rw http.ResponseWriter, r *http.Request) {t := template.Must(template.ParseFiles("index.html", "header.html"))msg := "hello world!"t.ExecuteTemplate(rw, "html", msg)}func index2(rw http.ResponseWriter, r *http.Request) {t := template.Must(template.ParseFiles("index.html"))msg := "hello world!"t.ExecuteTemplate(rw, "html", msg)}func main() {http.HandleFunc("/index", index)http.HandleFunc("/index2", index2)http.ListenAndServe(":8080", http.DefaultServeMux)}

运行结果:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8kvuJQLy-1640598382214)(https://image2.icexmoon.xyz/image/image-20211227174201687.png)]

image-20211227174211812

可以看到,如果模版集中找不到对应的模版,块动作就会执行,否则就不执行。

本来以为这篇文章会很简单,依然写了2天…

谢谢阅读。

参考资料:

回帖
全部回帖({{commentCount}})
{{item.user.nickname}} {{item.user.group_title}} {{item.friend_time}}
{{item.content}}
{{item.comment_content_show ? '取消' : '回复'}} 删除
回帖
{{reply.user.nickname}} {{reply.user.group_title}} {{reply.friend_time}}
{{reply.content}}
{{reply.comment_content_show ? '取消' : '回复'}} 删除
回帖
收起
没有更多啦~
{{commentLoading ? '加载中...' : '查看更多评论'}}