优化html/template的组合

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

Optimising html/template Composition

问题

我正在寻找一种更好(更快、更有组织)的方法来拆分我的Go模板。我强烈倾向于使用html/template(或其包装器),因为我相信它的安全模型。

  • 目前我使用template.ParseGlobinit()函数中解析所有的模板文件。
  • 我对生成的模板应用template.Funcs
  • 我在每个模板中设置一个$title(例如listing_payment.tmpl),并将其传递给内容模板。
  • 我了解到html/template一旦解析,会将模板缓存在内存中。
  • 我的处理程序只调用t.ExecuteTemplate(w, "name.tmpl", map[string]interface{}),并且在每个请求中不进行任何愚蠢的解析。
  • 我通过以下方式组合模板(这是我觉得有点笨拙的部分):
{{ $title := "Page Title" }}
{{ template "head" $title }}
{{ template "checkout" }}
{{ template "top" }}
{{ template "sidebar_details" . }}
{{ template "sidebar_payments" }}
{{ template "sidebar_bottom" }}

<div class="bordered-content">
  ...
      {{ template "listing_content" . }}
  ...
</div>

{{ template "footer" }}
{{ template "bottom" }}

我的三个问题是:

  1. 这种方式的性能如何?多个{{ template "name" }}标签是否会导致每个请求的性能损耗?在对较重的页面进行压力测试时,我看到了很多write - broken pipe错误。这可能只是由于套接字超时(即套接字在写入完成之前关闭)而不是某种每个请求的组合(如果我理解错了,请纠正我!)。
  2. 在html/template包的限制下,有没有更好的方法来实现这个?Django模板文档中的第一个示例接近我想要的。可以扩展基本布局并根据需要替换标题、侧边栏和内容块。
  3. 有点离题:当template.ExecuteTemplate在请求过程中返回错误时,有没有一种惯用的处理方式?如果我将writer传递给错误处理程序,页面上会出现混乱的内容(因为它会继续写入),但重定向似乎不符合惯用的HTTP方式。
英文:

I'm looking to see if there is a better (faster, more organised) way to split up my templates in Go. I strongly prefer to stick to html/template (or a wrapper thereof) since I trust its security model.

  • Right now I use template.ParseGlob to parse all of my template files in within init().

  • I apply template.Funcs to the resulting templates

  • I set a $title in each template (i.e. listing_payment.tmpl) and pass this to the content template.

  • I understand that html/template caches templates in memory once parsed

  • My handlers only call t.ExecuteTemplate(w, &quot;name.tmpl&quot;, map[string]interface{}) and don't do any silly parsing on each request.

  • I compose templates from multiple pieces (and this is the bit I find clunky) as below:

     {{ $title := &quot;Page Title&quot; }}
     {{ template &quot;head&quot; $title }}
     {{ template &quot;checkout&quot; }}
     {{ template &quot;top&quot; }}
     {{ template &quot;sidebar_details&quot; . }}
     {{ template &quot;sidebar_payments&quot; }}
     {{ template &quot;sidebar_bottom&quot; }}
    
     &lt;div class=&quot;bordered-content&quot;&gt;
       ...
           {{ template &quot;listing_content&quot; . }}
       ...
     &lt;/div&gt;
    
     {{ template &quot;footer&quot;}}
     {{ template &quot;bottom&quot; }}
    

My three questions are:

  1. Is this performant, or do the multiple {{ template &quot;name&quot; }} tags result in a potential per-request performance hit? I see a lot of write - broken pipe errors when stress testing heavier pages. This might just be due to socket timeouts (i.e. socket closing before the writer can finish) rather than some kind of per-request composition, though (correct me if otherwise!)

  2. Is there a better way to do this within the constraints of the html/template package? The first example in Django's template docs approaches what I'd like. Extend a base layout and replace the title, sidebar and content blocks as needed.

  3. Somewhat tangential: when template.ExecuteTemplate returns an error during a request, is there an idiomatic way to handle it? If I pass the writer to an error handler I end up with soup on the page (because it just continues writing), but a re-direct doesn't seem like idiomatic HTTP.

答案1

得分: 12

通过Reddit上的一些帮助,我设法找到了一个相当明智(且高效)的方法,可以实现以下功能:

  • 使用内容块构建布局
  • 创建有效地“扩展”这些布局的模板
  • 使用其他模板填充块(脚本、侧边栏等)

base.tmpl

&lt;html&gt;
&lt;head&gt;
    {{ template &quot;title&quot; .}}
&lt;/head&gt;
&lt;body&gt;
    {{ template &quot;scripts&quot; . }}
    {{ template &quot;sidebar&quot; . }}
    {{ template &quot;content&quot; . }}
&lt;footer&gt;
    ...
&lt;/footer&gt;
&lt;/body&gt;

</html>

index.tmpl

