From 9bcbe357be0a5a946e6b372176fe35729cb1727f Mon Sep 17 00:00:00 2001 From: astaxie Date: Tue, 15 Jan 2013 13:47:43 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E4=B8=80=E4=BA=9B=E6=94=B9?= =?UTF-8?q?=E8=BF=9B=20#140?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 14.7.md | 2 +- 4.4.md | 109 ++++----- 7.4.md | 689 ++++++++++++++++++++++++++++---------------------------- ref.md | 3 +- 4 files changed, 407 insertions(+), 396 deletions(-) diff --git a/14.7.md b/14.7.md index 28cda81c..586fa498 100644 --- a/14.7.md +++ b/14.7.md @@ -1,5 +1,5 @@ # 14.7 小结 -这一章主要阐述了如何基于beego框架进行扩展,这包括静态文件的支持,静态文件主要讲述了如何利用beego进行快速的网站开发,利用bootstrap搭建漂亮的站点;第二小结讲解了如何在beego中集成sessionManager,方便用户在利用beego的时候快速的使用session;第三小结介绍了表单和验证,基于Go语言的struct的定义使得我们在开发Web的过程中从重复的工作中解放出来,而且加入了验证之后可以尽量做到数据安全,第四小结介绍了用户认证,用户认证主要有三方面的需求,http basic和http digest认证,第三方认证,自定义认证,通过代码演示了如何利用现有的第三方包集成到beego应用中来实现这些认证;第五小节介绍了多语言的支持,beego中集成了go-i18n这个多语言包,用户可以很方便的利用该库开发多语言的Web应用;第六小节介绍了如何集成Go的pprof包,pprof包是用于性能调试的工具,通过对beego的改造之后集成了pprof包,使得用户可以利用pprof测试基于beego开发的应用,通过这六个小节的介绍我们扩展出来了一个比较强壮的beego框架,这个框架足以应付目前大多数的Web应用,用户可以继续发挥自己的想象力去扩展,我这里只是简单的介绍了我能像的几个比较重要的扩展。 +这一章主要阐述了如何基于beego框架进行扩展,这包括静态文件的支持,静态文件主要讲述了如何利用beego进行快速的网站开发,利用bootstrap搭建漂亮的站点;第二小结讲解了如何在beego中集成sessionManager,方便用户在利用beego的时候快速的使用session;第三小结介绍了表单和验证,基于Go语言的struct的定义使得我们在开发Web的过程中从重复的工作中解放出来,而且加入了验证之后可以尽量做到数据安全,第四小结介绍了用户认证,用户认证主要有三方面的需求,http basic和http digest认证,第三方认证,自定义认证,通过代码演示了如何利用现有的第三方包集成到beego应用中来实现这些认证;第五小节介绍了多语言的支持,beego中集成了go-i18n这个多语言包,用户可以很方便的利用该库开发多语言的Web应用;第六小节介绍了如何集成Go的pprof包,pprof包是用于性能调试的工具,通过对beego的改造之后集成了pprof包,使得用户可以利用pprof测试基于beego开发的应用,通过这六个小节的介绍我们扩展出来了一个比较强壮的beego框架,这个框架足以应付目前大多数的Web应用,用户可以继续发挥自己的想象力去扩展,我这里只是简单的介绍了我能想的到的几个比较重要的扩展。 ## links * [目录]() diff --git a/4.4.md b/4.4.md index 87033c86..99edb422 100644 --- a/4.4.md +++ b/4.4.md @@ -1,53 +1,56 @@ -# 4.4防止多次递交表单 - -不知道你是否曾经看到过一个论坛或者博客,在一个帖子或者文章后面出现多条重复的记录,这些大多数是因为用户重复递交了留言的表单引起的。由于种种原因,用户经常会重复递交表单。通常这只是鼠标的误操作,如双击了递交按钮,也可能是为了编辑或者再次核对填写过的信息,点击了浏览器的后退按钮,然后又再次点击了递交按钮而不是浏览器的前进按钮。当然,也可能是故意的——比如,在某项在线调查或者博彩活动中重复投票。那我们如何有效的防止用户多次递交相同的表单呢? - -解决方案是在表单中添加一个带有唯一值的隐藏字段。在验证表单时,先检查带有该惟一值的表单是否已经递交过了。如果是,拒绝再次递交;如果不是,则处理表单进行逻辑处理。另外,如果是采用了Ajax模式递交表单的话,当表单递交后,通过javascript来禁用表单的递交按钮。 - -我继续拿4.2小节的例子优化: - - 用户名: - 密码: - - - -我们在模版里面增加了一个隐藏字段`token`,这个值我们通过MD5(时间戳)来获取惟一值,然后我们把这个值存储到服务器端(session来控制,我们将在第六章讲解如何保存),以方便表单提交时比对判定。 - - func login(w http.ResponseWriter, r *http.Request) { - fmt.Println("method:", r.Method) //获取请求的方法 - if r.Method == "GET" { - crutime := time.Now().Unix() - h := md5.New() - io.WriteString(h, strconv.FormatInt(crutime, 10)) - token := fmt.Sprintf("%x", h.Sum(nil)) - - t, _ := template.ParseFiles("login.gtpl") - t.Execute(w, token) - } else { - //请求的是登陆数据,那么执行登陆的逻辑判断 - r.ParseForm() - token := r.Form.Get("token") - if token != "" { - //验证token的合法性 - } else { - //不存在token报错 - } - fmt.Println("username length:", len(r.Form["username"][0])) - fmt.Println("username:", template.HTMLEscapeString(r.Form.Get("username"))) //输出到服务器端 - fmt.Println("password:", template.HTMLEscapeString(r.Form.Get("password"))) - template.HTMLEscape(w, []byte(r.Form.Get("username"))) //输出到客户端 - } - } - -上面的代码输出到页面的源码如下: - -![](images/4.4.token.png?raw=true) - -我们看到token已经有输出值,你可以不断的刷新,可以看到这个值在不断的变化。这样就保证了每次显示form表单的时候都是唯一的,用户递交的表单保持了唯一性。 - -我们的解决方案可以防止非恶意的攻击,并能使恶意用户暂时不知所措,然后,它却不能排除所有的欺骗性的动机,对此类情况还需要更复杂的工作。 - -## links - * [目录]() - * 上一节: [预防跨站脚本](<4.3.md>) - * 下一节: [处理文件上传](<4.5.md>) +# 4.4防止多次递交表单 + +不知道你是否曾经看到过一个论坛或者博客,在一个帖子或者文章后面出现多条重复的记录,这些大多数是因为用户重复递交了留言的表单引起的。由于种种原因,用户经常会重复递交表单。通常这只是鼠标的误操作,如双击了递交按钮,也可能是为了编辑或者再次核对填写过的信息,点击了浏览器的后退按钮,然后又再次点击了递交按钮而不是浏览器的前进按钮。当然,也可能是故意的——比如,在某项在线调查或者博彩活动中重复投票。那我们如何有效的防止用户多次递交相同的表单呢? + +解决方案是在表单中添加一个带有唯一值的隐藏字段。在验证表单时,先检查带有该惟一值的表单是否已经递交过了。如果是,拒绝再次递交;如果不是,则处理表单进行逻辑处理。另外,如果是采用了Ajax模式递交表单的话,当表单递交后,通过javascript来禁用表单的递交按钮。 + +我继续拿4.2小节的例子优化: + + 足球 + 篮球 + 网球 + 用户名: + 密码: + + + +我们在模版里面增加了一个隐藏字段`token`,这个值我们通过MD5(时间戳)来获取惟一值,然后我们把这个值存储到服务器端(session来控制,我们将在第六章讲解如何保存),以方便表单提交时比对判定。 + + func login(w http.ResponseWriter, r *http.Request) { + fmt.Println("method:", r.Method) //获取请求的方法 + if r.Method == "GET" { + crutime := time.Now().Unix() + h := md5.New() + io.WriteString(h, strconv.FormatInt(crutime, 10)) + token := fmt.Sprintf("%x", h.Sum(nil)) + + t, _ := template.ParseFiles("login.gtpl") + t.Execute(w, token) + } else { + //请求的是登陆数据,那么执行登陆的逻辑判断 + r.ParseForm() + token := r.Form.Get("token") + if token != "" { + //验证token的合法性 + } else { + //不存在token报错 + } + fmt.Println("username length:", len(r.Form["username"][0])) + fmt.Println("username:", template.HTMLEscapeString(r.Form.Get("username"))) //输出到服务器端 + fmt.Println("password:", template.HTMLEscapeString(r.Form.Get("password"))) + template.HTMLEscape(w, []byte(r.Form.Get("username"))) //输出到客户端 + } + } + +上面的代码输出到页面的源码如下: + +![](images/4.4.token.png?raw=true) + +我们看到token已经有输出值,你可以不断的刷新,可以看到这个值在不断的变化。这样就保证了每次显示form表单的时候都是唯一的,用户递交的表单保持了唯一性。 + +我们的解决方案可以防止非恶意的攻击,并能使恶意用户暂时不知所措,然后,它却不能排除所有的欺骗性的动机,对此类情况还需要更复杂的工作。 + +## links + * [目录]() + * 上一节: [预防跨站脚本](<4.3.md>) + * 下一节: [处理文件上传](<4.5.md>) diff --git a/7.4.md b/7.4.md index fba8e8cc..1ac88990 100644 --- a/7.4.md +++ b/7.4.md @@ -1,341 +1,348 @@ -# 7.4 模板处理 -## 什么是模板 -你一定听说过一种叫做MVC的设计模式,Model处理数据,View展现结果,Controller控制用户的请求,至于View层的处理,在很多动态语言里面都是通过在静态HTML中插入动态语言生成的数据,例如JSP中通过插入`<%=....=%>`,PHP中通过插入``来实现的。 - -通过下面这个图可以说明模板的机制 - -![](images/7.4.template.png?raw=true) - -Web应用反馈给客户端的信息中的大部分内容是静态的,不变的,而另外少部分是根据用户的请求来动态生成的,例如要显示用户的访问记录列表。用户之间只有记录数据是不同的,而列表的样式则是固定的,此时采用模板可以复用很多静态代码。 - -## Go模板使用 -在Go语言中,我们使用`template`包来进行模板处理,使用类似`Parse`、`ParseFile`、`Execute`等方法从文件或者字符串加载模板,然后执行类似上面图片展示的模板的merge操作。请看下面的例子: - - func handler(w http.ResponseWriter, r *http.Request) { - t := template.New("some template") //创建一个模板 - t, _ = t.ParseFiles("tmpl/welcome.html", nil) //解析模板文件 - user := GetUser() //获取当前用户信息 - t.Execute(w, user) //执行模板的merger操作 - } - -通过上面的例子我们可以看到Go语言的模板操作非常的简单方便,和其他语言的模板处理类似,都是先获取数据,然后渲染数据。 - -为了演示和测试代码的方便,我们在接下来的例子中采用如下格式的代码 - -- 使用Parse代替ParseFiles,因为Parse可以直接测试一个字符串,而不需要额外的文件 -- 不使用handler来写演示代码,而是每个测试一个main,方便测试 -- 使用`os.Stdout`代替`http.ResponseWriter`,因为`os.Stdout`实现了`io.Writer`接口 - -## 模板中如何插入数据? -上面我们演示了如何解析并渲染模板,接下来让我们来更加详细的了解如何把数据渲染出来。一个模板都是应用在一个Go的对象之上,Go对象的字段如何插入到模板中呢? - -### 字段操作 -Go语言的模板通过`{{}}`来包含需要在渲染时被替换的字段,`{{.}}`表示当前的对象,这和Java或者C++中的this类似,如果要访问当前对象的字段通过`{{.FieldName}}`,但是需要注意一点:这个字段必须是导出的(字段首字母必须是大写的),否则在渲染的时候就会报错,请看下面的这个例子: - - package main - - import ( - "html/template" - "os" - ) - - type Person struct { - UserName string - } - - func main() { - t := template.New("fieldname example") - t, _ = t.Parse("hello {{.UserName}}!") - p := Person{UserName: "Astaxie"} - t.Execute(os.Stdout, p) - } - -上面的代码我们可以正确的输出`hello Astaxie`,但是如果我们稍微修改一下代码,在模板中含有了未导出的字段,那么就会报错 - - type Person struct { - UserName string - email string //未导出的字段,首字母是小写的 - } - - t, _ = t.Parse("hello {{.UserName}}! {{.email}}") - -上面的代码就会报错,因为我们调用了一个未导出的字段,但是如果我们调用了一个不存在的字段是不会报错的,而是输出为空。 - -如果模板中输出`{{.}}`,这个一般应用与字符串对象,默认会调用fmt包输出字符串的内容。 - -### 输出嵌套字段内容 -上面我们例子展示了如何针对一个对象的字段输出,那么如果字段里面还有对象,如何来循环的输出这些内容呢?我们可以使用`{{with …}}…{{end}}`和`{{range …}}{{end}}`来进行数据的输出。详细的使用请看下面的例子: - - package main - - import ( - "html/template" - "os" - ) - - type Friend struct { - Fname string - } - - type Person struct { - UserName string - Emails []string - Friends []*Friend - } - - func main() { - f1 := Friend{Fname: "minux.ma"} - f2 := Friend{Fname: "xushiwei"} - t := template.New("fieldname example") - t, _ = t.Parse(`hello {{.UserName}}! - {{range .Emails}} - an email {{.}} - {{end}} - {{with .Friends}} - {{range .}} - my friend name is {{.Fname}} - {{end}} - {{end}} - `) - p := Person{UserName: "Astaxie", - Emails: []string{"astaxie@beego.me", "astaxie@gmail.com"}, - Friends: []*Friend{&f1, &f2}} - t.Execute(os.Stdout, p) - } - -### pipelines -Unix用户已经很熟悉什么是`pipe`了,`ls | grep "name"`类似这样的语法你是不是经常使用,过滤当前目录下面的文件,显示含有"name"的数据,他表达的意思就是前面的输出可以当做后面的输入,最后显示我们想要的数据,而Go语言模板最强大的一点就是支持pipe数据,在Go语言里面任何`{{}}`里面的都是pipelines数据,例如我们上面输出的email里面如果还有一些可能引起XSS注入的,那么我们如何来进行转化呢? - - {{. | html}} - -在email输出的地方我们可以采用如上方式可以把输出全部转化html的实体,上面的这种方式和我们平常写Unix的方式是不是一模一样,操作起来相当的简便,调用其他的函数也是类似的方式。 - -### 条件处理 -在Go模板里面如果需要进行条件判断,那么我们可以使用和Go语言的`if-else`语法类似的方式来咱先,如果pipeline为空,那么if就认为是false,下面的例子展示了如何使用`if-else`语法: - - package main - - import ( - "os" - "text/template" - ) - - func main() { - tEmpty := template.New("template test") - tEmpty = template.Must(tEmpty.Parse("空 pipeline if demo: {{if ``}} 不会输出. {{end}}\n")) - tEmpty.Execute(os.Stdout, nil) - - tWithValue := template.New("template test") - tWithValue = template.Must(tWithValue.Parse("不为空的 pipeline if demo: {{if `anything`}} 我有内容,我会输出. {{end}}\n")) - tWithValue.Execute(os.Stdout, nil) - - tIfElse := template.New("template test") - tIfElse = template.Must(tIfElse.Parse("if-else demo: {{if `anything`}} if部分 {{else}} else部分.{{end}}\n")) - tIfElse.Execute(os.Stdout, nil) - } - -通过上面的演示代码我们知道`if-else`语法相当的简单,在使用过程中很容易集成到我们的模板代码中。 - -### 模板变量 -有时候,我们在模板使用过程中需要定义一些局部变量,我们可以在一些操作中申明局部变量,例如`with``range``if`过程中申明局部变量,这个变量的作用域是`{{end}}`之前,Go语言通过申明的局部变量格式如下所示: - - $variable := pipeline - -详细的例子看下面的: - - {{with $x := "output" | printf "%q"}}{{$x}}{{end}} - {{with $x := "output"}}{{printf "%q" $x}}{{end}} - {{with $x := "output"}}{{$x | printf "%q"}}{{end}} -### 模板函数 -模板在输出对象的字段值时,采用了`fmt`包把对象转化成了字符串。但是有时候我们的需求可能不是这样的,例如有时候我们为了防止垃圾邮件发送者通过采集网页的方式来发送给我们的邮箱信息,我们希望把`@`替换成`at`例如:`astaxie at beego.me`,如果要实现这样的功能,我们就需要自定义函数来做这个功能。 - -每一个模板函数都有一个唯一值的名字,然后与一个Go函数关联,通过如下的方式来关联 - - type FuncMap map[string]interface{} - -例如,如果我们想要的email函数的模板函数名是`emailDeal`,它关联的Go函数名称是`EmailDealWith`,n那么我们可以通过下面的方式来注册这个函数 - - t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith}) - -`EmailDealWith`这个函数的参数和返回值定义如下: - - func EmailDealWith(args …interface{}) string - -我们来看下面的实现例子: - - package main - - import ( - "fmt" - "html/template" - "os" - "strings" - ) - - type Friend struct { - Fname string - } - - type Person struct { - UserName string - Emails []string - Friends []*Friend - } - - func EmailDealWith(args ...interface{}) string { - ok := false - var s string - if len(args) == 1 { - s, ok = args[0].(string) - } - if !ok { - s = fmt.Sprint(args...) - } - // find the @ symbol - substrs := strings.Split(s, "@") - if len(substrs) != 2 { - return s - } - // replace the @ by " at " - return (substrs[0] + " at " + substrs[1]) - } - - func main() { - f1 := Friend{Fname: "minux.ma"} - f2 := Friend{Fname: "xushiwei"} - t := template.New("fieldname example") - t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith}) - t, _ = t.Parse(`hello {{.UserName}}! - {{range .Emails}} - an emails {{.|emailDeal}} - {{end}} - {{with .Friends}} - {{range .}} - my friend name is {{.Fname}} - {{end}} - {{end}} - `) - p := Person{UserName: "Astaxie", - Emails: []string{"astaxie@beego.me", "astaxie@gmail.com"}, - Friends: []*Friend{&f1, &f2}} - t.Execute(os.Stdout, p) - } - - -上面演示了如何自定义函数,其实,在模板包内部已经有内置的实现函数,下面代码截取自模板包里面 - - var builtins = FuncMap{ - "and": and, - "call": call, - "html": HTMLEscaper, - "index": index, - "js": JSEscaper, - "len": length, - "not": not, - "or": or, - "print": fmt.Sprint, - "printf": fmt.Sprintf, - "println": fmt.Sprintln, - "urlquery": URLQueryEscaper, - } - - -## Must操作 -模板包里面有一个函数`Must`,它的作用是检测模板是否正确,例如大括号是否匹配,注释是否正确的关闭,变量是否正确的书写。接下来我们演示一个例子,用Must来判断模板是否正确: - - package main - - import ( - "fmt" - "text/template" - ) - - func main() { - tOk := template.New("first") - template.Must(tOk.Parse(" some static text /* and a comment */")) - fmt.Println("The first one parsed OK.") - - template.Must(template.New("second").Parse("some static text {{ .Name }}")) - fmt.Println("The second one parsed OK.") - - fmt.Println("The next one ought to fail.") - tErr := template.New("check parse error with Must") - template.Must(tErr.Parse(" some static text {{ .Name }")) - } - -讲输出如下内容 - - The first one parsed OK. - The second one parsed OK. - The next one ought to fail. - panic: template: check parse error with Must:1: unexpected "}" in command - -## 嵌套模板 -我们平常开发Web应用的时候,经常会遇到一些模板有些部分是固定不变的,然后可以抽取出来作为一个独立的部分,例如一个博客的头部和尾部是不变的,而唯一改变的是中间的内容部分。所以我们可以定义成`header`、`content`、`footer`三个部分。Go语言中通过如下的语法来申明 - - {{define "子模板名称"}}内容{{end}} - -通过如下方式来调用: - - {{template "子模板名称"}} - -接下来我们演示如何使用嵌套模板,我们定义三个文件,`header.tmpl`、`content.tmpl`、`footer.tmpl`文件,里面的内容如下 - - //header.tmpl - {{define "header"}} - - - 演示信息 - - - {{end}} - - //content.tmpl - {{define "content"}} - {{template "header"}} -

