一个Golang模板的include设计

目录

  • 1. 版权
  • 2. 背景
  • 3. 设计
  • 4. 代码
  • 5. 测试
    • 顺序
  • 6. 参考

1. 版权

本文为原创, 遵循 CC 4.0 BY-SA 版权协议, 转载需注明出处: https://blog.csdn.net/big_cheng/article/details/107520826.
文中代码属于 public domain (无版权).

2. 背景

Golang text/template目前不支持 include - 合并其他模板文件的内容 (类似jsp include).

按目前api来看通常只能#ParseFiles 先传通用模板再传定制内容模板, 以及使用#AddParseTree和#Clone 辅助. 但每个特定页面需要哪些模板及顺序信息只能通过配置, 这样增加了工作且维护不方便.

首先想到的是定义一个"include"函数来指定本模板需要导入的模板, 如:

{{include "base.tmpl"}}

...

但其实Golang模板里的函数在解析时并不执行而是运行时才执行, 所以#Parse时base.tmpl里的内容并没导入. 如果运行时再导入将来不及 (顺序如何保证? 部分内容已经解析了).

3. 设计

include 需要在解析时知道当前模板还需要哪些模板, 以及它们之间的顺序.

通过使用现有"{{define}} {{end}}" action, 并由其名称指定待导入的文件, 可以在解析当前模板后知道它还需要的模板.
由于define原本用来定义关联模板, 所以定义导入模板时名称格式要有区别. 如:

{{define "file:/base.tmpl"}}

...

导入文件名称使用相对于根目录的路径, 保证唯一.

解析完当前模板后, 遍历其每个"file:" 的"define", 也解析并加到map - 如此递归, 直到最基础的模板文件.
例如a (依赖)-> b (依赖)-> c(最后解析), 如果a/b/c都定义了模板"foo", 则a里的应该最优先. 所以最后要按相反顺序: c所有关联模板 加上b所有关联模板 再加上a所有关联模板 才是最终结果.

4. 代码

按解析顺序存储已解析的模板文件名和结果模板:

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

5. 测试

/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不保持顺序, 在遍历文件解析结果时就丢失了顺序. 待讨论.

6. 参考

GolangPkg text/template | 笔记

使用Golang模板拼sql(及校验)

你可能感兴趣的:(golang,templates)