当前这个网站没有用本人最熟悉的前端框架来开发。一是觉得没必要,博客本来就没多少特效,与服务器交互几乎为零,并没有多少技术;二是感觉那些框架打包出来文件有点臃肿,租了服务器,部署了博客才知道带宽有多贵,那些动辄几MB的js文件有点浪费带宽了(虽然可以优化打包文件,但还是比这种臃肿、更大);三是我算是前端小白,维护过公司老项目,那个用的是Java生成的HTML文件,感觉很神奇还能这么用。所以开发博客时选择了这种老式开发方式([🐕狗头保命]),这篇文章用来做个笔记,记录一下用法,省的下次开发新功能时还要去之前模板、搜索引擎找,汇总一下用的的语法和使用方式。
前置
所有代码都在GitHub上https://github.com/hongyun-robot/go-template。
博客目前部分html代码块渲染有bug。已修复本文用gin来当作服务器、动态生成HTML。gin的解析模板还是html/template包,所以语法一样。
textcopy code目录结构 --template --grammary.gohtml --layout.gohtml --header.gohtml --footer.gohtml --main.go
gocopy codepackage main import ( "github.com/gin-gonic/gin" "net/http" ) func main() { r := gin.New() r.LoadHTMLGlob("template/*") r.GET("/grammar", func(c *gin.Context) { c.HTML(http.StatusOK, "layout", gin.H{}) }) // Listen and Server in 0.0.0.0:8080 r.Run(":8888") }
定义模板
在文件中使用{{ define "name" }}
和 {{ end }}
来定义一个模板。
使用 {{ define "layout" }}
语法定义了模板文件。使用 {{ .hello }}
语法来渲染数据。
htmlcopy code{{/* template/layout */}} {{ define "layout" }} <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>go-template 语法</title> </head> <body> <p>{{ .hello }}</p> </body> </html> {{ end }}
gocopy coder.GET("/grammar", func(c *gin.Context) { c.HTML(http.StatusOK, "layout", gin.H{ "hello": "Hello World", }) })
作用域
在上一个例子中使用了 {{ .hello }}
语法,这个里面的 .
表示当前全局作用域对象值。对于 gin 表示 gin.H{}
这个map 变量;对于 func (t *Template) Execute(wr io.Writer, data any) error
方法等价于 data
变量。
整个文件、range
模块、with
模块和 block
模块都是不同的作用域。习惯把整个文件作用域称为全局作用域,其他则称为局部作用域。
全局作用域值 .
称为 dot
。
!!! abstract 注意
在循环局部作用域中 dot
指向循环 pipelines
值而不是全局对象值,$
指向全局对象值。建议用 $
替换 dot
来进行使用。
!!!
嵌套模板
使用 {{ template "name" pipeline? }}
语法调用其他模板,实现模板嵌套、重复使用、模板递归等操作。
pipeline
可以视为管道,将数据传递到调用模板里面,改变调用模板里面全局作用域值,将 .
设为传递过来的值。
可以将一个大模板拆分成多个小模版,维护更方便;将重复逻辑部分,要在不同位置使用的拆分出来,使用时直接调用模板。我觉得叫组件更适合。
定义了三个模板文件,grammary、header、footer。在 grammary 模板中引入了header和footer模板,并且将自己的全局作用域值dot传递给这两个模板。
htmlcopy code{{/* template/grammar */}} {{ define "grammar" }} {{ template "header" . }} <p>由我来组成身体 {{ .hello }} 。</p> {{ template "footer" . }} {{ end }}
htmlcopy code{{/* template/header */}} {{ define "header" }} <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <p>{{ .hello }}我来组成头部</p> {{ end }}
htmlcopy code{{/* template/footer */}} {{ define "footer" }} <p>{{ .hello }}我来组成底部</p> </body> </html> {{ end }}
gocopy coder.GET("/grammar", func(c *gin.Context) { c.HTML(http.StatusOK, "grammar", gin.H{ "hello": "Hello World", }) })
去除空白
在上面渲染出来源码有很多空行。go在非动态数据上渲染是所有文字、空白和换行符都会保留。
在动态内容解析上使用 {{-
和 -}}
可以去除前后面空白或者换行符。对于动态内容又分为两种情况
- 变量等使用,前后面有空格的
{{ .hello }}
- 调用模板、循环等定义的
{{ template "name" pipeline? }}
这一行,会渲染空白行
对上面代码做简单修改,去除空白内容以及空白行:
规则:
- template 调用模板不会生成任何空白和换行符
- define 定义模板会在这 define 后面生成空白行
- end 结束模板定义,会在 end 前面生成空白行
- 注释不会生成任何空白符或者换行符
建议:去除空白行,可以形成类似代码压缩效果,减少网络传输。对于浏览器来说影响几乎不计。
- 任何的
{{}}
前后换行符和空白都会保留,在不影响生成代码样式上,尽量前后都加上-
,去除空白和换行符 - 模板定义:
{{ define "name" -}}
、{{- end }}
这样定义模板,可以去除定义模板前后的空白行;
htmlcopy code{{/* template/grammar */}} {{ define "grammar" -}} {{ template "header" . }} <p>由我来组成身体 {{- .hello -}} 。</p> {{ template "footer" . }} {{- end }}
htmlcopy code{{/* template/footer */}} {{ define "footer" -}} <p>{{ .hello }}我来组成底部</p> </body> </html> {{- end }}
htmlcopy code{{/* template/header */}} {{ define "header" -}} <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <p>{{ .hello }}我来组成头部</p> {{- end }}
注释
模板中 {{/* comments */}}
这样书写注释
管道 Pipelines
在模板中,Pipelines 的概念是传递数据,可能是链状的,也可能是一个函数的参数。只要能产生数据都属于 Pipelines。
以下几种示例都会生成 “output”
htmlcopy code{{/* template/grammar */}} ...... <p> {{"\"output\""}} 字符串常量 <p> {{`"output"`}} 原始字符串常量 </p> <p> {{printf "%q" "output"}} 函数调用 </p> <p> {{"output" | printf "%q"}} 函数调用,最后一个参数来自前一个command的返回值 </p> <p> {{printf "%q" (print "out" "put")}} 加括号的参数 </p> <p> {{"put" | printf "%s%s" "out" | printf "%q"}} 玩出花的管道的链式调用 </p> <p> {{"output" | printf "%s" | printf "%q"}} 管道的链式调用 </p>
变量
在go中使用 $name := pipelines
语法来定义一个变量,通常在循环中使用。
$
是一个特殊变量,代指全局作用域对象。在无其他作用域$ == .
dot- 声明的变量仅在当前作用域有效
- 内层作用域可以访问外层变量,外层不可以访问内层变量
htmlcopy code{{/* template/grammar */}} ...... {{ range $value, $key := $}} <p>{{$key}} - {{$value}}</p> <p>{{ . }}</p> <p>{{ $ }}</p> {{ end }} ......
循环
有两种语法可以遍历 map
、 slice
、 array
、 channel
等类型。
还提供了 {{ break }}
和 {{ continue }}
来跳出循环和跳过本次循环。
{{ range pipeline }} T1 {{ end }}
{{ range pipeline }} T1 {{ else }} T0 {{ end }}
第二种语法: 如果 pipeline
为真执行 T1 代码块,否则执行 T0 代码块。相当于加了个if判断。
当变量只有一个时,此时变量指向每次迭代的值;
左侧变量有两个时,此时第一个变量指向迭代的
key
值,第二个指向迭代值;- 如果迭代对象为map类型时,则第一个变量为 map key值;
- 如果迭代对象为slice、array类型时,第一个变量为每次迭代的下标(索引值)。
在循环作用域中,
.
指向迭代的值,$
才指向全局作用域对象值。- 不管是 map 还是slice等迭代,
.
都指向迭代值。
- 不管是 map 还是slice等迭代,
{{ break }}
和{{ continue }}
用法 go 语法一致,不需要加{{ end }}
htmlcopy code{{/* template/grammar */}} ...... {{ range $value := .map}} <p>{{$value}}</p> <p>. = {{ . }}</p> <p>$ = {{ $ }}</p> {{ end }} {{ range $key, $value := .map}} <p>{{$key}} - {{$value}}</p> {{ end }} {{ range $value := .slice}} <p>{{$value}}</p> {{ end }} {{ range $key, $value := .slice}} {{ if eq $key 1 }} {{ continue }} {{ end }} <p>{{$key}} - {{$value}}</p> <p>. = {{ . }}</p> {{ end }} ......
gocopy code/* main.go */ "map": map[string]string{"key1": "value1", "key2": "value2"}, "slice": []string{"slice1", "slice2", "slice3"},
函数
执行模板时,函数从两个函数字典中查找:首先是模板函数字典,然后是全局函数字典。一般不在模板内定义函数,而是使用Funcs方法添加函数到模板里。.Method [Argument...]
函数可以有1~2个返回值,如果有第二个返回值,那么必须为 error 接口类型。如果第二个返回了非 nil 类型那么模板执行会中断并返回给调用者错误。
函数或者方法支持链式调用,.Field1.Key1.Method1.Field2.Key2.Method2
,深度不限。
- 自定义函数一定要在加载模板文件之前加载,不然使用会报错
- 自定义函数还可以当全局变量来使用,返回查找数据库或者缓存值
自定义函数
gocopy code/* main.go */ func add(x, y int) int { return x + y } r.SetFuncMap(template.FuncMap{ "add": add, }) r.LoadHTMLGlob("template/*")
htmlcopy code{{/* template/grammar */}} ...... <p>1 + 2 = {{ add 1 2 }}</p>
以下是内置函数列表:
textcopy codeand 函数返回它的第一个empty参数或者最后一个参数; 就是说"and x y"等价于"if x then y else x";所有参数都会执行; or 返回第一个非empty参数或者最后一个参数; 亦即"or x y"等价于"if x then x else y";所有参数都会执行; not 返回它的单个参数的布尔值的否定 len 返回它的参数的整数类型长度 index 执行结果为第一个参数以剩下的参数为索引/键指向的值; 如"index x 1 2 3"返回x[1][2][3]的值;每个被索引的主体必须是数组、切片或者字典。 print 即fmt.Sprint printf 即fmt.Sprintf println 即fmt.Sprintln html 返回其参数文本表示的HTML逸码等价表示。 urlquery 返回其参数文本表示的可嵌入URL查询的逸码等价表示。 js 返回其参数文本表示的JavaScript逸码等价表示。 call 执行结果是调用第一个参数的返回值,该参数必须是函数类型,其余参数作为调用该函数的参数; 如"call .X.Y 1 2"等价于go语言里的dot.X.Y(1, 2); 其中Y是函数类型的字段或者字典的值,或者其他类似情况; call的第一个参数的执行结果必须是函数类型的值(和预定义函数如print明显不同); 该函数类型值必须有1到2个返回值,如果有2个则后一个必须是error接口类型; 如果有2个返回值的方法返回的error非nil,模板执行会中断并返回给调用模板执行者该错误; 布尔函数会将任何类型的零值视为假,其余视为真。 eq 如果arg1 == arg2则返回真 ne 如果arg1 != arg2则返回真 lt 如果arg1 < arg2则返回真 le 如果arg1 <= arg2则返回真 gt 如果arg1 > arg2则返回真 ge 如果arg1 >= arg2则返回真
条件判断
pipeline 判断条件为各数据类型的零值。例如
- 数值类型为
0
- 指针、接口为
nil
- array、slice、map或string则是len为0。
textcopy code{{if pipeline}} T1 {{end}} 如果pipeline的值为empty,不产生输出,否则输出T1执行结果。不改变dot的值。 Empty值包括false、0、任意nil指针或者nil接口,任意长度为0的数组、切片、字典。 {{if pipeline}} T1 {{else}} T0 {{end}} 如果pipeline的值为empty,输出T0执行结果,否则输出T1执行结果。不改变dot的值。 {{if pipeline}} T1 {{else if pipeline}} T0 {{end}} 用于简化if-else链条,else action可以直接包含另一个if;等价于: {{if pipeline}} T1 {{else}}{{if pipeline}} T0 {{end}}{{end}}
with新建作用域
with
用来将 .
值指向 pipeline 。
第二种语法也是加了 if 的语法糖格式。
textcopy code{{with pipeline}} T1 {{end}} {{with pipeline}} T1 {{else}} T0 {{end}}
block
我的理解为局部定义模板。
新建一个 component.gohtml
模板文件,在 grammar.gohtml
中调用这个模板文件,如果 block
定义的名称等于调用这个文件名称则使用 block
的内容,否则使用原本的模板文件。
htmlcopy code{{/* template/component */}} {{ define "component" }} <p>我是定义的组件 {{ .hello }}</p> {{ end }}
htmlcopy code{{/* template/component */}} <!-- 如果block "component1",则这里显示原本模板内容 --> <!-- 否则显示 block 定义的内容 --> {{ template "component"}} {{ block "component1" . }} <p>我是 block 默认内容</p> {{ end }}
评论 (0 条)