Go 1.16 引入了 embed 包,将非 .go 文件打包成二进制文件,极大地方便了 Go 程序的部署。标准库html/template中也增加了ParseFS函数,它将embed.FS中包含的所有模板文件编译成一个模板树。
// templates.go
package templates
import (
"embed"
"html/template"
)
//go:embed views/*.html
var tmplFS embed.FS
type Template struct {
templates *template.Template
}
func New() *Template {
funcMap := template.FuncMap{
"inc": inc,
}
templates := template.Must(template.New("").Funcs(funcMap).ParseFS(tmplFS, "views/*.html"))
return &Template{
templates: templates,
}
}
// main.go
t := templates.New()
t.templates是一个全局模板,包含所有匹配的views/*.html模板,它们都是相关的,可以互相引用,模板的名字就是文件的名字,例如article.html.
进一步,我们为*Template类型定义了一个Render方法,它实现了Echo web框架的Renderer接口。
// templates.go
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
return t.templates.ExecuteTemplate(w, name, data)
}
然后,您可以为 Echo 指定渲染器,以便在每个处理程序处生成 HTML 响应,只需将模板名称传递给 c.Render 函数即可。
// main.go
func main() {
t := templates.New()
e := echo.New()
e.Renderer = t
}
// handler.go
func (h *Handler) articlePage(c echo.Context) error {
id := c.Param("id")
article, err := h.service.GetArticle(c.Request().Context(), id)
...
return c.Render(http.StatusOK, "article.html", article)
}
由于t.templates模板包含了所有已解析的模板,所以每个模板名称都可以直接使用。
为了组装 HTML,我们需要使用模板继承。例如,为基本的HTML框架和<head>元素定义一个layout.html,并设置{{block "title"}}和{{block "content"}},其他模板继承layout.html,并填充或覆盖布局模板的同名块他们自己定义的块。
以下是layout.html模板的内容。
<!DOCTYPE html>
<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.0">
<title>{{block "title" .}}{{end}}</title>
<script src="/static/main.js"></script>
</head>
<body>
<div class="main">{{block "content" .}}{{end}}</div>
</body>
</html>
对于其他模板,您可以参考(继承自)layout.html,并在layout.html模板中定义块。
例如,login.html 如下所示。
{{template "layout.html" .}}
{{define "title"}}Login{{end}}
{{define "content"}}
<form class="account-form" method="post" action="/account/login" data-controller="login">
<div div="account-form-title">Login</div>
<input type="phone" name="phone" maxlength="13" class="account-form-input" placeholder="Phone" tabindex="1">
<div class="account-form-field-submit ">
<button type="submit" class="btn btn-phone">Login</button>
</div>
</form>
{{end}}
article.html 也引用了 layout.html:
{{template "layout.html" .}}
{{define "title"}}<h1>{{.Title}}</h1>{{end}}
{{define "content"}}
<p>{{.URL}}</p>
<article>{{.Content}}</article>
{{end}}
我们希望 login.html 模板中定义的块在渲染它时覆盖 layout.html 中的块,并且在渲染 article.html 模板时也是如此。但事实并非如此,这取决于 Go 文本/模板的实现。在我们对ParseFS(tmplFS, "views/*.html")的实现中,假设先解析article.html,将其content块解析为模板名,那么当稍后解析login.html模板时,在其中也找到了content块, text/template 会用后面解析的内容覆盖同名的模板,所以当所有的模板都被解析后,我们的模板树中其实只有一个名为content的模板,也就是最后定义的content已解析的模板文件。
因此,我们在执行article.html模板时,有可能content模板不是这个模板定义的内容,而是其他模板定义的content。
社区已经针对这个问题提出了一些解决方案。例如,不是使用全局模板,而是在每次渲染时创建一个新模板,仅包含 layout.html 和子模板的内容。但这真的很乏味。事实上,当 Go 1.6 为文本/模板引入 block 指令 [1] 时,我们能够使用 Clone 方法做我们想做的事情,只需对上面的代码进行一些更改。
// templates.go
package templates
import (
"embed"
"html/template"
"io"
"github.com/labstack/echo/v4"
)
//go:embed views/*.html
var tmplFS embed.FS
type Template struct {
templates *template.Template
}
func New() *Template {
funcMap := template.FuncMap{
"inc": inc,
}
templates := template.Must(template.New("").Funcs(funcMap).ParseFS(tmplFS, "views/*.html"))
return &Template{
templates: templates,
}
}
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
tmpl := template.Must(t.templates.Clone())
tmpl = template.Must(tmpl.ParseFS(tmplFS, "views/"+name))
return tmpl.ExecuteTemplate(w, name, data)
}
可以看到这里只修改了Render函数。我们将不执行全局模板,而是将其克隆到一个新模板中,而这个新模板中的content 块可能不是我们想要的,所以这里我们解析一个我们最终会在其上渲染的子模板的内容这个全局模板的顶部,因此新添加的子模板的content 将覆盖以前的,可能不正确的content。我们的目标子模板引用了全局模板中的layout.html,这并没有冲突,而且由于从不执行全局模板(每次执行时我们在Render函数中克隆一个新的全局模板),所以也是干净的。当一个模板最终执行时,我们就有了一个干净的layout.html,里面有我们想要的content内容,相当于每次执行都会生成一个新的模板,里面只包含我们需要的布局模板和子模板。思路是一样的,只不过不是在执行模板时手动生成新模板,而是在Render函数中自动完成。
当然你也可以使用{{ template }}来引用子模板中的其他布局模板,只要这些布局模板不相互覆盖,你只需要指定目标子模板的名称即可执行时,模板引擎会自动使用其中定义的{{ template }}标签为我们查找布局模板,这些模板都在克隆的全局模板中。
[1]https://github.com/golang/go/commit/12dfc3bee482f16263ce4673a0cce399127e2a0d