template源码分析
golang 的模版使用分两步
1,模版解析
tpl, err := template.Parse(filename)得到文件名为名字的模板,并保存在tpl变量中
template.ParseFiles(filenames)可以解析一组模板
template.ParseGlob(pattern)会根据pattern解析所有匹配的模板并保存
2,数据绑定
tpl.Execute(io.Writer, data)去执行, 模板渲染后的内容写入到io.Writer中。Data是传给模板的动态数据。
tpl.ExecuteTemplate(io.Writer, name, data)和上面的简单模板类似,只不过传入了一个模板的名字,指定要渲染的模板(因为tpl可以包含多个模板
嵌套模板定义
嵌套模板定义如下:
{ {define "footer"}}<footer><p>Here is the footer</p></footer>{ {end}}
其他模板中使用:
{ {template "footer"}}模版的数据绑定也有两种方式
1,模版变量
通过.获取变量
通过{ { .FieldName }}来访问它的字段
链式访问 { { .Struct.StructTwo.Field }}
2,函数变量
2.1,调用结构体的方法
{ {if .User.HasPermission "feature-a"}}2.2 调用结构体的函数变量(属性是一个函数)
type User struct {ID intEmail stringHasPermission func(string) bool}{ {if (call .User.HasPermission "feature-b")}}
2.3使用template.FuncMap创建自定义的函数
FuncMap通过map[string]interface{}将函数名映射到函数上。注意映射的函数必须只有一个返回值,或者有两个返回值但是第二个是error类型。
testTemplate, err = template.New("hello.gohtml").Funcs(template.FuncMap{"hasPermission": func(user User, feature string) bool {if user.ID == 1 && feature == "feature-a" {return true}return false},}).ParseFiles("hello.gohtml")
{ { if hasPermission .User "feature-a" }}2.4第三方自定义函数
比如sprig库
由于函数变量的方式绑定数据可以使用闭包,所以数据的绑定时机有两个
1,使用闭包
template.New("hello.gohtml").Funcs(template.FuncMap2,Execute数据绑定
tpl.Execute(io.Writer, data)常用的两个模版包
"text/template"template包实现了数据驱动的用于生成文本输出的模板。其实简单来说就是将一组文本嵌入另一组文本模版中,返回一个你期望的文本
"html/template"生成HTML格式的输出,该包提供了相同的接口,但会自动将输出转化为安全的HTML格式输出,可以抵抗一些网络攻击。  内部其实是用了"text/template"
看下这两个包的源码目录
text/template
cd /usr/local/go/src/text/templatetree.|____option.go|____examplefunc_test.go|____testdata| |____file1.tmpl| |____tmpl1.tmpl| |____tmpl2.tmpl| |____file2.tmpl|____examplefiles_test.go|____example_test.go|____exec_test.go|____link_test.go|____multi_test.go|____parse| |____lex_test.go| |____lex.go| |____parse_test.go| |____node.go| |____parse.go|____exec.go|____template.go|____helper.go|____doc.go|____funcs.go
html/template
cd /usr/local/go/src/html/templatetree.|____testdata| |____file1.tmpl| |____tmpl1.tmpl| |____fs.zip| |____tmpl2.tmpl| |____file2.tmpl|____clone_test.go|____error.go|____examplefiles_test.go|____example_test.go|____content_test.go|____escape.go|____exec_test.go|____transition_test.go|____multi_test.go|____js_test.go|____element_string.go|____urlpart_string.go|____transition.go|____css_test.go|____template_test.go|____html.go|____state_string.go|____js.go|____html_test.go|____delim_string.go|____template.go|____doc.go|____context.go|____attr_string.go|____content.go|____css.go|____url.go|____escape_test.go|____attr.go|____url_test.go|____jsctx_string.go
下面简单介绍下text/template
它的工作流程大概分为下面几步,
通过lex 进行词法分析=>parse解析语法树=>execute渲染生成目标文件
首先看Template结构:
type Template struct {name string*parse.Tree*commonleftDelim stringrightDelim string}
name是这个Template的名称,Tree是解析树,common是另一个结构,leftDelim和rightDelim是左右两边的分隔符,默认为{{和}}。
type common struct {tmpl map[string]*Template // Map from name to defined templates.option optionmuFuncs sync.RWMutex // protects parseFuncs and execFuncsparseFuncs FuncMapexecFuncs map[string]reflect.Value}
这个结构的第一个字段tmpl是一个Template的map结构,key为template的name,value为Template。也就是说,一个common结构
中可以包含多个Template,而Template结构中又指向了一个common结构。所以,common是一个模板组,在这个模板组中的(tmpl字段)所有Template都共享一个common(模板组),模板组中包含parseFuncs和execFuncs。
使用template.New()函数可以创建一个空的、无解析数据的模板,同时还会创建一个common,也就是模板组。
func New(name string) *Template {t := &Template{name: name,}t.init()return t}
其中t为模板的关联名称,name为模板的名称,t.init()表示如果模板对象t还没有common结构,就构造一个新的common组:
func (t *Template) init() {if t.common == nil {c := new(common)c.tmpl = make(map[string]*Template)c.parseFuncs = make(FuncMap)c.execFuncs = make(map[string]reflect.Value)t.common = c}}
新创建的common是空的,只有进行模板解析(Parse(),ParseFiles()等操作)之后,才会将模板添加到common的tmpl字段(map结构)中。
在执行了Parse()方法后,将会把模板name添加到common tmpl字段的map结构中,其中模板name为map的key,模板为map的value。
除了template.New()函数,还有一个Template.New()方法:
func (t *Template) New(name string) *Template {t.init()nt := &Template{name: name,common: t.common,leftDelim: t.leftDelim,rightDelim: t.rightDelim,}return nt}
通过调用t.New()方法,可以创建一个新的名为name的模板对象,并将此对象加入到t模板组中。
Execute()和ExecuteTemplate()
这两个方法都可以用来应用已经解析好的模板,应用表示对需要评估的数据进行操作,并和无需评估数据进行合并,然后输出>到io.Writer中:
func (t *Template) Execute(wr io.Writer, data interface{}) error
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data interface{}) error
两者的区别在于Execute()是应用整个common中已定义的模板对象,而ExecuteTemplate()可以选择common中某个已定义的模板进行应用。
FuncMap和Funcs()
template内置了一系列函数,但这些函数毕竟有限,可能无法满足特殊的需求。template允许我们定义自己的函数,添加到common中,然后就可以在待解析的内容中像使用内置函数一样使用自定义的函数。
下面看看模版解析的 过程
parse.Parse 函数把文本解析成map[string]*parse.Tree的树map对象,然后把它append到当前模板的t.temp中
func (t *Tree) Parse(text, leftDelim, rightDelim string, treeSet map[string]*Tree, funcs ...map[string]interface{}) (tree *Tree, err error) {defer t.recover(&err)t.ParseName = t.Namet.startParse(funcs, lex(t.Name, text, leftDelim, rightDelim), treeSet)t.text = textt.parse()t.add()t.stopParse()return t, nil}
// lex creates a new scanner for the input string.func lex(name, input, left, right string) *lexer {if left == "" {left = leftDelim}if right == "" {right = rightDelim}l := &lexer{name: name,input: input,leftDelim: left,rightDelim: right,items: make(chan item),line: 1,}go l.run()return l}
// run runs the state machine for the lexer.func (l *lexer) run() {for state := lexText; state != nil; {state = state(l)}close(l.items)}
// lexText scans until an opening action delimiter, "{{".func lexText(l *lexer) stateFn {l.width = 0if x := strings.Index(l.input[l.pos:], l.leftDelim); x >= 0 {ldn := Pos(len(l.leftDelim))l.pos += Pos(x)trimLength := Pos(0)if strings.HasPrefix(l.input[l.pos+ldn:], leftTrimMarker) {trimLength = rightTrimLength(l.input[l.start:l.pos])}l.pos -= trimLengthif l.pos > l.start {l.emit(itemText)}l.pos += trimLengthl.ignore()return lexLeftDelim} else {l.pos = Pos(len(l.input))}// Correctly reached EOF.if l.pos > l.start {l.emit(itemText)}l.emit(itemEOF)return nil}
下面看下数据绑定的过程,其实就是遍历语法树
// Walk functions step through the major pieces of the template structure,// generating output as they go.func (s *state) walk(dot reflect.Value, node parse.Node) {s.at(node)switch node := node.(type) {case *parse.ActionNode:// Do not pop variables so they persist until next end.// Also, if the action declares variables, don't print the result.val := s.evalPipeline(dot, node.Pipe)if len(node.Pipe.Decl) == 0 {s.printValue(node, val)}case *parse.IfNode:s.walkIfOrWith(parse.NodeIf, dot, node.Pipe, node.List, node.ElseList)case *parse.ListNode:for _, node := range node.Nodes {s.walk(dot, node)}case *parse.RangeNode:s.walkRange(dot, node)case *parse.TemplateNode:s.walkTemplate(dot, node)case *parse.TextNode:if _, err := s.wr.Write(node.Text); err != nil {s.writeError(err)}case *parse.WithNode:s.walkIfOrWith(parse.NodeWith, dot, node.Pipe, node.List, node.ElseList)default:s.errorf("unknown node: %s", node)}}
推荐阅读
