如何在golang的html/template中使用基础模板文件?

huangapple go评论67阅读模式
英文:

How to use base template file for golang html/template?

问题

你好!以下是使用基本布局在Go语言中的示例:

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"}}

在这个示例中,base.html 是基本布局文件,定义了整个网页的结构,包括头部、内容和底部。page1.html 和 page2.html 分别定义了各自的内容部分,并通过 {{define "content"}} 来指定内容的位置。然后,通过 {{template "base.html"}} 将内容插入到基本布局中。

这样,当访问 /page1 时,会加载 page1.html,并将其内容插入到 base.html 的 {{template "content" .}} 处;当访问 /page2 时,会加载 page2.html,并将其内容插入到 base.html 的 {{template "content" .}} 处。

希望这个示例对你有帮助!如果还有其他问题,请随时提问。

英文:

Have gin-gonic web app.

There are 3 files:

  1. base.html -- base layout file

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

    header...

    {{template "content" .}}

    footer...

    </body>
    </html>

  2. page1.html, for /page1

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

  3. page2.html, for /page2

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

The problem is that /page1 and /page2 use one template - page2.html. I think that I have misunderstanding of such constructions: {{define &quot;content&quot;}}, {{template &quot;base.html&quot;}}.

Please, can you show an example how to use base layouts in golang?

答案1

得分: 27

你可以使用base.html,只要你将模板与你的"content"一起解析,就像这样:

base.html

{{define "base"}}
<!DOCTYPE html>
<html lang="en">
<body>

header...

{{template "content" .}}

footer...

</body>
</html>
{{end}}

page1.html

{{define "content"}}
我是页面1
{{end}}

page2.html

{{define "content"}}
我是页面2
{{end}}

然后使用ParseFiles("your-page.html", "base.html")解析,再使用你的上下文执行ExecuteTemplate。

tmpl, err := template.New("").ParseFiles("page1.html", "base.html")
// 检查错误
err = tmpl.ExecuteTemplate(w, "base", yourContext)
英文:

You can use the base.html as long as you parse the template along with your "content", like so:

base.html

{{define &quot;base&quot;}}
&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;body&gt;

header...

{{template &quot;content&quot; .}}

footer...

&lt;/body&gt;
&lt;/html&gt;
{{end}}

page1.html

{{define &quot;content&quot;}}
I&#39;m page 1
{{end}}

page2.html

{{define &quot;content&quot;}}
I&#39;m page 2
{{end}}

then ParseFiles with ("your-page.html", "base.html"), and ExecuteTemplate with your context.

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

答案2

得分: 18

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)
}

然后,你可以通过将模板名称传递给c.Render函数,在每个处理程序中指定Echo的渲染器,以便生成HTML响应。

// 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,我们需要使用模板继承。例如,我们可以定义一个layout.html作为基本HTML框架和<head>元素,并设置{{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的text/template实现导致的。在我们的ParseFS(tmplFS, "views/*.html")实现中,假设首先解析了article.html,并将其content块解析为模板名称,然后当稍后解析login.html模板时,也在其中找到了一个content块,text/template将使用后解析的内容覆盖相同名称的模板,因此当所有模板都解析完毕时,实际上只有一个名为content的模板存在于我们的模板树中,它是最后一个解析的模板文件中定义的content

因此,当我们执行article.html模板时,content模板可能不是该模板中定义的内容,而是其他模板中定义的content

社区对这个问题提出了一些解决方案。例如,可以每次渲染时创建一个新模板,只包含layout.html和子模板的内容。但这样做非常繁琐。实际上,当Go 1.6引入text/template的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

英文:

Go 1.16 introduces the embed package, which packages non-.go files into binaries, greatly facilitating the deployment of Go programs. The ParseFS function was also added to the standard library html/template, which compiles all the template files contained in embed.FS into a template tree.

// templates.go
package templates

import (
	&quot;embed&quot;
	&quot;html/template&quot;
)

//go:embed views/*.html
var tmplFS embed.FS

type Template struct {
	templates *template.Template
}

