hz 自定义模板使用

hz 自定义模板使用。

Hertz 提供的命令行工具 (以下称为"hz") 支持自定义模板功能,包括:

  • 自定义 layout 模板 (即生成代码的目录结构,这些结构与具体的 idl 定义无关,不需要 idl 也可以直接生成)
  • 自定义 package 模板 (即与 idl 定义相关的代码结构,包括 handler、model、router 等)

hz 生成代码的结构 中的代码结构为例(集成 hz 和 hz client 生成的代码),介绍 hz 提供的默认模板:

.
├── biz                                
│   ├── handler                          // biz/handler 为默认 handler_dir,可通过 --handler_dir 修改
│   │   ├── hello                        // handler 相关代码,与 idl 有关,package 模板生成
│   │   │   └── example
│   │   │       └── hello_service.go 
│   │   └── ping.go                      // layout 模板生成
│   ├── model                            // biz/model 为默认 model_dir,可通过 --model_dir 修改
│   │   └── hello                      
│   │       └── example
│   │           └── hello.go             // 由 hz 调用 thriftgo 生成,不涉及 layout 模板和 package 模板
│   │           └── hello_service        // 调用 hz client 命令得到,与 idl 有关,package 模板生成
│   │               └── hello_service.go
│   │               └── hertz_client.go
│   └── router                           // biz/router 为默认 router_dir,可通过 --router_dir 修改
│       ├── hello                        // router 相关代码,与 idl 有关,package 模板生成
│       │   └── example
│       │       ├── hello.go           
│       │       └── middleware.go      
│       └── register.go                  // 未指定 idl 时,由 layout 模板生成;指定 idl 时,由 package 模板生成
├── go.mod                               // go.mod 文件,layout 模板生成
├── main.go                              // 程序入口,layout 模板生成
├── router.go                            // 用户自定义除 idl 外的路由方法,layout 模板生成
└── router_gen.go                        // hz 生成的路由注册代码,用于调用用户自定义的路由以及 hz 生成的路由,layout 模板生成
├── .hz                                
├── build.sh                             // 程序编译脚本,layout 模板生成
├── script                                
│   └── bootstrap.sh                     // 程序运行脚本,layout 模板生成
└── .gitignore                           // layout 模板生成

用户可自己提供模板以及渲染参数,并结合 hz 的能力,来完成自定义的代码生成结构。

layout 模板

hz 利用了 go template 支持以 “yaml” 的格式定义 layout 模板,并使用 “json” 定义模板渲染数据。

用户可根据默认模板来修改或重写,从而满足自身需求。

注意:当在命令行中未指定自定义模板渲染文件时,hz 会使用 默认渲染参数 渲染自定义 layout 模板,此时应保证自定义 layout 模板的渲染参数在 默认渲染参数 的范围内。

默认 layout 模板

注:以下的 body 均为 go template

