本文为原创, 遵循 CC 4.0 BY-SA 版权协议, 转载需注明出处: https://blog.csdn.net/big_cheng/article/details/107520826.
文中代码属于 public domain (无版权).
Golang text/template目前不支持 include - 合并其他模板文件的内容 (类似jsp include).
按目前api来看通常只能#ParseFiles 先传通用模板再传定制内容模板, 以及使用#AddParseTree和#Clone 辅助. 但每个特定页面需要哪些模板及顺序信息只能通过配置, 这样增加了工作且维护不方便.
首先想到的是定义一个"include"函数来指定本模板需要导入的模板, 如:
{{include "base.tmpl"}}
...
但其实Golang模板里的函数在解析时并不执行而是运行时才执行, 所以#Parse时base.tmpl里的内容并没导入. 如果运行时再导入将来不及 (顺序如何保证? 部分内容已经解析了).
include 需要在解析时知道当前模板还需要哪些模板, 以及它们之间的顺序.
通过使用现有"{{define}} {{end}}" action, 并由其名称指定待导入的文件, 可以在解析当前模板后知道它还需要的模板.
由于define原本用来定义关联模板, 所以定义导入模板时名称格式要有区别. 如:
{{define "file:/base.tmpl"}}
...
导入文件名称使用相对于根目录的路径, 保证唯一.
解析完当前模板后, 遍历其每个"file:" 的"define", 也解析并加到map - 如此递归, 直到最基础的模板文件.
例如a (依赖)-> b (依赖)-> c(最后解析), 如果a/b/c都定义了模板"foo", 则a里的应该最优先. 所以最后要按相反顺序: c所有关联模板 加上b所有关联模板 再加上a所有关联模板 才是最终结果.
按解析顺序存储已解析的模板文件名和结果模板:
var filenames []string = make([]string, 0)
var tmpls = make([]*template.Template, 0)
var dir string // 根目录
递归函数处理一个文件:
func processOne(filename string) error {
b, err := ioutil.ReadFile(filepath.Join(dir, filename))
if err != nil {
return err
}
tmpl, err := template.New(filename).Parse(string(b))
if err != nil {
return err
}
filenames = append(filenames, filename)
tmpls = append(tmpls, tmpl)
for _, t := range tmpl.Templates() { // 注: tmpl.Templates()无序
if name := t.Name(); strings.HasPrefix(name, "file:") {
filename = name[5:]
found := false
for _, v := range filenames {
if v == filename {
found = true
break
}
}
if !found {
if err = processOne(filename); err != nil {
return err
}
}
}
}
return nil
}
遍历的每个"file:"先检查其尚未处理过.
主函数 (这里写死了arg):
func main() {
var err error
if dir, err = os.Getwd(); err != nil {
panic(err)
}
arg := "/b3/index.tmpl"
if err = processOne(arg); err != nil {
panic(err)
}
println(strings.Join(filenames, ","))
// build
tmpl := template.New(arg)
for i := len(tmpls) - 1; i >= 0; i-- {
t := tmpls[i]
for _, t2 := range t.Templates() {
if name := t2.Name(); !strings.HasPrefix(name, "file:") {
nt, err := tmpl.AddParseTree(name, t2.Tree)
if err != nil {
panic(err)
}
if i == 0 && name == arg {
tmpl = nt
}
}
}
}
println("==== Execute Result ====")
if err = tmpl.Execute(os.Stdout, "index"); err != nil {
panic(err)
}
}
“build” 部分按最基础文件 -> arg文件的顺序重新组装最终结果对象. 注意html #AddParseTree 与text #AddParseTree 稍有不同: 当name==arg时必须用nt 替换tmpl, 否则原tmpl.Tree 为nil 会在执行时报错"is an incomplete or empty template".
/b3/index.tmpl:
{{define "file:/a3/frame.tmpl"}}{{end}}
{{template "/a3/frame.tmpl" .}}
{{define "body"}}
this is index body.
{{end}}
/a3/frame.tmpl:
Title.
{{define "file:/a3/base.tmpl"}}{{end}}
{{template "/a3/base.tmpl" .}}
{{define "foot"}}
this is frame foot.
{{end}}
/a3/base.tmpl:
{{define "head"}}
this is head of {{.}}.
{{end}}
{{template "head" .}}
{{define "body"}}
this is base body.
{{end}}
{{template "body"}}
{{define "foot"}}
this is base foot.
{{end}}
{{template "foot"}}
base定义head/body/foot; frame加了title、覆盖了foot; index 覆盖了body. head的显示通过参数定制.
运行结果:
/b3/index.tmpl,/a3/frame.tmpl,/a3/base.tmpl
==== Execute Result ====
Title.
this is head of index.
this is index body.
this is frame foot.
看到解析后的顺序是index ->(依赖) frame -> base.
结果符合预期: title有、head有参数、body是index的、foot是frame的.
/a3/o_index.tmpl:
{{define "file:/a3/o_a.tmpl"}}{{end}}
{{template "/a3/o_a.tmpl"}}
{{define "file:/a3/o_b.tmpl"}}{{end}}
{{template "/a3/o_b.tmpl"}}
o_index
/a3/o_a.tmpl:
{{define "foo"}}
this is o_a:foo.
{{end}}
{{template "foo"}}
{{define "bar"}}
this is o_a:bar.
{{end}}
{{template "bar"}}
/a3/o_b.tmpl:
{{define "bar"}}
this is o_b:bar.
{{end}}
{{template "bar"}}
{{define "baz"}}
this is o_b:baz.
{{end}}
{{template "baz"}}
o_a = foo + bar, o_b = bar + baz, 两个bar 内容不同. index = o_a + o_b, 那倒底使用哪个bar?
修改arg, 测试2次:
/a3/o_index.tmpl,/a3/o_a.tmpl,/a3/o_b.tmpl
......
/a3/o_index.tmpl,/a3/o_b.tmpl,/a3/o_a.tmpl
......
可见结果不确定. 因为template代码使用的Golang map不保持顺序, 在遍历文件解析结果时就丢失了顺序. 待讨论.
GolangPkg text/template | 笔记
使用Golang模板拼sql(及校验)