{{ define &quot;title&quot;}}&lt;title&gt;首页&lt;/title&gt;{{ end }}
// 我们必须在基本布局中定义每个块。
{{ define &quot;scripts&quot; }} {{ end }} 
{{ define &quot;sidebar&quot; }}
    // 我们有一个根据页面变化的两部分侧边栏
    {{ template &quot;sidebar_index&quot; }} 
    {{ template &quot;sidebar_base&quot; }}
{{ end }}
{{ define &quot;content&quot; }}
    {{ template &quot;listings_table&quot; . }}
{{ end }}

... 还有我们的 Go 代码,利用了在 这个 Stack Overflow 回答 中概述的 map[string]*template.Template 方法:

var templates map[string]*template.Template

var ErrTemplateDoesNotExist = errors.New(&quot;模板不存在。&quot;)

// 在程序初始化时加载模板
func init() {
	if templates == nil {
		templates = make(map[string]*template.Template)
	}

    templates[&quot;index.html&quot;] = template.Must(template.ParseFiles(&quot;index.tmpl&quot;, &quot;sidebar_index.tmpl&quot;, &quot;sidebar_base.tmpl&quot;, &quot;listings_table.tmpl&quot;, &quot;base.tmpl&quot;))
    ...
}

// renderTemplate 是 template.ExecuteTemplate 的包装函数。
func renderTemplate(w http.ResponseWriter, name string, data map[string]interface{}) error {
	// 确保模板存在于映射中。
	tmpl, ok := templates[name]
	if !ok {
		return ErrTemplateDoesNotExist
	}

	w.Header().Set(&quot;Content-Type&quot;, &quot;text/html; charset=utf-8&quot;)
	tmpl.ExecuteTemplate(w, &quot;base&quot;, data)

	return nil
}

根据初始基准测试(使用 wrk),在处理大量负载时似乎更高效,这很可能是因为我们不需要在每个请求中传递整个 ParseGlob 的模板。它还使得编写模板本身变得更简单。

英文:

With some help on Reddit I managed to work out a fairly sensible (and performant) approach to this that allows:

  • Building layouts with content blocks
  • Creating templates that effectively "extend" these layouts
  • Filling in blocks (scripts, sidebars, etc.) with other templates

base.tmpl

&lt;html&gt;
&lt;head&gt;
    {{ template &quot;title&quot; .}}
&lt;/head&gt;
&lt;body&gt;
    {{ template &quot;scripts&quot; . }}
    {{ template &quot;sidebar&quot; . }}
    {{ template &quot;content&quot; . }}
&lt;footer&gt;
    ...
&lt;/footer&gt;
&lt;/body&gt;

</html>

index.tmpl

{{ define &quot;title&quot;}}&lt;title&gt;Index Page&lt;/title&gt;{{ end }}
// We must define every block in the base layout.
{{ define &quot;scripts&quot; }} {{ end }} 
{{ define &quot;sidebar&quot; }}
    // We have a two part sidebar that changes depending on the page
    {{ template &quot;sidebar_index&quot; }} 
    {{ template &quot;sidebar_base&quot; }}
{{ end }}
{{ define &quot;content&quot; }}
    {{ template &quot;listings_table&quot; . }}
{{ end }}

... and our Go code, which leverages the map[string]*template.Template approach outlined in this SO answer:

var templates map[string]*template.Template

var ErrTemplateDoesNotExist = errors.New(&quot;The template does not exist.&quot;)

// Load templates on program initialisation
func init() {
	if templates == nil {
		templates = make(map[string]*template.Template)
	}

    templates[&quot;index.html&quot;] = template.Must(template.ParseFiles(&quot;index.tmpl&quot;, &quot;sidebar_index.tmpl&quot;, &quot;sidebar_base.tmpl&quot;, &quot;listings_table.tmpl&quot;, &quot;base.tmpl&quot;))
    ...
}

// renderTemplate is a wrapper around template.ExecuteTemplate.
func renderTemplate(w http.ResponseWriter, name string, data map[string]interface{}) error {
	// Ensure the template exists in the map.
	tmpl, ok := templates[name]
	if !ok {
		return ErrTemplateDoesNotExist
	}

	w.Header().Set(&quot;Content-Type&quot;, &quot;text/html; charset=utf-8&quot;)
	tmpl.ExecuteTemplate(w, &quot;base&quot;, data)

	return nil
}

From initial benchmarks (using wrk) it seems to be a fair bit more performant when it comes to heavy load, likely due to the fact that we're not passing around a whole ParseGlob worth of templates every request. It also makes authoring the templates themselves a lot simpler.

huangapple
  • 本文由 发表于 2014年6月7日 13:13:21
  • 转载请务必保留本文链接:https://go.coder-hub.com/24093923.html
匿名

发表评论

匿名网友

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

确定