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

页面渲染

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

1. HTML

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

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

渲染页面的操作是用户来完成,所以需要在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,
	})
}
相关推荐
阿里云云原生2 天前
MSE Nacos Prompt 管理:让 AI Agent 的核心配置真正可治理
微服务·云原生
阿里云云原生2 天前
阿里云微服务引擎 MSE 及 API 网关 2026 年 1 月产品动态
微服务
花酒锄作田2 天前
Gin 框架中的规范响应格式设计与实现
golang·gin
麦聪聊数据2 天前
统一 Web SQL 平台如何收编企业内部的“野生数据看板”?
数据库·sql·低代码·微服务·架构
qwfys2003 天前
How to install golang 1.26.0 to Ubuntu 24.04
ubuntu·golang·install
云司科技codebuddy3 天前
技术支持过硬Trae核心代理
大数据·运维·python·微服务
codeejun3 天前
每日一Go-25、Go语言进阶:深入并发模式1
开发语言·后端·golang
递归尽头是星辰3 天前
微服务事务分级治理:从 Seata 全模式到 TDSQL 实战
微服务·云原生·架构·分布式事务·事务分级治理
没有bug.的程序员3 天前
订单系统重构史诗:从单体巨兽到微服务矩阵的演进、数据一致性内核与分布式事务
java·微服务·矩阵·重构·分布式事务·数据一致性·订单系统
石牌桥网管3 天前
Go 泛型(Generics)
服务器·开发语言·golang