Go 1.16 引入了 embed 包,将非 .go 文件打包成二进制文件,极大地方便了 Go 程序的部署。该ParseFS函数还添加到标准库 html/template 中,它将 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.


此外,我们Render为该*Template类型定义了一个方法,该方法实现Renderer了 Echo Web 框架的接口。


// 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>


<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模板中定义blocks。


例如,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.6block为文本/模板引入指令 [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.htmlcontent我们想要的内容,相当于每次执行都会生成一个新的模板,里面只包含我们需要的布局模板和子模板。思路是一样的,只不过不是在执行模板时手动生成新模板,而是在Render函数中自动完成。


当然,也可以使用{{ template }}来引用子模板中的其他布局模板,只要这些布局模板不相互覆盖,执行时只需要指定目标子模板的名称,模板引擎会自动使用其中{{ template }}定义的标签为我们寻找布局模板,这些模板都在克隆的全局模板中。


[1] https://github.com/golang/go/commit/12dfc3bee482f16263ce4673a0cce399127e2a0d