260×260

科学搜查官yuchanns

理想的生活是纯粹地热爱技术
  • Shenzhen, China
  • 后端开发工程师
Posted 8 months ago

pongo2使用笔记

pongo21是一个Django风格的模板引擎,目标是模板语法完全适配Django。

简介

虽然传统的MVC/MTV大势已去,日益复杂的前端技术致使如今的Trending是前后分离,后端模板引擎不再受到开发者的重视,但是依然有开源库还提供这一功能。

甚至gin本身也自带了一个简陋的模板引擎,虽然正式开发中基本上不会用到这个功能,但是笔者还是想要了解一下相关的事情。然而在稍微阅读了gin的文档和源码之后,稍微写一个小demo时,笔者发现gin的模板引擎过于简陋,很是不爽。于是决定找找第三方的模板引擎。

在笔者有限的经历中,用起来最强大和舒爽的模板引擎当属Jinja2——这是一个用python实现的著名的模板引擎,具有诸如MacroBlockInclude等强大功能,同时性能也很好。在pkg.go.dev中,笔者轻易找到了一个自称A Django-syntax like template-engine的模板引擎,它就是pongo2。这里对不了解python的读者解释一下,Jinja2和Django模板引擎的语法基本一致。

pongo2的文档很简单,仅提供了Api的说明,作者表示关于模板的用法,只需要查看Django的文档就可以了2

接入gin

笔者并不打算浪费篇幅描述关于模板语法的内容,这些在Django文档上可以清楚地了解到。

本文的重点是研究将pongo2和gin结合起来使用。

分析gin的模板引擎接口

gin的文档中关于模板引擎的使用描述不多,如何替换模板引擎我们可以先从使用的源码入手:

// HTML方法渲染指定文件名的HTTP模板 // 同时更新HTTP code并设置Content-Type为"text/html" func (c *Context) HTML(code int, name string, obj interface{}) { instance := c.engine.HTMLRender.Instance(name, obj) c.Render(code, instance) }

常规的模板渲染用法就是在路由中调用*gin.Context.HTML方法,从源码中我们可以看到此方法先获取一个模板引擎的实例,然后调用实例的Render方法进行渲染。

也就是说我们只需要替换掉c.engine.HTMLRender就可以了。

继续追溯源码,可以看到HTMLRender是一个接口,同时它内部又包含了另一个接口Render

type HTMLRender interface { // Instance方法返回一个HTML实例 Instance(string, interface{}) Render } type Render interface { // Render方法写入数据和自定义的ContentType Render(http.ResponseWriter) error // WriteContentType方法写入自定义的ContentType WriteContentType(w http.ResponseWriter) }

这下答案就清晰了。我们所要做的工作就是在pongo2和gin之间编写出实现这两个接口中间结构。

分析pongo2的API

快速浏览pogon2仓库的API-usage examples,可以看到渲染方式就是先调用pongo2.FromString方法构建模板,然后调用模板的Execute方法就可以获得渲染结果。当然,我们要使用的不是这两个方法。追溯FromString方法可以看到类似功能的方法还有FromFileFromCache等方法。

思考 FromFile很明显就是我们寻求的从html模板构建渲染模板的方法,但是为什么笔者还要提到FromCache呢?

如果你阅读两个方法的源码,就会发现内容大多相似,区别只在于后者会将第一次的读取结果缓存在内存中,这对性能有一定的影响。

这意味着我们在正式环境中应该是使用后者;与之相对的,没有缓存功能的前者则适合开发中使用,因为开发过程中时常对html模板做出改动需要实时反映。

并且Render接口的Render方法要求的是将内容写入http.ResponseWriter实例中,这是一个实现了io.Writer接口的结构,于是我们从源码中找到接收对应接口参数的方法ExecuteWriter

实现思路

现在整理一下实现思路:

  • 编写一个名为PongoRender的结构体,它通过返回创建一个实现Render接口的名为PongoHTML的实例来实现HTMLRender接口。
  • PongoHTML结构在Render方法中调用ExecuteWriter方法来实际进行渲染工作。
  • 那么具有ExecuteWriter的结构体*pongo2.Template需要包含在PongoHTML结构体中作为一个属性成员。
  • 在实例化的过程中注意根据gin.Mode来判断程序运行环境进而选择通过FromFile还是FromCache来构建模板。
  • 至于WriteContentType需要完成的工作可以直接复制默认模板引擎的代码。

下面直接给出结果,可以结合注释进行理解:

package main import ( "github.com/flosch/pongo2" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/render" "net/http" "path" ) type PongoRender struct { TmplDir string // 考虑到模板位置不定,需要一个字符串来存储绝对父路径 } // 返回一个模板引擎实例用于替换*gin.Engine.HTMLRender func New(tmplDir string) *PongoRender { return &PongoRender{ TmplDir: tmplDir, } } func (p *PongoRender) Instance(name string, data interface{}) render.Render { var template *pongo2.Template fileName := path.Join(p.TmplDir, name) // 拼接绝对父路径和相对路径 if gin.Mode() == gin.DebugMode { // 判断运行环境构建pongo2模板 template = pongo2.Must(pongo2.FromFile(fileName)) } else { template = pongo2.Must(pongo2.FromCache(fileName)) } return &PongoHTML{ Template: template, Name: name, Data: data.(pongo2.Context), // 断言为pongo2接受的类型 } } type PongoHTML struct { Template *pongo2.Template // pongo2模板 Name string // 模板文件绝对路径 Data pongo2.Context // 上下文数据 } // Render方法通过pongo2模板向http.ResponseWriter写入数据和自定义的ContentType func (p *PongoHTML) Render(w http.ResponseWriter) error { p.WriteContentType(w) return p.Template.ExecuteWriter(p.Data, w) } func (p *PongoHTML) WriteContentType(w http.ResponseWriter) { header := w.Header() if val := header["Content-Type"]; len(val) == 0 { header["Content-Type"] = []string{"text/html; charset=utf-8"} } }

本文相关代码yuchanns/gobyexample