GO自研微服务框架-页面渲染

发布时间:2024年01月15日

页面渲染

在实际开发中,接口返回需要支持返回HTML,JSON,XML等,在HTML返回中,要支持模板

1. HTML

渲染HTML,需要明确几个元素

  1. content-type = text/html; charset=utf-8
  2. 模板Template
  3. 渲染数据

渲染页面的操作是用户来完成,所以需要在Context中提供对应的方法

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
}
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 {
	*router
	funcMap    template.FuncMap
	HTMLRender 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 = filepath

	http.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 {
			string
			Cap 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 render

import "net/http"

type Render interface {
	Render(w http.ResponseWriter) error
	WriteContentType(w http.ResponseWriter)
}

internal 目录下的包,不允许被其他项目中进行导入,这是在 Go 1.4 当中引入的 feature,会在编译时执行

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
}

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 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)
}

func (c *Context) XML(status int, data any) error {
	return c.Render(status, render.XML{Data: data})
}

7.1.2 JSON
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
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)
}

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 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) {}

func (c *Context) Redirect(status int, location string) {
	c.Render(status, render.Redirect{
		Code:     status,
		Request:  c.R,
		Location: location,
	})
}
文章来源:https://blog.csdn.net/lisus2007/article/details/135602308
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。