傳統的 MVC 框架大多數是基於 Action 設計的字尾式對映,然而,現在 Web 流行 REST 風格的架構。儘管使用 Filter 或者 rewrite 能夠透過 URL 重寫實現 REST 風格的 URL,但是為什麼不直接設計一個全新的 REST 風格的 MVC 框架呢?本小節就是基於這種思路來講述如何從頭設計一個基於 REST 風格的 MVC 框架中的 controller,最大限度地簡化 Web 應用的開發,甚至編寫一行程式碼就可以實現“Hello, world”。
MVC 設計模式是目前 Web 應用開發中最常見的架構模式,透過分離 Model(模型)、View(檢視)和 Controller(控制器),可以更容易實現易於擴充套件的使用者介面(UI)。Model 指後臺回傳的資料;View 指需要渲染的頁面,通常是範本頁面,渲染後的內容通常是 HTML;Controller 指 Web 開發人員編寫的處理不同 URL 的控制器,如前面小節講述的路由就是 URL 請求轉發到控制器的過程,controller 在整個的 MVC 框架中起到了一個核心的作用,負責處理業務邏輯,因此控制器是整個框架中必不可少的一部分,Model 和 View 對於有些業務需求是可以不寫的,例如沒有資料處理的邏輯處理,沒有頁面輸出的 302 調整之類別的就不需要 Model 和 View,但是 controller 這一環節是必不可少的。
前面小節介紹了路由實現了註冊 struct 的功能,而 struct 中實現了 REST 方式,因此我們需要設計一個用於邏輯處理 controller 的基底類別,這裡主要設計了兩個型別,一個 struct、一個 interface
type Controller struct {
Ct *Context
Tpl *template.Template
Data map[interface{}]interface{}
ChildName string
TplNames string
Layout []string
TplExt string
}
type ControllerInterface interface {
Init(ct *Context, cn string) //初始化上下文和子類別名稱稱
Prepare() //開始執行之前的一些處理
Get() //method=GET 的處理
Post() //method=POST 的處理
Delete() //method=DELETE 的處理
Put() //method=PUT 的處理
Head() //method=HEAD 的處理
Patch() //method=PATCH 的處理
Options() //method=OPTIONS 的處理
Finish() //執行完成之後的處理
Render() error //執行完 method 對應的方法之後渲染頁面
}
那麼前面介紹的路由 add 函式的時候是定義了 ControllerInterface 型別,因此,只要我們實現這個介面就可以,所以我們的基底類別 Controller 實現如下的方法:
func (c *Controller) Init(ct *Context, cn string) {
c.Data = make(map[interface{}]interface{})
c.Layout = make([]string, 0)
c.TplNames = ""
c.ChildName = cn
c.Ct = ct
c.TplExt = "tpl"
}
func (c *Controller) Prepare() {
}
func (c *Controller) Finish() {
}
func (c *Controller) Get() {
http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}
func (c *Controller) Post() {
http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}
func (c *Controller) Delete() {
http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}
func (c *Controller) Put() {
http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}
func (c *Controller) Head() {
http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}
func (c *Controller) Patch() {
http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}
func (c *Controller) Options() {
http.Error(c.Ct.ResponseWriter, "Method Not Allowed", 405)
}
func (c *Controller) Render() error {
if len(c.Layout) > 0 {
var filenames []string
for _, file := range c.Layout {
filenames = append(filenames, path.Join(ViewsPath, file))
}
t, err := template.ParseFiles(filenames...)
if err != nil {
Trace("template ParseFiles err:", err)
}
err = t.ExecuteTemplate(c.Ct.ResponseWriter, c.TplNames, c.Data)
if err != nil {
Trace("template Execute err:", err)
}
} else {
if c.TplNames == "" {
c.TplNames = c.ChildName + "/" + c.Ct.Request.Method + "." + c.TplExt
}
t, err := template.ParseFiles(path.Join(ViewsPath, c.TplNames))
if err != nil {
Trace("template ParseFiles err:", err)
}
err = t.Execute(c.Ct.ResponseWriter, c.Data)
if err != nil {
Trace("template Execute err:", err)
}
}
return nil
}
func (c *Controller) Redirect(url string, code int) {
c.Ct.Redirect(code, url)
}
上面的 controller 基底類別已經實現了介面定義的函式,透過路由根據 url 執行相應的 controller 的原則,會依次執行如下:
Init() 初始化
Prepare() 執行之前的初始化,每個繼承的子類別可以來實現該函式
method() 根據不同的 method 執行不同的函式:GET、POST、PUT、HEAD 等,子類別來實現這些函式,如果沒實現,那麼預設都是 403
Render() 可選,根據全域性變數 AutoRender 來判斷是否執行
Finish() 執行完之後執行的操作,每個繼承的子類別可以來實現該函式
上面 beego 框架中完成了 controller 基底類別的設計,那麼我們在我們的應用中可以這樣來設計我們的方法:
package controllers
import (
"github.com/astaxie/beego"
)
type MainController struct {
beego.Controller
}
func (this *MainController) Get() {
this.Data["Username"] = "astaxie"
this.Data["Email"] = "[email protected]"
this.TplNames = "index.tpl"
}
上面的方式我們實現了子類別 MainController,實現了 Get 方法,那麼如果使用者透過其他的方式(POST/HEAD 等)來訪問該資源都將回傳 405,而如果是 Get 來訪問,因為我們設定了 AutoRender=true,那麼在執行完 Get 方法之後會自動執行 Render 函式,就會顯示如下介面:
index.tpl 的程式碼如下所示,我們可以看到資料的設定和顯示都是相當的簡單方便:
<!DOCTYPE html>
<html>
<head>
<title>beego welcome template</title>
</head>
<body>
<h1>Hello, world!{{.Username}},{{.Email}}</h1>
</body>
</html>