演示嵌套

-
    -
  • 嵌套使用define定义子模板
  • -
  • 调用使用template
  • -
- {{template "footer"}} - {{end}} - - //footer.tmpl - {{define "footer"}} - - - {{end}} - -演示代码如下: - - package main - - import ( - "fmt" - "os" - "text/template" - ) - - func main() { - s1, _ := template.ParseFiles("header.tmpl", "content.tmpl", "footer.tmpl") - s1.ExecuteTemplate(os.Stdout, "header", nil) - fmt.Println() - s1.ExecuteTemplate(os.Stdout, "content", nil) - fmt.Println() - s1.ExecuteTemplate(os.Stdout, "footer", nil) - fmt.Println() - s1.Execute(os.Stdout, nil) - } - -通过上面的例子我们可以看到通过`template.ParseFiles`把所有的嵌套模板全部解析到模板里面,其实每一个定义的{{define}}都是一个独立的模板,他们相互独立,是并行存在的关系,内部其实存储的是类似map的一种关系(key是模板的名称,value是模板的内容),然后我们通过`ExecuteTemplate`来执行相应的子模板内容,我们可以看到header、footer都是相对独立的,都能输出内容,contenrt中因为嵌套了header和footer的内容,就会同时输出三个的内容。但是当我们执行`s1.Execute`,没有任何的输出,因为在默认的情况下没有默认的子模板,所以不会输出任何的东西。 - ->同一个集合类的模板是互相知晓的,如果同一模板被多个集合使用,则它需要在多个集合中分别解析 - -## 总结 -通过上面对模板的详细介绍,我们了解了如何把动态数据与模板融合:如何输出循环数据、如何自定义函数、如何嵌套模板等等。通过模板技术的应用,我们可以完成MVC模式中V的处理,接下来的章节我们将介绍如何来处理M和C。 - -## links - * [目录]() - * 上一节: [正则处理](<7.3.md>) - * 下一节: [小结](<7.5.md>) +# 7.4 模板处理 +## 什么是模板 +你一定听说过一种叫做MVC的设计模式,Model处理数据,View展现结果,Controller控制用户的请求,至于View层的处理,在很多动态语言里面都是通过在静态HTML中插入动态语言生成的数据,例如JSP中通过插入`<%=....=%>`,PHP中通过插入``来实现的。 + +通过下面这个图可以说明模板的机制 + +![](images/7.4.template.png?raw=true) + +Web应用反馈给客户端的信息中的大部分内容是静态的,不变的,而另外少部分是根据用户的请求来动态生成的,例如要显示用户的访问记录列表。用户之间只有记录数据是不同的,而列表的样式则是固定的,此时采用模板可以复用很多静态代码。 + +## Go模板使用 +在Go语言中,我们使用`template`包来进行模板处理,使用类似`Parse`、`ParseFile`、`Execute`等方法从文件或者字符串加载模板,然后执行类似上面图片展示的模板的merge操作。请看下面的例子: + + func handler(w http.ResponseWriter, r *http.Request) { + t := template.New("some template") //创建一个模板 + t, _ = t.ParseFiles("tmpl/welcome.html", nil) //解析模板文件 + user := GetUser() //获取当前用户信息 + t.Execute(w, user) //执行模板的merger操作 + } + +通过上面的例子我们可以看到Go语言的模板操作非常的简单方便,和其他语言的模板处理类似,都是先获取数据,然后渲染数据。 + +为了演示和测试代码的方便,我们在接下来的例子中采用如下格式的代码 + +- 使用Parse代替ParseFiles,因为Parse可以直接测试一个字符串,而不需要额外的文件 +- 不使用handler来写演示代码,而是每个测试一个main,方便测试 +- 使用`os.Stdout`代替`http.ResponseWriter`,因为`os.Stdout`实现了`io.Writer`接口 + +## 模板中如何插入数据? +上面我们演示了如何解析并渲染模板,接下来让我们来更加详细的了解如何把数据渲染出来。一个模板都是应用在一个Go的对象之上,Go对象的字段如何插入到模板中呢? + +### 字段操作 +Go语言的模板通过`{{}}`来包含需要在渲染时被替换的字段,`{{.}}`表示当前的对象,这和Java或者C++中的this类似,如果要访问当前对象的字段通过`{{.FieldName}}`,但是需要注意一点:这个字段必须是导出的(字段首字母必须是大写的),否则在渲染的时候就会报错,请看下面的这个例子: + + package main + + import ( + "html/template" + "os" + ) + + type Person struct { + UserName string + } + + func main() { + t := template.New("fieldname example") + t, _ = t.Parse("hello {{.UserName}}!") + p := Person{UserName: "Astaxie"} + t.Execute(os.Stdout, p) + } + +上面的代码我们可以正确的输出`hello Astaxie`,但是如果我们稍微修改一下代码,在模板中含有了未导出的字段,那么就会报错 + + type Person struct { + UserName string + email string //未导出的字段,首字母是小写的 + } + + t, _ = t.Parse("hello {{.UserName}}! {{.email}}") + +上面的代码就会报错,因为我们调用了一个未导出的字段,但是如果我们调用了一个不存在的字段是不会报错的,而是输出为空。 + +如果模板中输出`{{.}}`,这个一般应用与字符串对象,默认会调用fmt包输出字符串的内容。 + +### 输出嵌套字段内容 +上面我们例子展示了如何针对一个对象的字段输出,那么如果字段里面还有对象,如何来循环的输出这些内容呢?我们可以使用`{{with …}}…{{end}}`和`{{range …}}{{end}}`来进行数据的输出。 + +- {{range}} 这个和Go语法里面的range类似,循环操作数据 +- {{with}}操作是指当前对象的值,类似上下文的概念 + +详细的使用请看下面的例子: + + package main + + import ( + "html/template" + "os" + ) + + type Friend struct { + Fname string + } + + type Person struct { + UserName string + Emails []string + Friends []*Friend + } + + func main() { + f1 := Friend{Fname: "minux.ma"} + f2 := Friend{Fname: "xushiwei"} + t := template.New("fieldname example") + t, _ = t.Parse(`hello {{.UserName}}! + {{range .Emails}} + an email {{.}} + {{end}} + {{with .Friends}} + {{range .}} + my friend name is {{.Fname}} + {{end}} + {{end}} + `) + p := Person{UserName: "Astaxie", + Emails: []string{"astaxie@beego.me", "astaxie@gmail.com"}, + Friends: []*Friend{&f1, &f2}} + t.Execute(os.Stdout, p) + } + +### 条件处理 +在Go模板里面如果需要进行条件判断,那么我们可以使用和Go语言的`if-else`语法类似的方式来处理,如果pipeline为空,那么if就认为是false,下面的例子展示了如何使用`if-else`语法: + + package main + + import ( + "os" + "text/template" + ) + + func main() { + tEmpty := template.New("template test") + tEmpty = template.Must(tEmpty.Parse("空 pipeline if demo: {{if ``}} 不会输出. {{end}}\n")) + tEmpty.Execute(os.Stdout, nil) + + tWithValue := template.New("template test") + tWithValue = template.Must(tWithValue.Parse("不为空的 pipeline if demo: {{if `anything`}} 我有内容,我会输出. {{end}}\n")) + tWithValue.Execute(os.Stdout, nil) + + tIfElse := template.New("template test") + tIfElse = template.Must(tIfElse.Parse("if-else demo: {{if `anything`}} if部分 {{else}} else部分.{{end}}\n")) + tIfElse.Execute(os.Stdout, nil) + } + +通过上面的演示代码我们知道`if-else`语法相当的简单,在使用过程中很容易集成到我们的模板代码中。 + +> 注意:if里面无法使用条件判断,例如.Mail=="astaxie@gmail.com",这样的判断是不正确的,if里面只能是bool值 + +### pipelines +Unix用户已经很熟悉什么是`pipe`了,`ls | grep "beego"`类似这样的语法你是不是经常使用,过滤当前目录下面的文件,显示含有"beego"的数据,表达的意思就是前面的输出可以当做后面的输入,最后显示我们想要的数据,而Go语言模板最强大的一点就是支持pipe数据,在Go语言里面任何`{{}}`里面的都是pipelines数据,例如我们上面输出的email里面如果还有一些可能引起XSS注入的,那么我们如何来进行转化呢? + + {{. | html}} + +在email输出的地方我们可以采用如上方式可以把输出全部转化html的实体,上面的这种方式和我们平常写Unix的方式是不是一模一样,操作起来相当的简便,调用其他的函数也是类似的方式。 + +### 模板变量 +有时候,我们在模板使用过程中需要定义一些局部变量,我们可以在一些操作中申明局部变量,例如`with``range``if`过程中申明局部变量,这个变量的作用域是`{{end}}`之前,Go语言通过申明的局部变量格式如下所示: + + $variable := pipeline + +详细的例子看下面的: + + {{with $x := "output" | printf "%q"}}{{$x}}{{end}} + {{with $x := "output"}}{{printf "%q" $x}}{{end}} + {{with $x := "output"}}{{$x | printf "%q"}}{{end}} +### 模板函数 +模板在输出对象的字段值时,采用了`fmt`包把对象转化成了字符串。但是有时候我们的需求可能不是这样的,例如有时候我们为了防止垃圾邮件发送者通过采集网页的方式来发送给我们的邮箱信息,我们希望把`@`替换成`at`例如:`astaxie at beego.me`,如果要实现这样的功能,我们就需要自定义函数来做这个功能。 + +每一个模板函数都有一个唯一值的名字,然后与一个Go函数关联,通过如下的方式来关联 + + type FuncMap map[string]interface{} + +例如,如果我们想要的email函数的模板函数名是`emailDeal`,它关联的Go函数名称是`EmailDealWith`,n那么我们可以通过下面的方式来注册这个函数 + + t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith}) + +`EmailDealWith`这个函数的参数和返回值定义如下: + + func EmailDealWith(args …interface{}) string + +我们来看下面的实现例子: + + package main + + import ( + "fmt" + "html/template" + "os" + "strings" + ) + + type Friend struct { + Fname string + } + + type Person struct { + UserName string + Emails []string + Friends []*Friend + } + + func EmailDealWith(args ...interface{}) string { + ok := false + var s string + if len(args) == 1 { + s, ok = args[0].(string) + } + if !ok { + s = fmt.Sprint(args...) + } + // find the @ symbol + substrs := strings.Split(s, "@") + if len(substrs) != 2 { + return s + } + // replace the @ by " at " + return (substrs[0] + " at " + substrs[1]) + } + + func main() { + f1 := Friend{Fname: "minux.ma"} + f2 := Friend{Fname: "xushiwei"} + t := template.New("fieldname example") + t = t.Funcs(template.FuncMap{"emailDeal": EmailDealWith}) + t, _ = t.Parse(`hello {{.UserName}}! + {{range .Emails}} + an emails {{.|emailDeal}} + {{end}} + {{with .Friends}} + {{range .}} + my friend name is {{.Fname}} + {{end}} + {{end}} + `) + p := Person{UserName: "Astaxie", + Emails: []string{"astaxie@beego.me", "astaxie@gmail.com"}, + Friends: []*Friend{&f1, &f2}} + t.Execute(os.Stdout, p) + } + + +上面演示了如何自定义函数,其实,在模板包内部已经有内置的实现函数,下面代码截取自模板包里面 + + var builtins = FuncMap{ + "and": and, + "call": call, + "html": HTMLEscaper, + "index": index, + "js": JSEscaper, + "len": length, + "not": not, + "or": or, + "print": fmt.Sprint, + "printf": fmt.Sprintf, + "println": fmt.Sprintln, + "urlquery": URLQueryEscaper, + } + + +## Must操作 +模板包里面有一个函数`Must`,它的作用是检测模板是否正确,例如大括号是否匹配,注释是否正确的关闭,变量是否正确的书写。接下来我们演示一个例子,用Must来判断模板是否正确: + + package main + + import ( + "fmt" + "text/template" + ) + + func main() { + tOk := template.New("first") + template.Must(tOk.Parse(" some static text /* and a comment */")) + fmt.Println("The first one parsed OK.") + + template.Must(template.New("second").Parse("some static text {{ .Name }}")) + fmt.Println("The second one parsed OK.") + + fmt.Println("The next one ought to fail.") + tErr := template.New("check parse error with Must") + template.Must(tErr.Parse(" some static text {{ .Name }")) + } + +讲输出如下内容 + + The first one parsed OK. + The second one parsed OK. + The next one ought to fail. + panic: template: check parse error with Must:1: unexpected "}" in command + +## 嵌套模板 +我们平常开发Web应用的时候,经常会遇到一些模板有些部分是固定不变的,然后可以抽取出来作为一个独立的部分,例如一个博客的头部和尾部是不变的,而唯一改变的是中间的内容部分。所以我们可以定义成`header`、`content`、`footer`三个部分。Go语言中通过如下的语法来申明 + + {{define "子模板名称"}}内容{{end}} + +通过如下方式来调用: + + {{template "子模板名称"}} + +接下来我们演示如何使用嵌套模板,我们定义三个文件,`header.tmpl`、`content.tmpl`、`footer.tmpl`文件,里面的内容如下 + + //header.tmpl + {{define "header"}} + + + 演示信息 + + + {{end}} + + //content.tmpl + {{define "content"}} + {{template "header"}} +

