編寫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時進行讀取,形成耗時太長。

後面的內容會講述如何將模板預編譯放到服務啓動時進行,同時說明怎樣支持模板變動後的動態熱更新。

相關文章
相關標籤/搜索