编写GO的WEB开发框架 (七): Response封装和模板渲染

WEB应用的处理流程中,获取请求参数,调用业务逻辑处理后,下一步就是响应输出,在GO中,就是向ResponseWriter写数据。html

fmt.Fprintf(this.ResponseWriter, format, data...)

封装经常使用输出方法

对于常规输出,作一个简单的封装,以简化调用的方式nginx

func (this *App)Resp(format string,data ...interface{}){
	fmt.Fprintf(this.ResponseWriter, format, data...)
}
// 调用
// this.Resp("hello %s",User.Name)

http协议中,响应内容除了body的输出,还包括 Header和Cookie等,对这两种也进行一个简单的封装数据库

func (this *App) SetHeader(key, val string){
}

func (this *App) SetCookie(c ...interface{}) (err error){
	// 支持三种方式(判断参数c的个数和类型)
	// SetCookie(name string,val string)
	// SetCookie(name string,val string,expire int) 
	// SetCookie(c *http.Cookie)
}

扩展http.ResponseWriter

http.ResponseWriter只是一个简单的接口,若是想要记录所响应的状态码、响应大小(access_log中用到),就须要扩展该接口。this

type resWriter struct {
	http.ResponseWriter
	Length int
	Code   int
}
//重写Write方法,记录响应的内容大小
func (this *resWriter) Write(b []byte) (n int, err error) {
	n, err = this.ResponseWriter.Write(b)
	this.Length += n
	return
}
//重写WriterHeader方法,记录响应码
func (this *resWriter) WriteHeader(code int) {
	this.ResponseWriter.WriteHeader(code)
	this.Code = code
}

这样,在须要用到ResponseWriter做为参数的地方,统一使用resWriter替换,resWriter除了调用ResponseWriter来正常输出,还会自动记录响应内容的大小和状态码了。code

视图模板

** html/template ** 包提供了使用视图模板进行渲染的功能。其基本使用方法是:regexp

  • 字符串模板(可手工从文件或数据库读入)
tpl := "" //模板内容
tmpl, err := template.New("test").Parse(tpl) 
//check err
tmpl.Execute(this.ResWriter, data)
  • 直接渲染模板文件
s1, _ := template.ParseFiles("a.tmpl")
s1.Exeute(this.ResWriter,data)

传统方式下多模板合并

实际应用时,通常会将一个网页分红头部,内容和页脚等多个子模板,传统的方式这样编写模板:orm

//header.tpl
{{define "header"}}
<!doctype html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
{{end}}

//body.tpl
{{define "content"}}
{{template "header"}}
<h1>演示嵌套</h1>
<ul>
    <li>嵌套使用define定义子模板</li>
    <li>调用使用template</li>
</ul>
{{template "footer"}}
{{end}}

//footer.tpl
{{define "footer"}}
</body>
</html>
{{end}}

而后用如下方式渲染:htm

t, _ := template.ParseFiles("header.tpl", "body.tpl", "footer.tpl")
t.ExecuteTemplate(this.ResWriter, "header", nil)
t.ExecuteTemplate(this.ResWriter, "body", nil)
t.ExecuteTemplate(this.ResWriter, "footer", nil)
t.Execute(this.ResWriter, nil)

能够看出,这种方式不管是编写模板仍是渲染都有不足:递归

  • 模板的定义有点罗嗦,子模板要使用define定义,引用子模板的地方还要用template关键字声明接口

  • 渲染时要屡次调用Excute

改进的多模板合并

我理想的多模板组织方式是:

//main.tpl
{{#include header.tpl}}
<body>
<h1>演示嵌套</h1>
<ul>
    <li>嵌套使用define定义子模板</li>
    <li>调用使用template</li>
</ul>
</body>
{{#include footer.tpl}}

//header.tpl
<!doctype html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>

//footer.tpl
</body>
</html>

这种方式与人的思惟比较接近,比较容易理解。

更好的方式是:能够改用nginx的SSI语法来include,能够直接进行全局预览:

<!--# include file="footer.tpl" -->

而后,直接在Controller中使用** this.Render("main.tpl",data) ** 调用便可,内部的引用自动完成。

使用者无需知道主模板使用了多个子模板,更重要的是,修改了模板结构后(好比再分拆出一个子模板),只要里面的要替换的变量不变,根本不用改代码和重编译,甚至是服务都不用重启。

Render具体实现

下面来看看支持上述方式使用模板的Render如何实现:

基本思路是读取模板文件内容,用正则搜索其中的 {{#include xxx}}内容,获得全部子模板,并将子模板的内容读取回填到相应位置(暂不支持子模板再include子模板,要支持其实也是一个递归,但不必)

func (this *App) Render(tpl string, data interface{})
	viewPath : = "" //模板文件目录
	mf, err := os.Open(ViewPath + tpl)
	//check err
	defer mf.Close()
	content, _ := ioutil.ReadAll(mf)
	reg := regexp.MustCompile(`\{\{#include "(.*)"\}\}`)
	//遍历引用的子文件
	for _, v := range reg.FindAllSubmatch(content, -1) { //遍历匹配到的内容进行替换
		incFile := fmt.Sprintf("%s/%s", viewPath, v[1]) //同一层目录
		f1, err := os.Open(incFile)
		//check error
		defer f1.Close()
		incContent, _ := ioutil.ReadAll(f1)
		content = bytes.Replace(content, v[0], incContent, 1)
	}
	t := template.New(tpl).Parse(string(content))
	t.Excute(this.ResWriter,data)
}

需注意的是,模板的编译最好在服务启动时就进行,避免在Render时进行读取,形成耗时太长。

后面的内容会讲述如何将模板预编译放到服务启动时进行,同时说明怎样支持模板变动后的动态热更新。

相关文章
相关标签/搜索