页面渲染
在实际开发中,接口返回需要支持返回HTML,JSON,XML等,在HTML返回中,要支持模板
1. HTML
渲染HTML,需要明确几个元素
- content-type =
text/html; charset=utf-8
- 模板Template
- 渲染数据
渲染页面的操作是用户来完成,所以需要在Context中提供对应的方法
go
package msgo
import (
"log"
"net/http"
)
type Context struct {
W http.ResponseWriter
R *http.Request
}
func (c *Context) HTML(status int, html string) error {
//状态是200
c.W.Header().Set("Content-Type", "text/html; charset=utf-8")
c.W.WriteHeader(http.StatusOK)
_, err := c.W.Write([]byte(html))
return err
}
go
g.Get("/html", func(ctx *msgo.Context) {
ctx.HTML(http.StatusOK, "<h1>GO自研微服务框架</h1>")
})
1.1 加入模板支持
go
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)
}
}
go
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)
}
})
go
{{ define "header" }}
<h1>这是头部页</h1>
{{ end }}
go
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>这是首页</h1>
</body>
</html>
go
<!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 改造-提前将模板加载到内存
如果使用到模板,并不需要在访问的时候再加载,可以在启动的时候,就将所有的模板加载到内存中,这样加快访问速度
go
type Engine struct {
*router
funcMap template.FuncMap
HTMLRender render.HTMLRender
}
go
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}
}
go
type HTMLRender struct {
Template *template.Template
}
go
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
}
go
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
go
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
}
go
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
go
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
}
go
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文件等等的
go
g.Get("/excel", func(ctx *msgo.Context) {
ctx.File("tpl/test.xlsx")
})
go
func (c *Context) File(filePath string) {
http.ServeFile(c.W, c.R, filePath)
}
指定文件名字:
go
func isASCII(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] > unicode.MaxASCII {
return false
}
}
return true
}
go
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)
}
从文件系统获取:
go
g.Get("/fs", func(ctx *msgo.Context) {
ctx.FileFromFS("test.xlsx", http.Dir("tpl"))
})
go
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 = filepath
http.FileServer(fs).ServeHTTP(c.W, c.R)
}
5. 重定向页面
在一些前后端分离开发中,我们需要进行页面的跳转,并不是去加载模板
go
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)
}
go
g.Get("/redirect", func(ctx *msgo.Context) {
ctx.Redirect(http.StatusFound, "/user/template")
})
6. String
go
func StringToBytes(s string) []byte {
return *(*[]byte)(unsafe.Pointer(
&struct {
string
Cap int
}{s, len(s)},
))
}
go
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
}
go
g.Get("/string", func(ctx *msgo.Context) {
ctx.String(http.StatusOK, "%s 是由 %s 制作 \n", "goweb框架", "go微服务框架")
})
7. 接口提取
实际上,我们需要支持的格式是很多的,将其抽象提取成接口,便于后续拓展
go
package render
import "net/http"
type Render interface {
Render(w http.ResponseWriter) error
WriteContentType(w http.ResponseWriter)
}
internal 目录下的包,不允许被其他项目中进行导入,这是在 Go 1.4 当中引入的 feature,会在编译时执行
go
package render
import (
"fmt"
"github.com/mszlu521/msgo/internal/bytesconv"
"net/http"
)
type String struct {
Format string
Data []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
}
go
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
go
package render
import (
"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)
}
go
func (c *Context) XML(status int, data any) error {
return c.Render(status, render.XML{Data: data})
}
7.1.2 JSON
go
package render
import (
"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
go
package render
import (
"html/template"
"net/http"
)
type HTMLData any
type HTML struct {
Template *template.Template
Name string
Data HTMLData
IsTemplate 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)
}
go
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
go
package render
import (
"fmt"
"net/http"
)
type Redirect struct {
Code int
Request *http.Request
Location 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) {}
go
func (c *Context) Redirect(status int, location string) {
c.Render(status, render.Redirect{
Code: status,
Request: c.R,
Location: location,
})
}