演示嵌套

+
    +
  • 嵌套使用define定义子模板
  • +
  • 调用使用template
  • +
+ {{template "footer"}} + {{end}} + + //footer.tmpl + {{define "footer"}} + + + {{end}} + +演示代码如下: + + package main + + import ( + "fmt" + "os" + "text/template" + ) + + func main() { + s1, _ := template.ParseFiles("header.tmpl", "content.tmpl", "footer.tmpl") + s1.ExecuteTemplate(os.Stdout, "header", nil) + fmt.Println() + s1.ExecuteTemplate(os.Stdout, "content", nil) + fmt.Println() + s1.ExecuteTemplate(os.Stdout, "footer", nil) + fmt.Println() + s1.Execute(os.Stdout, nil) + } + +通过上面的例子我们可以看到通过`template.ParseFiles`把所有的嵌套模板全部解析到模板里面,其实每一个定义的{{define}}都是一个独立的模板,他们相互独立,是并行存在的关系,内部其实存储的是类似map的一种关系(key是模板的名称,value是模板的内容),然后我们通过`ExecuteTemplate`来执行相应的子模板内容,我们可以看到header、footer都是相对独立的,都能输出内容,contenrt中因为嵌套了header和footer的内容,就会同时输出三个的内容。但是当我们执行`s1.Execute`,没有任何的输出,因为在默认的情况下没有默认的子模板,所以不会输出任何的东西。 + +>同一个集合类的模板是互相知晓的,如果同一模板被多个集合使用,则它需要在多个集合中分别解析 + +## 总结 +通过上面对模板的详细介绍,我们了解了如何把动态数据与模板融合:如何输出循环数据、如何自定义函数、如何嵌套模板等等。通过模板技术的应用,我们可以完成MVC模式中V的处理,接下来的章节我们将介绍如何来处理M和C。 + +## links + * [目录]() + * 上一节: [正则处理](<7.3.md>) + * 下一节: [小结](<7.5.md>) diff --git a/ref.md b/ref.md index c4b05a7e..e3aa2d8b 100644 --- a/ref.md +++ b/ref.md @@ -7,4 +7,5 @@ 3. [go book](http://go-book.appsp0t.com/) 4. [golangtutorials](http://golangtutorials.blogspot.com) 5. [轩脉刃de刀光剑影](http://www.cnblogs.com/yjf512/) -6. [Go 官网文档](http://golang.org/doc/) \ No newline at end of file +6. [Go 官网文档](http://golang.org/doc/) +7. [Network programming with Go](http://jan.newmarch.name/go/) \ No newline at end of file