hz 自定义模板使用
Hertz 提供的命令行工具 (以下称为"hz") 支持自定义模板功能,包括:
- 自定义 layout 模板 (即生成代码的目录结构)
- 自定义 package 模板 (即与 service 相关的代码结构,包括 handler、router 等)
用户可自己提供模板以及渲染参数,并结合 hz 的能力,来完成自定义的代码生成结构。
自定义 layout 模板
用户可根据默认模板来修改或重写,从而满足自身需求
hz 利用了 go template 支持以 “yaml” 的格式定义模板,并使用 “json” 定义模板渲染数据。
所谓的 layout 模板是指整个项目的结构,这些结构与具体的 idl 定义无关,不需要 idl 也可以直接生成,默认的结构如下:
.
├── biz
│ ├── handler
│ │ └── ping.go
│ │ └── ****.go // 按照服务划分的 handler 集合,位置可根据 handler_dir 改变
│ ├── model
│ │ └── model.go // idl 生成的 struct,位置可根据 model_dir 改变
│ └── router // 未开发自定义 dir
│ └── register.go // 路由注册,用来调用具体的路由注册
│ └── route.go // 具体路由注册位置
│ └── middleware.go // 默认 middleware 生成位置
├── .hz // hz 创建代码标志
├── go.mod
├── main.go // 启动入口
├── router.go // 用户自定义路由写入位置
└── router_gen.go // hz 生成的路由注册调用
IDL
// hello.thrift
namespace go hello.example
struct HelloReq {
1: string Name (api.query="name");
}
struct HelloResp {
1: string RespBody;
}
service HelloService {
HelloResp HelloMethod(1: HelloReq request) (api.get="/hello");
}
命令
hz new --mod=github.com/hertz/hello --idl=./hertzDemo/hello.thrift --customize_layout=template/layout.yaml --customize_layout_data_path=template/data.json
默认 layout 模板的含义
注:以下的 body 均为 go template
layouts:
# 生成的 handler 的目录,只有目录下有文件才会生成
- path: biz/handler/
delims:
- ""
- ""
body: ""
# 生成的 model 的目录,只有目录下有文件才会生成
- path: biz/model/
delims:
- ""
- ""
body: ""
# 项目 main 文件,
- path: main.go
delims:
- ""
- ""
body: |-
// Code generated by hertz generator.
package main
import (
"github.com/cloudwego/hertz/pkg/app/server"
)
func main() {
h := server.Default()
register(h)
h.Spin()
}
# go.mod 文件,需要模板渲染数据{{.GoModule}}才能生成
- path: go.mod
delims:
- '{{'
- '}}'
body: |-
module {{.GoModule}}
{{- if .UseApacheThrift}}
replace github.com/apache/thrift => github.com/apache/thrift v0.13.0
{{- end}}
# .gitignore 文件
- path: .gitignore
delims:
- ""
- ""
body: "*.o\n*.a\n*.so\n_obj\n_test\n*.[568vq]\n[568vq].out\n*.cgo1.go\n*.cgo2.c\n_cgo_defun.c\n_cgo_gotypes.go\n_cgo_export.*\n_testmain.go\n*.exe\n*.exe~\n*.test\n*.prof\n*.rar\n*.zip\n*.gz\n*.psd\n*.bmd\n*.cfg\n*.pptx\n*.log\n*nohup.out\n*settings.pyc\n*.sublime-project\n*.sublime-workspace\n!.gitkeep\n.DS_Store\n/.idea\n/.vscode\n/output\n*.local.yml\ndumped_hertz_remote_config.json\n\t\t
\ "
# .hz 文件,包含 hz 版本,是 hz 创建的项目的标志,不需要传渲染数据
- path: .hz
delims:
- '{{'
- '}}'
body: |-
// Code generated by hz. DO NOT EDIT.
hz version: {{.hzVersion}}
# ping 自带 ping 的 handler
- path: biz/handler/ping.go
delims:
- ""
- ""
body: |-
// Code generated by hertz generator.
package handler
import (
"context"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/common/utils"
)
// Ping .
func Ping(ctx context.Context, c *app.RequestContext) {
c.JSON(200, utils.H{
"message": "pong",
})
}
# 定义路由注册的文件,需要模板渲染数据{{.RouterPkgPath}}才能生成
- path: router_gen.go
delims:
- ""
- ""
body: |-
// Code generated by hertz generator. DO NOT EDIT.
package main
import (
"github.com/cloudwego/hertz/pkg/app/server"
router "{{.RouterPkgPath}}"
)
// register registers all routers.
func register(r *server.Hertz) {
router.GeneratedRegister(r)
customizedRegister(r)
}
# 自定义路由注册的文件
- path: router.go
delims:
- ""
- ""
body: |-
// Code generated by hertz generator.
package main
import (
"github.com/cloudwego/hertz/pkg/app/server"
handler "{{.HandlerPkgPath}}"
)
// customizeRegister registers customize routers.
func customizedRegister(r *server.Hertz){
r.GET("/ping", handler.Ping)
// your code ...
}
# 默认路由注册文件,不要修改
- path: biz/router/register.go
delims:
- ""
- ""
body: |-
// Code generated by hertz generator. DO NOT EDIT.
package router
import (
"github.com/cloudwego/hertz/pkg/app/server"
)
// GeneratedRegister registers routers generated by IDL.
func GeneratedRegister(r *server.Hertz){
//INSERT_POINT: DO NOT DELETE THIS LINE!
}
模板渲染参数文件的含义
当指定了自定义模板以及渲染数据后,此时命令行指定的选项将不会被作为渲染数据,因此,模板中的渲染数据需要用户自己定义。
hz 使用了"json"来指定渲染数据,下面进行介绍
{
// 全局的渲染参数
"*": {
"GoModule": "github.com/hz/test", // 要和命令行指定的一致,否则后续生成 model、handler 等代码将使用命令行指定的 mod,导致出现不一致。
"ServiceName": "p.s.m", // 要和命令行指定的一致
"UseApacheThrift": false // 根据是否使用"thrift"设置"true"/"false"
},
// router_gen.go 路由注册的渲染数据,
// "biz/router"指向默认 idl 注册的路由代码的 module,不要修改
"router_gen.go": {
"RouterPkgPath": "github.com/hz/test/biz/router"
}
}
自定义一个 layout 模板
目前,hz 生成的项目 layout 已经是一个 hertz 项目最最最基础的骨架了,所以不建议删除现有的模板里的文件。
不过如果用户想要一个别的 layout,当然也可以根据自身需求来删除相应的文件 (除"biz/register.go"外,其余都可以动)
我们十分欢迎用户来贡献自己的模板
下面假设用户只想要 “main.go” 以及 “go.mod” 文件,那么我们对默认模板进行修改,如下:
template
// layout.yaml
layouts:
# 项目 main 文件,
- path: main.go
delims:
- ""
- ""
body: |-
// Code generated by hertz generator.
package main
import (
"github.com/cloudwego/hertz/pkg/app/server"
"{{.GoModule}}/biz/router"
)
func main() {
h := server.Default()
router.GeneratedRegister(h)
// do what you wanted
// add some render data: {{.MainData}}
h.Spin()
}
# go.mod 文件,需要模板渲染数据{{.GoModule}}才能生成
- path: go.mod
delims:
- '{{'
- '}}'
body: |-
module {{.GoModule}}
{{- if .UseApacheThrift}}
replace github.com/apache/thrift => github.com/apache/thrift v0.13.0
{{- end}}
# 默认路由注册文件,没必要修改
- path: biz/router/register.go
delims:
- ""
- ""
body: |-
// Code generated by hertz generator. DO NOT EDIT.
package router
import (
"github.com/cloudwego/hertz/pkg/app/server"
)
// GeneratedRegister registers routers generated by IDL.
func GeneratedRegister(r *server.Hertz){
//INSERT_POINT: DO NOT DELETE THIS LINE!
}
render data
{
"*": {
"GoModule": "github.com/hertz/hello",
"ServiceName": "hello",
"UseApacheThrift": true
},
"main.go": {
"MainData": "this is customized render data"
}
}
命令:
hz new --mod=github.com/hertz/hello --idl=./hertzDemo/hello.thrift --customize_layout=template/layout.yaml --customize_layout_data_path=template/data.json
自定义 package 模板
hz 模板的模板地址:
用户可根据默认模板来修改或重写,从而符合自身需求
- 所谓的 package 模板是指与 idl 定义相关的服务代码,这部分代码涉及到定义 idl 时指定的 service、go_package/namespace 等,主要包括以下几部分:
- handler.go:处理函数逻辑
- router.go:具体的 idl 定义的服务的路由注册逻辑
- register.go:调用 router.go 中内容的逻辑
model 代码:生成的 go struct;不过由于目前使用插件来生成 model 代码工具没权限来修改 model 的模板,所以这部分功能先不开放
命令
hz new --mod=github.com/hertz/hello --handler_dir=handler_test --idl=hertzDemo/hello.thrift --customize_package=template/package.yaml
默认 package 模板
注意:自定义 package 模板没有提供渲染数据的功能,这里主要是因为这些渲染数据是 hz 工具解析生成的,所以暂时不提供自己写渲染数据的功能。可以修改下模板里面与渲染数据无关的部分,以满足自身需求。
# 以下数据都是 yaml marshal 得到的,所以可能看起来比较乱
layouts:
# path 只表示 handler.go 的模板,具体的 handler 路径由默认路径和 handler_dir 决定
- path: handler.go
delims:
- '{{'
- '}}'
body: |-
// Code generated by hertz generator.
package {{.PackageName}}
import (
"context"
"github.com/cloudwego/hertz/pkg/app"
{{- range $k, $v := .Imports}}
{{$k}} "{{$v.Package}}"
{{- end}}
)
{{range $_, $MethodInfo := .Methods}}
{{$MethodInfo.Comment}}
func {{$MethodInfo.Name}}(ctx context.Context, c *app.RequestContext) {
var err error
{{if ne $MethodInfo.RequestTypeName "" -}}
var req {{$MethodInfo.RequestTypeName}}
err = c.BindAndValidate(&req)
if err != nil {
c.String(400, err.Error())
return
}
{{end}}
resp := new({{$MethodInfo.ReturnTypeName}})
c.{{.Serializer}}(200, resp)
}
{{end}}
# path 只表示 router.go 的模板,其路径固定在:biz/router/namespace/
- path: router.go
delims:
- '{{'
- '}}'
body: |-
// Code generated by hertz generator. DO NOT EDIT.
package {{$.PackageName}}
import (
"github.com/cloudwego/hertz/pkg/app/server"
{{range $k, $v := .HandlerPackages}}{{$k}} "{{$v}}"{{end}}
)
/*
This file will register all the routes of the services in the master idl.
And it will update automatically when you use the "update" command for the idl.
So don't modify the contents of the file, or your code will be deleted when it is updated.
*/
{{define "g"}}
{{- if eq .Path "/"}}r
{{- else}}{{.GroupName}}{{end}}
{{- end}}
{{define "G"}}
{{- if ne .Handler ""}}
{{- .GroupName}}.{{.HttpMethod}}("{{.Path}}", append({{.MiddleWare}}Mw(), {{.Handler}})...)
{{- end}}
{{- if ne (len .Children) 0}}
{{.MiddleWare}} := {{template "g" .}}.Group("{{.Path}}", {{.MiddleWare}}Mw()...)
{{- end}}
{{- range $_, $router := .Children}}
{{- if ne .Handler ""}}
{{template "G" $router}}
{{- else}}
{ {{template "G" $router}}
}
{{- end}}
{{- end}}
{{- end}}
// Register register routes based on the IDL 'api.${HTTP Method}' annotation.
func Register(r *server.Hertz) {
{{template "G" .Router}}
}
# path 只表示 register.go 的模板,register 的路径固定为 biz/router/register.go
- path: register.go
delims:
- ""
- ""
body: |-
// Code generated by hertz generator. DO NOT EDIT.
package router
import (
"github.com/cloudwego/hertz/pkg/app/server"
{{$.PkgAlias}} "{{$.Pkg}}"
)
// GeneratedRegister registers routers generated by IDL.
func GeneratedRegister(r *server.Hertz){
//INSERT_POINT: DO NOT DELETE THIS LINE!
{{$.PkgAlias}}.Register(r)
}
- path: model.go
delims:
- ""
- ""
body: ""
# path 只表示 middleware.go 的模板,middleware 的路径和 router.go 一样为:biz/router/namespace/
- path: middleware.go
delims:
- '{{'
- '}}'
body: |-
// Code generated by hertz generator.
package {{$.PackageName}}
import (
"github.com/cloudwego/hertz/pkg/app"
)
{{define "M"}}
func {{.MiddleWare}}Mw() []app.HandlerFunc {
// your code...
return nil
}
{{range $_, $router := $.Children}}{{template "M" $router}}{{end}}
{{- end}}
{{template "M" .Router}}
# path 只表示 client.go 的模板,client 代码的生成路径由用户指定"${client_dir}"
- path: client.go
delims:
- '{{'
- '}}'
body: |-
// Code generated by hertz generator.
package {{$.PackageName}}
import (
"github.com/cloudwego/hertz/pkg/app/client"
"github.com/cloudwego/hertz/pkg/common/config"
)
type {{.ServiceName}}Client struct {
client * client.Client
}
func New{{.ServiceName}}Client(opt ...config.ClientOption) (*{{.ServiceName}}Client, error) {
c, err := client.NewClient(opt...)
if err != nil {
return nil, err
}
return &{{.ServiceName}}Client{
client: c,
}, nil
}
# handler_single 表示单独的 handler 模板,用于 update 的时候更新每一个新增的 handler
- path: handler_single.go
delims:
- '{{'
- '}}'
body: |+
{{.Comment}}
func {{.Name}}(ctx context.Context, c *app.RequestContext) {
// this my demo
var err error
{{if ne .RequestTypeName "" -}}
var req {{.RequestTypeName}}
err = c.BindAndValidate(&req)
if err != nil {
c.String(400, err.Error())
return
}
{{end}}
resp := new({{.ReturnTypeName}})
c.{{.Serializer}}(200, resp)
}
# middleware_single 表示单独的 middleware 模板,用于 update 的时候更新每一个新增的 middleware_single
- path: middleware_single.go
delims:
- '{{'
- '}}'
body: |+
func {{.MiddleWare}}Mw() []app.HandlerFunc {
// your code...
return nil
}
自定义一个 package 模板
与 layout 模板一样,用户同样可以自定义 package 模板。
就 package 提供的模板来说,一般用户可能只有自定义 handler.go 的模板的需求,因为 router.go/middleware.go/register.go 一般与 idl 定义相关而用户无需关心,因此 hz 目前也将这些模板生成的位置固定了,一般也无需修改。
因此,用户可根据自身的需求来自定义生成的 handler 模板,加速开发速度;但是由于默认的 handler 模板集成了一些 model 的信息以及 package 信息,所以需要 hz 工具来提供渲染数据。这部分用户可根据自身情况酌情来修改,一般建议留下 model 信息。
覆盖默认模板
目前,hz 本身自带了如下的模板:
- handler.go
- router.go
- register.go
- middleware.go
- client.go
- handler_single.go
- middleware_single.go
- idl_client.go
- hertz_client.go
以上这些模板是工具运行最基础的模板,在自定义模板的时候:
- 如果指定了同名模板会覆盖掉默认的内容
- 如果没指定同名模板会使用默认的模板
因此,大家在自定义模板的时候需要根据自己的实际情况来考虑是否需要覆盖掉这些模板
添加一个新的模板
考虑到大家有时可能需要针对 IDL 的某些信息新增自己的一些实现,例如为每个生成的 handler 加一下单测等需求。因此,hz 的模板里允许用户自定义新的模板,并提供模板的渲染参数数据源。
模板形式:
- path: biz/Fgy/{{$HandlerName}}.go // 路径 + 文件名,支持渲染数据
loop_method: bool // 是否按照 idl 中定义的 method 生成多个文件,配合 path 渲染使用
loop_service: bool // 是否按照 idl 中定义的 service 生成多个文件,配合 path 渲染使用
update_behavior: // 在使用 hz update 的时候对于该文件的更新行为
type: string // 更新行为:skip/cover/append
append_key: "method"/"service" // 在 append 行为的时候,指定追加的渲染数据源,method/service
insert_key: string // 在 append 行为的时候追加逻辑的“key”,根据这个 key 判断是否需要进行追加
append_content_tpl: string // 在 append 行为的时候,指定追加内容的模板
import_tpl: []string // 要新增的 import 的模板
body: string // 生成文件的模板内容
模板数据源
- 文件路径渲染:在指定文件路径的时候可使用如下渲染数据
type FilePathRenderInfo struct {
MasterIDLName string // master IDL name
GenPackage string // master IDL generate code package
HandlerDir string // handler generate dir
ModelDir string // model generate dir
RouterDir string // router generate dir
ProjectDir string // projectDir
GoModule string // go module
ServiceName string // service name, changed as services are traversed
MethodName string // method name, changed as methods are traversed
HandlerGenPath string // "api.gen_path" value
}
- 单个文件的渲染数据:在单独定义一个文件时使用的渲染数据,可根据 “IDLPackageRenderInfo” 的定义解出所有 IDL 的信息
type CustomizedFileForIDL struct {
*IDLPackageRenderInfo
FilePath string
FilePackage string
}
- Method 级别的渲染数据:当指定"loop_method"时,会使用到的渲染数据,会以每个 method 为单位生成一个文件
type CustomizedFileForMethod struct {
*HttpMethod // 每个 method 定义的解析出来的具体信息
FilePath string // 当循环生成 method 文件时,该文件路径
FilePackage string // 当循环生成 method 文件时,该文件的 go package 名
ServiceInfo *Service // 该 method 所属的 service 定义的信息
}
type HttpMethod struct {
Name string
HTTPMethod string
Comment string
RequestTypeName string
ReturnTypeName string
Path string // 请求路由
Serializer string
OutputDir string
Models map[string]*model.Model
}
- Service 级别的渲染数据:当指定"loop_service"时,会使用到的渲染数据,会以每个 service 为单位生成一个文件
type CustomizedFileForService struct {
*Service // 该 service 的具体信息,包括 service 名字,servide 内定义的 method 的信息等
FilePath string // 当循环生成 service 文件时,该文件路径
FilePackage string // 当循环生成 service 文件时,该文件的 go package 名
IDLPackageInfo *IDLPackageRenderInfo // 该 service 所属的 IDL 定义的信息
}
type Service struct {
Name string
Methods []*HttpMethod
ClientMethods []*ClientMethod
Models []*model.Model // all dependency models
BaseDomain string // base domain for client code
}
下面给出一个简单的自定义 handler 模板的示例:
example
example:https://github.com/cloudwego/hertz-examples/tree/main/hz/template
-
修改默认 handler 的内容
-
为 handler 新增一个单测文件
layouts:
- path: handler.go
body: |-
{{$OutDirs := GetUniqueHandlerOutDir .Methods}}
package {{.PackageName}}
import (
"context"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/protocol/consts"
{{- range $k, $v := .Imports}}
{{$k}} "{{$v.Package}}"
{{- end}}
{{- range $_, $OutDir := $OutDirs}}
{{if eq $OutDir "" -}}
"{{$.ProjPackage}}/biz/service"
{{- else -}}
"{{$.ProjPackage}}/biz/service/{{$OutDir}}"
{{- end -}}
{{- end}}
"{{$.ProjPackage}}/biz/utils"
)
{{range $_, $MethodInfo := .Methods}}
{{$MethodInfo.Comment}}
func {{$MethodInfo.Name}}(ctx context.Context, c *app.RequestContext) {
var err error
{{if ne $MethodInfo.RequestTypeName "" -}}
var req {{$MethodInfo.RequestTypeName}}
err = c.BindAndValidate(&req)
if err != nil {
utils.SendErrResponse(ctx, c, consts.StatusOK, err)
return
}
{{end}}
{{if eq $MethodInfo.OutputDir "" -}}
resp,err := service.New{{$MethodInfo.Name}}Service(ctx, c).Run(&req)
if err != nil {
utils.SendErrResponse(ctx, c, consts.StatusOK, err)
return
}
{{else}}
resp,err := {{$MethodInfo.OutputDir}}.New{{$MethodInfo.Name}}Service(ctx, c).Run(&req)
if err != nil {
utils.SendErrResponse(ctx, c, consts.StatusOK, err)
return
}
{{end}}
utils.SendSuccessResponse(ctx, c, consts.StatusOK, resp)
}
{{end}}
update_behavior:
import_tpl:
- |-
{{$OutDirs := GetUniqueHandlerOutDir .Methods}}
{{- range $_, $OutDir := $OutDirs}}
{{if eq $OutDir "" -}}
"{{$.ProjPackage}}/biz/service"
{{- else -}}
"{{$.ProjPackage}}/biz/service/{{$OutDir}}"
{{end}}
{{- end}}
- path: handler_single.go
body: |+
{{.Comment}}
func {{.Name}}(ctx context.Context, c *app.RequestContext) {
var err error
{{if ne .RequestTypeName "" -}}
var req {{.RequestTypeName}}
err = c.BindAndValidate(&req)
if err != nil {
utils.SendErrResponse(ctx, c, consts.StatusOK, err)
return
}
{{end}}
{{if eq .OutputDir "" -}}
resp,err := service.New{{.Name}}Service(ctx, c).Run(&req)
{{else}}
resp,err := {{.OutputDir}}.New{{.Name}}Service(ctx, c).Run(&req)
{{end}}
if err != nil {
utils.SendErrResponse(ctx, c, consts.StatusOK, err)
return
}
utils.SendSuccessResponse(ctx, c, consts.StatusOK, resp)
}=
- path: "{{.HandlerDir}}/{{.GenPackage}}/{{ToSnakeCase .ServiceName}}_test.go"
loop_service: true
update_behavior:
type: "append"
append_key: "method"
insert_key: "Test{{$.Name}}"
append_tpl: |-
func Test{{.Name}}(t *testing.T) {
h := server.Default()
h.GET("{{.Path}}", {{.Name}})
w := ut.PerformRequest(h.Engine, "{{.HTTPMethod}}", "{{.Path}}", &ut.Body{Body: bytes.NewBufferString(""), Len: 1},
ut.Header{})
resp := w.Result()
assert.DeepEqual(t, 201, resp.StatusCode())
assert.DeepEqual(t, "", string(resp.Body()))
// todo edit your unit test.
}
body: |-
package {{.FilePackage}}
import (
"bytes"
"testing"
"github.com/cloudwego/hertz/pkg/app/server"
"github.com/cloudwego/hertz/pkg/common/test/assert"
"github.com/cloudwego/hertz/pkg/common/ut"
)
{{range $_, $MethodInfo := $.Methods}}
func Test{{$MethodInfo.Name}}(t *testing.T) {
h := server.Default()
h.GET("{{$MethodInfo.Path}}", {{$MethodInfo.Name}})
w := ut.PerformRequest(h.Engine, "{{$MethodInfo.HTTPMethod}}", "{{$MethodInfo.Path}}", &ut.Body{Body: bytes.NewBufferString(""), Len: 1},
ut.Header{})
resp := w.Result()
assert.DeepEqual(t, 201, resp.StatusCode())
assert.DeepEqual(t, "", string(resp.Body()))
// todo edit your unit test.
}
{{end}}
MVC 模板实践
Hertz 提供了 一个 MVC 自定义模版的最佳实践,代码详见 code 。
注意事项
使用 layout 模板的注意事项
当用户使用了 layout 自定义模板后,那么生成的 layout 和渲染数据都由用户接管,所以用户需要提供其定义的 layout 的渲染数据。
使用 package 模板的注意事项
一般来说,用户使用 package 模板的时候大多数是为了修改默认的 handler 模板;不过,目前 hz 没有提供单个 handler 的模板,所以当 update 已经存在的 handler 文件时,会使用默认 handler 模板在 handler 文件尾追加新的 handler function。当对应的 handler 文件不存在的时候,才会使用自定义模板来生成 handler 文件。