页面渲染
在实际开发中,接口返回需要支持返回HTML,JSON,XML等,在HTML返回中,要支持模板
1. HTML
渲染HTML,需要明确几个元素
- content-type =
text/html; charset=utf-8
- 模板Template
- 渲染数据
渲染页面的操作是用户来完成,所以需要在Context中提供对应的方法
package msgoimport ("log""net/http"
)type Context struct {W http.ResponseWriterR *http.Request
}
func (c *Context) HTML(status int, html string) error {//状态是200c.W.Header().Set("Content-Type", "text/html; charset=utf-8")c.W.WriteHeader(http.StatusOK)_, err := c.W.Write([]byte(html))return err
}
g.Get("/html", func(ctx *msgo.Context) {ctx.HTML(http.StatusOK, "<h1>GO自研微服务框架</h1>")
})
1.1 加入模板支持
func (c *Context) HTMLTemplate(name string, funcMap template.FuncMap, data any, fileName ...string) {t := template.New(name)t.Funcs(funcMap)t, err := t.ParseFiles(fileName...)if err != nil {log.Println(err)return}c.W.Header().Set("Content-Type", "text/html; charset=utf-8")err = t.Execute(c.W, data)if err != nil {log.Println(err)}
}func (c *Context) HTMLTemplateGlob(name string, funcMap template.FuncMap, pattern string, data any) {t := template.New(name)t.Funcs(funcMap)t, err := t.ParseGlob(pattern)if err != nil {log.Println(err)return}c.W.Header().Set("Content-Type", "text/html; charset=utf-8")err = t.Execute(c.W, data)if err != nil {log.Println(err)}
}
g.Get("/htmltemplate", func(ctx *msgo.Context) {user := &User{Name: "lisus",}err := ctx.HTMLTemplate("login.html", user, "tpl/login.html", "tpl/header.html")if err != nil {log.Println(err)}})g.Get("/htmltemplateGlob", func(ctx *msgo.Context) {user := &User{Name: "lisus",}err := ctx.HTMLTemplateGlob("login.html", user, "tpl/*.html")if err != nil {log.Println(err)}})
{{ define "header" }}
<h1>这是头部页</h1>
{{ end }}
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body><h1>这是首页</h1>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body>{{ template "header" .}}<h1>这是登录页</h1><h2>用户名:{{ .Name }}</h2>
</body>
</html>
1.2 改造-提前将模板加载到内存
如果使用到模板,并不需要在访问的时候再加载,可以在启动的时候,就将所有的模板加载到内存中,这样加快访问速度
type Engine struct {*routerfuncMap template.FuncMapHTMLRender render.HTMLRender
}
func (e *Engine) SetFuncMap(funcMap template.FuncMap) {e.funcMap = funcMap
}// LoadTemplateGlob 加载所有模板
func (e *Engine) LoadTemplateGlob(pattern string) {t := template.Must(template.New("").Funcs(e.funcMap).ParseGlob(pattern))e.SetHtmlTemplate(t)
}func (e *Engine) SetHtmlTemplate(t *template.Template) {e.HTMLRender = render.HTMLRender{Template: t}
}
type HTMLRender struct {Template *template.Template
}
func (c *Context) Template(name string, data any) error {c.W.Header().Set("Content-Type", "text/html; charset=utf-8")err := c.engine.HTMLRender.Template.ExecuteTemplate(c.W, name, data)return err
}
engine.LoadTemplate("tpl/*.html")
g.Get("/template", func(ctx *msgo.Context) {user := &User{Name: "lisus",}err := ctx.Template("login.html", user)if err != nil {log.Println(err)}
})
2. JSON
除了返回模板页面,在多数情况下,返回JSON的应用场景也非常普遍。
有了上面的经验,在处理返回json的时候,会变得比较容易。
json的content-type=application/json; charset=utf-8
func (c *Context) JSON(status int, data any) error {c.W.Header().Set("Content-Type", "application/json; charset=utf-8")c.W.WriteHeader(status)rsp, err := json.Marshal(data)if err != nil {return err}_, err = c.W.Write(rsp)if err != nil {return err}return nil
}
g.Get("/json", func(ctx *msgo.Context) {user := &User{Name: "lisus",}err := ctx.JSON(200, user)if err != nil {log.Println(err)}
})
3. XML
content-type=application/xml;charset=utf-8
func (c *Context) XML(status int, data any) error {c.W.Header().Set("Content-Type", "application/xml; charset=utf-8")c.W.WriteHeader(status)err := xml.NewEncoder(c.W).Encode(data)return err
}
g.Get("/xml", func(ctx *msgo.Context) {user := &User{Name: "lisus",Age: 20,}err := ctx.XML(200, user)if err != nil {log.Println(err)}
})
4. 文件
下载文件的需求,需要返回excel文件,word文件等等的
g.Get("/excel", func(ctx *msgo.Context) {ctx.File("tpl/test.xlsx")})
func (c *Context) File(filePath string) {http.ServeFile(c.W, c.R, filePath)
}
指定文件名字:
func isASCII(s string) bool {for i := 0; i < len(s); i++ {if s[i] > unicode.MaxASCII {return false}}return true
}
func (c *Context) FileAttachment(filepath, filename string) {if isASCII(filename) {c.W.Header().Set("Content-Disposition", `attachment; filename="`+filename+`"`)} else {c.W.Header().Set("Content-Disposition", `attachment; filename*=UTF-8''`+url.QueryEscape(filename))}http.ServeFile(c.W, c.R, filepath)
}
从文件系统获取:
g.Get("/fs", func(ctx *msgo.Context) {ctx.FileFromFS("test.xlsx", http.Dir("tpl"))})
func (c *Context) FileFromFS(filepath string, fs http.FileSystem) {defer func(old string) {c.R.URL.Path = old}(c.R.URL.Path)c.R.URL.Path = filepathhttp.FileServer(fs).ServeHTTP(c.W, c.R)
}
5. 重定向页面
在一些前后端分离开发中,我们需要进行页面的跳转,并不是去加载模板
func (c *Context) Redirect(status int, location string) {if (status < http.StatusMultipleChoices || status > http.StatusPermanentRedirect) && status != http.StatusCreated {panic(fmt.Sprintf("Cannot redirect with status code %d", status))}http.Redirect(c.W, c.R, location, status)
}
g.Get("/redirect", func(ctx *msgo.Context) {ctx.Redirect(http.StatusFound, "/user/template")
})
6. String
func StringToBytes(s string) []byte {return *(*[]byte)(unsafe.Pointer(&struct {stringCap int}{s, len(s)},))
}
func (c *Context) String(status int, format string, values ...any) (err error) {plainContentType := "text/plain; charset=utf-8"c.W.Header().Set("Content-Type", plainContentType)c.W.WriteHeader(status)if len(values) > 0 {_, err = fmt.Fprintf(c.W, format, values...)return}_, err = c.W.Write(StringToBytes(format))return
}
g.Get("/string", func(ctx *msgo.Context) {ctx.String(http.StatusOK, "%s 是由 %s 制作 \n", "goweb框架", "go微服务框架")})
7. 接口提取
实际上,我们需要支持的格式是很多的,将其抽象提取成接口,便于后续拓展
package renderimport "net/http"type Render interface {Render(w http.ResponseWriter) errorWriteContentType(w http.ResponseWriter)
}
internal 目录下的包,不允许被其他项目中进行导入,这是在 Go 1.4 当中引入的 feature,会在编译时执行
package renderimport ("fmt""github.com/mszlu521/msgo/internal/bytesconv""net/http"
)type String struct {Format stringData []any
}var plainContentType = []string{"text/plain; charset=utf-8"}func (r String) WriteContentType(w http.ResponseWriter) {writeContentType(w, plainContentType)
}func (r String) Render(w http.ResponseWriter) error {return WriteString(w, r.Format, r.Data)
}func WriteString(w http.ResponseWriter, format string, data []any) (err error) {writeContentType(w, plainContentType)if len(data) > 0 {_, err = fmt.Fprintf(w, format, data...)return}_, err = w.Write(bytesconv.StringToBytes(format))return
}
func (c *Context) String(status int, format string, values ...any) (err error) {err = c.Render(status, render.String{Format: format,Data: values,})return
}func (c *Context) Render(code int, r render.Render) error {err := r.Render(c.W)c.W.WriteHeader(code)return err
}
7.1 其他渲染方式重构
7.1.1 XML
package renderimport ("encoding/xml""net/http"
)type XML struct {Data any
}var xmlContentType = []string{"application/xml; charset=utf-8"}func (r XML) Render(w http.ResponseWriter) error {r.WriteContentType(w)return xml.NewEncoder(w).Encode(r.Data)
}func (r XML) WriteContentType(w http.ResponseWriter) {writeContentType(w, xmlContentType)
}
func (c *Context) XML(status int, data any) error {return c.Render(status, render.XML{Data: data})
}
7.1.2 JSON
package renderimport ("encoding/json""net/http"
)type JSON struct {Data any
}var jsonContentType = []string{"application/json; charset=utf-8"}func (r JSON) Render(w http.ResponseWriter) error {return WriteJSON(w, r.Data)
}
func (r JSON) WriteContentType(w http.ResponseWriter) {writeContentType(w, jsonContentType)
}func WriteJSON(w http.ResponseWriter, obj any) error {writeContentType(w, jsonContentType)jsonBytes, err := json.Marshal(obj)if err != nil {return err}_, err = w.Write(jsonBytes)return err
}
7.1.3 HTML
package renderimport ("html/template""net/http"
)type HTMLData anytype HTML struct {Template *template.TemplateName stringData HTMLDataIsTemplate bool
}var htmlContentType = []string{"text/html; charset=utf-8"}type HTMLRender struct {Template *template.Template
}func (r HTML) Render(w http.ResponseWriter) error {r.WriteContentType(w)if !r.IsTemplate {_, err := w.Write([]byte(r.Data.(string)))return err}err := r.Template.ExecuteTemplate(w, r.Name, r.Data)return err
}func (r HTML) WriteContentType(w http.ResponseWriter) {writeContentType(w, htmlContentType)
}
func (c *Context) HTML(status int, html string) {c.Render(status, render.HTML{IsTemplate: false, Data: html})
}func (c *Context) HTMLTemplate(name string, data any) {c.Render(http.StatusOK, render.HTML{IsTemplate: true,Name: name,Data: data,Template: c.engin.HTMLRender.Template,})
}
7.1.4 Redirect
package renderimport ("fmt""net/http"
)type Redirect struct {Code intRequest *http.RequestLocation string
}func (r Redirect) Render(w http.ResponseWriter) error {if (r.Code < http.StatusMultipleChoices || r.Code > http.StatusPermanentRedirect) && r.Code != http.StatusCreated {panic(fmt.Sprintf("Cannot redirect with status code %d", r.Code))}http.Redirect(w, r.Request, r.Location, r.Code)return nil
}// WriteContentType (Redirect) don't write any ContentType.
func (r Redirect) WriteContentType(http.ResponseWriter) {}
func (c *Context) Redirect(status int, location string) {c.Render(status, render.Redirect{Code: status,Request: c.R,Location: location,})
}