【问题标题】:How to use base template file for golang html/template?如何为 golang html/template 使用基本模板文件?
【发布时间】:2016-04-14 08:44:44
【问题描述】:

拥有 gin-gonic 网络应用程序。

有3个文件:

1) base.html -- 基本布局文件

<!DOCTYPE html>
<html lang="en">
<body>

header...

{{template "content" .}}

footer...

</body>
</html>

2) page1.html,用于 /page1

{{define "content"}}
<div>
    <h1>Page1</h1>
</div>
{{end}}
{{template "base.html"}}

3) page2.html,用于/page2

{{define "content"}}
<div>
    <h1>Page2</h1>
</div>
{{end}}
{{template "base.html"}}

问题在于 /page1 和 /page2 使用一个模板 - page2.html。我认为我对这样的结构有误解:{{define "content"}}{{template "base.html"}}

请问,您能举例说明如何在 golang 中使用基本布局吗?

【问题讨论】:

    标签: go


    【解决方案1】:

    您可以使用 base.html,只要您将模板与“内容”一起解析,如下所示:

    base.html

    {{define "base"}}
    <!DOCTYPE html>
    <html lang="en">
    <body>
    
    header...
    
    {{template "content" .}}
    
    footer...
    
    </body>
    </html>
    {{end}}
    

    page1.html

    {{define "content"}}
    I'm page 1
    {{end}}
    

    page2.html

    {{define "content"}}
    I'm page 2
    {{end}}
    

    然后 ParseFiles 与 ("your-page.html", "base.html") 和 ExecuteTemplate 与您的上下文。

    tmpl, err := template.New("").ParseFiles("page1.html", "base.html")
    // check your err
    err = tmpl.ExecuteTemplate(w, "base", yourContext)
    

    【讨论】:

    • 这对我不起作用。我使用 ParseGlob,所以所有的 html 文件都被解析了。总是加载第二个模板,即使我在 ExecuteTempate 中指定了第一个模板。真不知道,为什么4年后还是这样……
    • @DasJott 您能否再次检查您是否正确设置了“定义”和“结束”块?如果您可以在 gist 或 Go Playground 上发布一些代码,这将有助于调试
    【解决方案2】:

    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框架和&lt;head&gt;元素定义一个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

    【讨论】:

      【解决方案3】:

      据我了解,当您使用ParseGlob() 时,Gin 会解析所有匹配的文件并从中创建一个模板对象。为了做你想做的事,你需要两个不同的模板(一个用于第 1 页,另一个用于第 2 页)。

      Gin documentation 说这是一个已知的限制并指出了克服它的方法:

      Gin 默认只允许使用一个 html.Template。检查 a multitemplate render 是否使用 go 1.6 block template 等功能。

      使用多模板库,您可以编写如下内容:

          render := multitemplate.NewRenderer()
      
          render.AddFromFiles("page1", "templates/base.html", "templates/page1.html")
          render.AddFromFiles("page2", "templates/base.html", "templates/page2.html")
      
          router := gin.Default()
          router.HTMLRender = render
      
          // Later
          ginContext.HTML(200, "page1", gin.H{
                  "title": "The Wonderful Page One",
              })
      

      这需要比我希望的更多的手动设置,但可以完成工作。

      【讨论】:

        【解决方案4】:

        避免使用地图并在单个模板中工作的最简单方法:

        base.html

        <!DOCTYPE html>
        <html lang="en">
        <body>
        
        header...
        
        {{block "content" .}}{{end}}
        
        footer...
        
        </body>
        </html>
        

        page1.html

        {{template "base.html" .}}
        {{define "content"}}This is page 1{{end}}
        

        page2.html

        {{template "base.html" .}}
        {{define "content"}}This is page 2{{end}}
        
        t := template.Must(template.ParseGlob("*.html"))
        err := t.ExecuteTemplate(w, "page1.html", context)
        err := t.ExecuteTemplate(w, "page2.html", context)
        

        【讨论】:

        • 您的代码打印“这是第 2 页”两次(刚刚测试过),因为 page2.html 覆盖了块 "content"
        猜你喜欢
        • 2019-02-11
        • 1970-01-01
        • 2011-11-02
        • 2015-02-20
        • 2019-11-17
        • 2017-10-11
        • 1970-01-01
        • 2022-01-09
        • 1970-01-01
        相关资源
        最近更新 更多