layouts:
  # 生成的 dal 的目录,只有目录下有文件才会生成
  - path: biz/dal/
    delims:
      - ""
      - ""
    body: ""
  # 生成的 handler 的目录,只有目录下有文件才会生成
  - path: biz/handler/
    delims:
      - ""
      - ""
    body: ""
  # 生成的 model 的目录,只有目录下有文件才会生成
  - path: biz/model/
    delims:
      - ""
      - ""
    body: ""
  # 生成的 service 的目录,只有目录下有文件才会生成
  - path: biz/service/
    delims:
      - ""
      - ""
    body: ""  
  # 项目 main.go 文件
  - 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}} {{.UseApacheThrift}} 才能生成
  - 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
    \ "
  # handler 中的 ping.go 文件,需要模板渲染数据 {{.HandlerPkg}} 才能生成
  - path: biz/handler/ping.go
    delims:
      - ""
      - ""
    body: |-
      // Code generated by hertz generator.

      package {{.HandlerPkg}}

      import (
      	"context"

      	"github.com/cloudwego/hertz/pkg/app"
      	"github.com/cloudwego/hertz/pkg/common/utils"
        "github.com/cloudwego/hertz/pkg/protocol/consts"
      )

      // Ping .
      func Ping(ctx context.Context, c *app.RequestContext) {
      	c.JSON(consts.StatusOK, 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)
      }      
  # 自定义路由注册的文件,需要模板渲染数据 {{.HandlerPkgPath}} 才能生成
  - 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 ...
      }      
  # 默认路由注册文件,不要修改,需要模板渲染数据 {{.RouterPkg}} 才能生成
  - path: biz/router/register.go
    delims:
      - ""
      - ""
    body: |-
      // Code generated by hertz generator. DO NOT EDIT.

      package {{.RouterPkg}}

      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!
      }      
  # 编译脚本,需要模板渲染数据 {{.ServiceName}} 才能生成
  - path: build.sh
    delims:
      - ""
      - ""
    body: |-
      #!/bin/bash
      RUN_NAME={{.ServiceName}}
      mkdir -p output/bin
      cp script/* output 2>/dev/null
      chmod +x output/bootstrap.sh
      go build -o output/bin/${RUN_NAME}      
  # 运行脚本,需要模板渲染数据 {{.ServiceName}} 才能生成
  - path: script/bootstrap.sh
    delims:
      - ""
      - ""
    body: |-
      #!/bin/bash
      CURDIR=$(cd $(dirname $0); pwd)
      BinaryName={{.ServiceName}}
      echo "$CURDIR/bin/${BinaryName}"
      exec $CURDIR/bin/${BinaryName}       

模板渲染参数文件

hz 使用了 “json” 来指定渲染数据,包括全局的渲染参数和各个文件的渲染参数。

全局渲染参数在各个文件中都能使用,文件渲染参数只能用于所属文件。

全局渲染参数

全局渲染参数的 key 为 “*",hz 默认提供了如下五个全局渲染参数:

渲染参数名 默认值 说明
GoModule - go module,可通过 –module 指定
ServiceName hertz_service 服务名,可通过 –service 指定
UseApacheThrift - idl 为 thrift 时为 true,否则为 false
HandlerPkg handler handler_dir 的最后一级路径,可通过 –handler_dir 修改
RouterPkg router router_dir 的最后一级路径,可通过 –router_dir 修改

注意:除 UseApacheThrift 外,其它参数都可以通过命令行指定,此时若在自定义渲染参数文件中也指定了该参数,应保证两处参数的值一致,否则可能会出现问题。因此我们建议,在使用自定义模板时 ServiceName, HandlerPkg, RouterPkg 不需要在命令行中指定,在渲染参数文件中指出即可,GOPATH 外指定 go mod 时应保证两处的一致性。

用户可以根据需求自定义全局渲染参数名和值,但需保证 key 为 “*"。

文件渲染参数

hz 默认提供了如下文件渲染参数:

{
  "router_gen.go": {
    "RouterPkgPath": "{GoModule}/biz/router",
    "HandlerPkgPath": "{GoModule}/biz/handler"
  },

  "router.go": {
    "RouterPkgPath": "{GoModule}/biz/router",
    "HandlerPkgPath": "{GoModule}/biz/handler"
  }
}

文件渲染参数仅作用于所属文件,key 为文件名(基于 out_dir 的相对路径),值可任意定义。

自定义一个 layout 模板

目前,hz 生成的项目 layout 已经是一个 hertz 项目最最最基础的骨架了,所以不建议删除现有的模板里的文件。

不过如果用户想要一个别的 layout,当然也可以根据自身需求来删除相应的文件 (除"biz/register.go"外,其余都可以动)

我们十分欢迎用户来贡献自己的模板。

下面假设用户只想要 “main.go” 以及 “go.mod” 文件,那么我们对默认模板进行修改,如下:

template

// layout.yaml
layouts:
  # 项目 main.go 文件
  - 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 模板

package 模板与 idl 定义相关,包括 handler、model、router 等。

用户可根据默认模板来修改或重写,从而满足自身需求。

默认 package 模板

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"
        "github.com/cloudwego/hertz/pkg/protocol/consts"

      {{- 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(consts.StatusBadRequest, err.Error())
      		return
      	}
      	{{end}}
      	resp := new({{$MethodInfo.ReturnTypeName}})

      	c.{{.Serializer}}(consts.StatusOK, resp)
      }
      {{end}}      
  # path 只表示 router.go 的模板,具体的路径由默认路径和 router_dir 决定
  - 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 的模板,具体的路径由默认路径和 router_dir 决定
  - path: register.go
    delims:
      - ""
      - ""
    body: |-
      // Code generated by hertz generator. DO NOT EDIT.

      package {{.PackageName}}

      import (
      	"github.com/cloudwego/hertz/pkg/app/server"
      	{{$.DepPkgAlias}} "{{$.DepPkg}}"
      )

      // GeneratedRegister registers routers generated by IDL.
      func GeneratedRegister(r *server.Hertz){
      	//INSERT_POINT: DO NOT DELETE THIS LINE!
      	{{$.DepPkgAlias}}.Register(r)
      }      
  - path: model.go
    delims:
      - ""
      - ""
    body: ""
  # path 只表示 middleware.go 的模板,middleware 的路径和 router.go 相同
  - 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 的模板,仅当使用 hz new --client_dir 或 hz update --client_dir 时生成,路径由 out_dir 和 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(consts.StatusBadRequest, err.Error())
      		return
      	}
      	{{end}}
      	resp := new({{.ReturnTypeName}})

      	c.{{.Serializer}}(consts.StatusOK, resp)
      }      
  # middleware_single 表示单独的 middleware 模板,用于 update 的时候更新每一个新增的 middleware_single
  - path: middleware_single.go
    delims:
      - '{{'
      - '}}'
    body: |+
      func {{.MiddleWare}}Mw() []app.HandlerFunc {
      	// your code...
      	return nil
      }      
  # hertz_client 由 hz client 命令生成,详细代码请参考 https://github.com/cloudwego/hertz/blob/develop/cmd/hz/generator/package_tpl.go#L271
  - path: hertz_client.go
    delims:
      - '{{'
      - '}}'
  # idl_client 由 hz client 命令生成,详细代码请参考 https://github.com/cloudwego/hertz/blob/develop/cmd/hz/generator/package_tpl.go#L862
  - path: idl_client.go
    delims:
      - '{{'
      - '}}'

模板渲染参数

注意:与 layout 模板不同,自定义 package 模板没有提供渲染数据的功能,这里主要是因为这些渲染数据是 hz 工具解析生成的,所以暂时不提供自己写渲染数据的功能。可以修改下模板里面与渲染数据无关的部分,以满足自身需求。

下面介绍 hz 默认提供的模板渲染参数。

  • 文件路径渲染:在指定文件路径的时候使用如下渲染数据

    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
    }
    

自定义一个 package 模板

与 layout 模板一样,用户同样可以自定义 package 模板。

就 package 提供的模板来说,一般用户可能只有自定义 handler.go 模板的需求,因为 router.go/middleware.go/register.go 一般与 idl 定义相关而用户无需关心,一般也无需修改。

因此,用户可根据自身的需求来自定义生成的 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

以上这些模板是工具运行最基础的模板,在自定义模板的时候:

  • 如果指定了同名模板会覆盖掉默认的内容
  • 如果没指定同名模板会使用默认的模板

因此,大家在自定义模板的时候需要根据自己的实际情况来考虑是否需要覆盖掉这些模板。

注意:用户在自定义模板时若要覆盖上述模板只需指出文件名,无需给出相对路径(如 handler.go),但新增自己的实现时需要给出基于 out_dir 的相对路径。

添加一个新的模板

考虑到大家有时可能需要针对 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: string // 在 append 行为的时候,指定追加的渲染数据源,method/service
      insert_key: string // 在 append 行为的时候追加逻辑的“key”,根据这个 key 判断是否需要进行追加
      append_content_tpl: string // 在 append 行为的时候,指定追加内容的模板
      import_tpl: []string // 要新增的 import 的模板
    body: string // 生成文件的模板内容

下面给出一个简单的自定义 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.{{.HTTPMethod}}("{{.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.{{$MethodInfo.HTTPMethod}}("{{$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

注意事项

使用 package 模板的注意事项

一般来说,用户使用 package 模板的时候大多数是为了修改默认的 handler 模板;不过,目前 hz 没有提供单个 handler 的模板,所以当 update 已经存在的 handler 文件时,会使用默认 handler_single 模板在 handler 文件尾追加新的 handler function。当对应的 handler 文件不存在的时候,才会使用自定义模板来生成 handler 文件。


最后修改 December 8, 2023 : docs(en): add volo guide and faq (#883) (42f8d6a)