func New() *Template {
	funcMap := template.FuncMap{
		&quot;inc&quot;: inc,
	}

	templates := template.Must(template.New(&quot;&quot;).Funcs(funcMap).ParseFS(tmplFS, &quot;views/*.html&quot;))
	return &amp;Template{
		templates: templates,
	}
}


// main.go
t := templates.New()

t.templates is a global template that contains all matching views/*.html templates, all of which are related and can be referenced to each other, and the name of the template is the name of the file, e.g. article.html.

Further, we define a Render method for the *Template type, which implements the Renderer interface of the Echo web framework.

// templates.go
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
	return t.templates.ExecuteTemplate(w, name, data)
}

You can then specify the renderer for Echo to facilitate generating HTML responses at each handler, simply by passing the name of the template to the c.Render function.

// 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(&quot;id&quot;)
	article, err := h.service.GetArticle(c.Request().Context(), id)
	...
	return c.Render(http.StatusOK, &quot;article.html&quot;, article)
}

Since the t.templates template contains all the parsed templates, each template name can be used directly.

In order to assemble HTMLs, we need to use template inheritance. For example, define a layout.html for the basic HTML frame and the &lt;head&gt; element, and set {{block &quot;title&quot;}} and {{block &quot;content&quot;}}, other templates inherit layout.html, and populate or override the layout template's blocks of the same name with their own defined blocks.

The following is the content of the layout.html template.

&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;

&lt;head&gt;
	&lt;meta charset=&quot;UTF-8&quot;&gt;
	&lt;meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;IE=edge&quot;&gt;
	&lt;meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;&gt;
	&lt;title&gt;{{block &quot;title&quot; .}}{{end}}&lt;/title&gt;
	&lt;script src=&quot;/static/main.js&quot;&gt;&lt;/script&gt;
&lt;/head&gt;

&lt;body&gt;
	&lt;div class=&quot;main&quot;&gt;{{block &quot;content&quot; .}}{{end}}&lt;/div&gt;
&lt;/body&gt;

&lt;/html&gt;

For other templates, you can refer to (inherit from) layout.html and define the blocks in the layout.html template.

For example, login.html reads as follows.

{{template &quot;layout.html&quot; .}}

{{define &quot;title&quot;}}Login{{end}}

{{define &quot;content&quot;}}
&lt;form class=&quot;account-form&quot; method=&quot;post&quot; action=&quot;/account/login&quot; data-controller=&quot;login&quot;&gt;
	&lt;div div=&quot;account-form-title&quot;&gt;Login&lt;/div&gt;
	&lt;input type=&quot;phone&quot; name=&quot;phone&quot; maxlength=&quot;13&quot; class=&quot;account-form-input&quot; placeholder=&quot;Phone&quot; tabindex=&quot;1&quot;&gt;
	&lt;div class=&quot;account-form-field-submit &quot;&gt;
		&lt;button type=&quot;submit&quot; class=&quot;btn btn-phone&quot;&gt;Login&lt;/button&gt;
	&lt;/div&gt;
&lt;/form&gt;
{{end}}

article.html also references layout.html:

{{template &quot;layout.html&quot; .}}

{{define &quot;title&quot;}}&lt;h1&gt;{{.Title}}&lt;/h1&gt;{{end}}

{{define &quot;content&quot;}}
&lt;p&gt;{{.URL}}&lt;/p&gt;
&lt;article&gt;{{.Content}}&lt;/article&gt;
{{end}}

We would expect the blocks defined in the login.html template to override the blocks in layout.html when rendering it, and also when rendering the article.html template. But that's not the case, and it's down to the Go text/template implementation. In our implementation of ParseFS(tmplFS, &quot;views/*.html&quot;), suppose article.html is parsed first and its content block is parsed as a template name, then when the login.html template is parsed later and acontent block is also found in it, text/template will overwrite the template of the same name with the later parsed content, so when all the templates are parsed, there is actually only one template named content in our template tree, which is the content defined in the last parsed template file.

Therefore, when we execute the article.html template, it is possible that the content template is not the content defined in this template, but the content defined in other templates.

The community has proposed some solutions to this problem. For example, instead of using a global template, a new template is created each time it is rendered, containing only the contents of layout.html and the sub-template. But this is really tedious. In fact, when Go 1.6 introduced the block directive [1] for text/template, we were able to do what we wanted with the Clone method, with just a few changes to the code above.

// templates.go
package templates

import (
	&quot;embed&quot;
	&quot;html/template&quot;
	&quot;io&quot;

	&quot;github.com/labstack/echo/v4&quot;
)

//go:embed views/*.html
var tmplFS embed.FS

type Template struct {
	templates *template.Template
}

func New() *Template {
	funcMap := template.FuncMap{
		&quot;inc&quot;: inc,
	}

	templates := template.Must(template.New(&quot;&quot;).Funcs(funcMap).ParseFS(tmplFS, &quot;views/*.html&quot;))
	return &amp;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, &quot;views/&quot;+name))
	return tmpl.ExecuteTemplate(w, name, data)
}

You can see that only the Render function has been modified here. Instead of executing the global template, we will clone it into a new template, and the content block in this new template may not be the one we want, so here we parse the content of a sub-template we will eventually render on top of this global template, so that the content of the newly added sub-template will overwrite the previous, possibly incorrect content. Our target sub-template references layout.html in the global template, which is not conflicted, and since the global template is never executed (we clone a new global template in the Render function each time it is executed), it is also clean. When a template is finally executed, we have a clean layout.html with the content content we want, which is equivalent to generating a new template each time we execute it, which contains only the layout template and sub-templates we need. The idea is the same, but instead of manually generating a new template when executing the template, it's done automatically in the Render function.

Of course, you can also use {{ template }} to refer to other layout templates in the sub-template, as long as these layout templates do not overwrite each other, you just need to specify the name of the target sub-template when executing, and the template engine will automatically use the {{ template }} tag defined in it to find the layout templates for us, which are all in the cloned global template.

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

答案3

得分: 2

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

Gin文档指出这是一个已知的限制,并指出了克服这个问题的方法:

> Gin默认只允许使用一个html.Template。查看multitemplate render以使用类似于go 1.6的block template功能。

使用multitemplate库,你可以这样编写代码:

	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

	// 后续代码
	ginContext.HTML(200, "page1", gin.H{
			"title": "The Wonderful Page One",
		})

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

英文:

As far as I understand, when you use ParseGlob(), Gin parses all the matching files and creates a single template object from them. For doing what you want, you'd need two different templates (one for page 1, another for page 2).

Gin documentation says this is a known limitation and points the way to overcome it:

> Gin allow by default use only one html.Template. Check a multitemplate render for using features like go 1.6 block template.

Using the multitemplate library, you can write something like this:

	render := multitemplate.NewRenderer()

	render.AddFromFiles(&quot;page1&quot;, &quot;templates/base.html&quot;, &quot;templates/page1.html&quot;)
	render.AddFromFiles(&quot;page2&quot;, &quot;templates/base.html&quot;, &quot;templates/page2.html&quot;)

    router := gin.Default()
	router.HTMLRender = render

	// Later
	ginContext.HTML(200, &quot;page1&quot;, gin.H{
			&quot;title&quot;: &quot;The Wonderful Page One&quot;,
		})

This requires more manual setup than I'd hope for, but gets the job done.

答案4

得分: -3

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

base.html

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

header...

{{block "content" .}}{{end}}

footer...

</body>
</html>

page1.html

{{template "base.html" .}}
{{define "content"}}这是页面1{{end}}

page2.html

{{template "base.html" .}}
{{define "content"}}这是页面2{{end}}
t := template.Must(template.ParseGlob("*.html"))
err := t.ExecuteTemplate(w, "page1.html", context)
err := t.ExecuteTemplate(w, "page2.html", context)
英文:

The easiest way which avoids a map and works in a single template:

base.html

&lt;!DOCTYPE html&gt;
&lt;html lang=&quot;en&quot;&gt;
&lt;body&gt;

header...

{{block &quot;content&quot; .}}{{end}}

footer...

&lt;/body&gt;
&lt;/html&gt;

page1.html

{{template &quot;base.html&quot; .}}
{{define &quot;content&quot;}}This is page 1{{end}}

page2.html

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

huangapple
  • 本文由 发表于 2016年4月14日 16:44:44
  • 转载请务必保留本文链接:https://go.coder-hub.com/36617949.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定