Vscode niuhe(牛河/河粉) 插件是一款面向 golang
的后端 IDL 代码生成(翻译)插件, 旨在实现前后端一致的 API设计, 开发和测试一体化协作开发流程, 提高开发效率. 它通过简单的 niuhe idl 语法 定义接口, 支持生成对应的 go 服务代码, 前端 api 定义, swagger 文档, 以及其他语言的协议.
niuhe IDl
是一种类似于 Python 的对象接口定义语言(Interface Definition Language, IDL
),语法简洁,开发者易上手。
插件借助对 niuhe IDl
的支持与拓展,开发者只需定义一次接口就能多处复用,避免前后端重复定义,接口更新也无需繁琐通知。此外,插件还能自动生成 CURD 相关前后端代码,前端代码完美适配管理后台模板, 轻松实现表定义, 数据增删改查和展示等基本功能。
niuhe 插件通过一套接口定义生成多套一致代码,可减少重复劳动,提升开发效率,提升项目的一致性和可维护性,让团队聚焦核心业务,非常适合中小开发团队使用。
核心功能
功能模块 | 核心能力 | 适用场景 |
---|---|---|
Go语言支持 | 自动生成服务框架代码:包含路由管理niuhe、请求参数解析zpform、响应格式处理等基础代码niuhe。niuhe 和 zpform 两个库是本插件的基石 | 后端开发 |
TypeScript支持 | 一键生成前端API调用代码,支持Web/小程序/React Native等场景 | 前端开发 |
文档生成 | 自动生成Swagger文档,支持导入Postman/Apifox等测试工具 【示例】, 接入 MCP 等 | 接口测试 |
多语言协议 | 生成标准化协议文件,支持自定义转换到其他编程语言 | 前端开发 |
Go服务核心特性
功能 | 优势 |
---|---|
智能路由 | 自动根据文件结构生成路由配置,无需手动维护 |
参数处理 | 内置请求参数校验和响应格式化,专注业务逻辑开发 |
XORM集成 | 自动生成数据库操作代码(表结构定义、DAO层、服务层) |
CURD操作 | 自动生成CURD API逻辑代码 |
常量管理 | 统一管理业务常量,提升代码可维护性 |
配套解决方案
开提供开箱即用的管理系统模板,加速企业级应用开发:
解决方案 | 亮点 |
---|---|
Admin-Core | 内置RBAC权限体系,快速实现用户/角色/菜单管理 |
Vue3管理模板 | 基于流行技术栈(Vue3+Element Plus),提供完整后台功能组件, 同时支持代码前端页面代码生成 |
实践案例
- admin-core-niuhe 完整示例项目(后端)
- admin-core-test 前端框架接入示例
- 在线演示 Vue3管理后台演示(账号: admin / 123456)
- niuhe插件生成文档导入 APIFOX 示例
快速开始
本章节将介绍如何安装与激活插件,实现一个 Hellow World 并介绍 niuhe 语法
本教程示例代码库: https://github.com/ma-guo/niuhe-mdbook
1. 安装与激活
- VSCode 插件市场搜索
niuhe
- 点击安装后,可通过下列四种方式激活插件生成代码
- 方式1: 点击资源管理器顶部出现的
图标
- 方式2: 点击工具栏中的
图标
- 方式3: 在资源管理器中选中任意文件并右键中
niuhe idl 生成
- 方式4:
Command + Shift + P
快捷键中 输入niuhe
查找niuhe idl 生成
- 方式1: 点击资源管理器顶部出现的
方式1和3 | 方式 2 | 方式4 |
---|---|---|
上图示例是旧版图标
</>
, 新版本图标已经更换为
2. Hello World 实战
本教程代码库为 niuhe-mdbook
在项目根目录创建:
#app=demo
class HelloReq():
'''测试请求'''
name = required.String(desc='用户名')
class HelloResp(Message):
'''测试响应'''
greeting = required.String(desc='问候语')
with services():
GET('示例接口', '/api/hello/world/', HelloReq, HelloResp)
点击 生成代码后,将自动创建如下文件:
.
├── README.md // 项目说明
├── conf
│ └── demo.yaml // 配置文件
├── makefile // linux/unix makefile 脚本
├── niuhe
│ └── all.niuhe // 项目入口文件
├── niuhe.log
└── src
└── demo
├── app
│ ├── api
│ │ ├── protos
│ │ │ └── gen_protos.go // 自定生成的 go 请求和响应结构定义文件
│ │ └── views
│ │ ├── gen_hello_views.go // 自定义生成的 go 路由处理器文件
│ │ ├── hello_views.go // hello 香港 view 的实现文件
│ │ └── init.go
│ └── common
│ └── consts
│ └── gen_consts.go // 常量定义文件
├── config
│ └── config.go // 配置文件定义, 读取 conf/demo.yaml 内容
├── go.mod
├── main.go // 项目入口
└── xorm
├── daos
│ └── init.go // dao 层基本定义定义
├── models
│ └── models.go // model 层定义示例
└── services
└── init.go // service 层基本定义
接下来我们跟随 README.md 指引,执行下列命令运行项目
cd src/demo && go mod init demo && go mod tidy && go mod vendor && cd ../../ && make run
此时我们在浏览器访问链接: http://localhost:19999/api/hello/world/, 即可看到返回的问候语。
{"message":"Name(必填)","result":-1}
由此我们完成了一个简单的 Hello World
示例。
niuhe 语法
niuhe idl
简称 niuhe
, 是智品网络后端团队开发的一门接口定义语言。
语法主要包括 注释
, 常量(枚举)
, 数据类型(class/struct)
, 接口定义(路由)
, 数据字段定义
五个部分。
niuhe 语法生成代码示意图
插件自定义了
.niuhe
后缀的代码高亮、代码提示和补全,也可选择python
来做语法高亮
应用名称
应用名称需定义在入口文件 niuhe/all.niuhe
中定义,使用 #app=应用名
来定义。
#app=demo
注释
注释同 python
相同, 单行注释使用 #
, 多行注释使用三个引号 '''
, 类和常量的说明用注释的方式写在类定义的下一行。
当多行注释出现在 class
后时注释将作为 class
的说明
常量(枚举)
class Language(ConstGroup):
'''语言枚举类'''
ZH = Item("zh", "中文")
EN = Item("en", "英文")
class LanguageType(ConstGroup):
'''语言类型枚举'''
ZH_CN = Item(1, "简体中文")
ZH_TW = Item(2, "繁体中文")
# 下面为新语法定义 ConstGroup
# 新语法中支持通过属性名指定属性值, 支持 value, desc 和 depr 三个属性, 可混用
# depr 可出现在字段和类上, 表示该字段或类已废弃
class LanguageType(ConstGroup, depr=True):
'''语言类型枚举'''
ZH_CN = Item(value=1, desc="简体中文")
ZH_TW = Item(2, desc="繁体中文")
EN = Item(3, desc="英文", depr=True)
FRANCE = Item(4, "法语", depr=True)
常量定义以 class
开头, 继承 ConstGroup
, 内部通过 Item
来定义具体的常量值。常量仅支持 String
和 Integer
类型,且必须指定一个唯一标识符和一个显示名称。上述定义生成后的代码(src/demo/app/common/consts/gen_consts.go
)为
package consts
// Generated by niuhe.idl
import "github.com/ma-guo/niuhe"
var Language struct {
*niuhe.StringConstGroup
ZH niuhe.StringConstItem `name:"中文" value:"zh"` // value: zh, name: 中文
EN niuhe.StringConstItem `name:"英文" value:"en"` // value: en, name: 英文
}
// 语言类型枚举
var LanguageType struct {
*niuhe.IntConstGroup
ZH_CN niuhe.IntConstItem `name:"简体中文" value:"1"` // value: 1, name: 简体中文
ZH_TW niuhe.IntConstItem `name:"繁体中文" value:"2"` // value: 2, name: 繁体中文
}
func init() {
niuhe.InitConstGroup(&Language)
niuhe.InitIntConstGroup(&LanguageType)
}
数据类型(class/struct)
在上一节 hello world 实战中,我们定义了一个接口的入参和出参类
class HelloReq():
'''测试请求'''
name = required.String(desc='用户名')
class HelloRsp(Message):
'''测试响应'''
greeting = required.String(desc='问候语')
# 可在 class 中通过 desc 和 depr 来定义类的描述和废弃状态
class HelloReq(depr=True, desc='测试请求', ver='1.1', used=True):
name = required.String(desc='用户名')
数据类型定义以 class
开头, 继承 Message
, Message
可不写, 类后跟随以注释形式的类说明(可选)和字段定义。
当类无字段时以 pass
做标记即可,可如下:
class HelloReq():
'''测试请求, 无参数'''
pass
上述定义生成的 struct
代码为:
package protos
// Generated by niuhe.idl
// 此文件由 niuhe.idl 自动生成, 请勿手动修改
// 测试请求
type HelloReq struct {
Name string `json:"name" zpf_name:"name" zpf_reqd:"true"` // 用户名
}
// 测试响应
type HelloRsp struct {
Greeting string `json:"greeting" zpf_name:"greeting" zpf_reqd:"true"` // 问候语
}
类的继承
类继承自Message
可以继承自自定义的其他类, 通过在类名后添加括号的方式实现:
class NihaoReq(HelloReq):
'''你好请求'''
mingzi = required.String(desc='名字')
生成的 struct 代码为:
// 你好请求
type NihaoReq struct {
Name string `json:"name" zpf_name:"name" zpf_reqd:"true"` // 用户名
Mingzi string `json:"mingzi" zpf_name:"mingzi" zpf_reqd:"true"` // 名字
}
字段定义
字段定于语法为:
member_name = label.type(desc='', cls=..., group=..., ...)
其中
label
可以是required
(必填的),optional
(可选的),repeated
(重复的/数组) 三种type
可以是Integer
,Decimal
,Float
,Long
,String
,Boolean
,Message
,Enum
,StringEnum
,File
,Any
11种数据类型。
这里
required
和optional
仅对入参有意义, 出参在go
中无此限制。repeated
在入参中同optional
字段可选属性有 desc
, group
, cls
, depr
, ver
, demo
, json
,omit
, value
, reg
, minlen
, maxlen
, minnum
, maxnum
, format
十五个属性。它们的说明如下
desc
: 描述信息, 可选, 建议填写字段说明, 否则失去文档定义语言意义。group
: 指定enum
的group
, 仅当type
为Enum
和StringEnum
时有效和必填, 如lang=required.Enum(desc='语言枚举', group=LanguageType)
cls
: 指定类的类型,仅当type
为Message
时有效和必填,如message=optional.Message(desc='消息体',cls=HelloReq)
depr
:depr=True
/depr=true
,将字段标记为deprecated
, 这在docs
文档中有体现, 如导入到 apifox 中时字段将被标记为废弃(删除线), 便于同前端进行沟通.ver
: 指定字段的版本号, 如ver='1.0.0
。新加字段时建议标记,同depr
一样,主要用于前后端沟通demo
: 指定字段的示例值, 如demo=1
, 这里赋予demo
值的类型要同type
字段标记的一致json
: 修改字段对应的 json 名, 如json='name'
, 默认为空.omit
: 指定字段为omitempty
, 如omit=True
; 通常情况下,结构体中的零值字段(如int的0,string的"",bool的false等)会被序列化为JSON,即使它们没有被明确地设置。如果你不希望包含这些零值字段,可指定字段的 omit.value
: 定义ConstGroup
时指定其值, 其他类型中无实际意义reg
:type
为String
时验证其正则表达式minlen
:type
为String
时验证其最小长度; 如minlen=1
maxlen
:type
为String
时验证其最大长度; 如maxlen=10
minnum
:type
数字时验证其最小值; 如minnum=1
maxnum
:type
数字时验证其最大值; 如maxnum=10
format
: 用于指定日期格式, 如format="2006-01-02"
实际上记住
desc
,cls
,group
三个即可,其他的 12 个是本插件拓展属性,不是高频使用属性
attrs
中reg
,minlen
,maxlen
,minnum
,maxnum
的属性请参考 zpform struct 定义
接口定义(路由)
接口定义以 with services():
为开始标记, 后面跟随一个或多个路由定义,每个路由定义如下:
http_method('接口说明', 'api_path(/mode/view/method/)', 入参类名(可选), 出参类名(可选)[, author='xxx', status=tested])
其中
http_method
可以是GET
,POST
,PUT
,DELETE
,PATCH
,HEAD
,OPTIONS
等 http method接口说明
: 接口的简要描述, 如'获取用户信息'
api_path
: 必须是三段式 api 的路径, 如/api/hello/world/
入参类名
: 可选, 如HelloReq
当不填写时,得自己处理入参和出参的解析, 入参和出参得同时可选或同时必填。出参类名
: 可选, 如HelloRsp
当不填写时,得自己处理入参和出参的解析, 入参和出参得同时可选或同时必填。author
apifox 支持, 指定接口作者/责任人, 字符串status
apifox 支持, 指定 API状态, 可选值designing|pending|developing|integrating|testing|tested|released|deprecated:exception:obsolete
本章节第二小节定义的路由如下:
with services():
GET('示例接口', '/api/hello/world/', NihaoReq, HelloRsp)
生成的代码在 src/demo/app/api/views/[gen_]hello_views.go
文件中。
[]
表示可选, 下同
RPC 方式定义接口
在定义接口时,可以通过 RPC
关键字来定义一个接口。语法如下:
RPC('接口说明', [http_method/url=]'api_path(/mode/view/method/)', [author='xxx', status=tested])[.args(...)][.returns(...)][.codes(...)]
其中:
http_method/url=
: 可选,指定 http 方法和路径,如get='/api/hello/world/'
,url='/api/hello/world/'
或者直接写'/api/hello/world/'
, 此时http_method
为GET
和POST
,http_method
可选值为GET
,POST
,PUT
,DELETE
,PATCH
,HEAD
,OPTIONS
, 因此RPC
方式中, 请求方法有九种定义方式。.args(...)
: 可选,指定接口的入参,如.args(name=required.String(desc='用户名'),...)
.returns(...)
: 可选,指定接口的出参,如.returns(greeting=required.String(desc='问候语'),...)
.codes(...)
: 可选,指定接口的状态码和描述, 可用于约束 api 可返回的错误状态码范围,如.codes(LanguageType.ZH_CN, LanguageType.ZH_TW, Item(100, '错误描述'))
,codes
中为定义的Integer
枚举值。服务端返回值检测请参考 错误代码定义和检测author
apifox 支持, 指定接口作者/责任人, 字符串status
apifox 支持, 指定 API状态, 可选值designing|pending|developing|integrating|testing|tested|released|deprecated:exception:obsolete
RPC
方式同GET
,POST
的差异是不用直接定义一个class
, 在一个文件中定义过多class
时不便于一目了然地查阅接口的入参和出参。同时
RPC
方式支持只定义入参和出参的情况, 此时会生成一个空结构来填补另一个未定义的参数结构
include 引用
在定义类时,可以通过 include
关键字引用其他文件中的类.
如在 comm.niuhe 文件中定义了如下内容:
# 公共类型定义
class NoneResp():
'''空响应'''
pass
class NoneReq():
'''空请求'''
pass
此时可在 *.niuhe
文件中通过 include
引入:
include('comm.niuhe')
# 然后通过 comm.NoneReq, comm.NoneResp 来使用这些类
with services():
GET('include 引用示例', '/api/include/demo/', comm.NoneReq, comm.NoneResp)
include
支持跨文件引用其他目录中的*.niuhe
文件
其他说明
入参参数中, 并不支持解析复杂结构, 如 Message
, Any
, Dict
等. 而出参则无此限制, 可以自由定义返回的 json
数据格式。
niuhe idl
中的更多关键字请参阅 niuhe 关键字
入门指南
本章节将介绍:
- 实现 Hello World,
- 如何生成 Swagger 文档和使用 Swagger 文档, 可导入 apifox, postman 等测试工具
- MCP接入, 提升 AI 对文档的理解能力
- 以及生成 TypeScript api, interface 定义。 可用于小程序, React, 等前端项目支持
Hello World
在第二部分第二节种们通过定义 niuhe/all.niuhe
实现了一个简单的 Hello World
示例。本节我们将介绍如何实现它。
我们将 src/demo/app/api/views/hello_views.go
中 World_GET
方法修改为如下:
func (v *Hello) World_GET(c *niuhe.Context, req *protos.HelloReq, rsp *protos.HelloRsp) error {
rsp.Greeting = "Hello World " + req.Name
return nil
}
重新运行程序并访问如下链接 http://localhost:19999/api/hello/world/?name=Tom
,即可看到返回的问候语。
{
"data": {
"greeting": "Hello World Tom"
},
"result": 0
}
这里 github.com/ma-guo/niuhe
包提供了一些基础的功能,如上下文管理、请求参数解析等和返回结构包装。我们只关心具体业务逻辑即可。
插件生成 view 文件时会生成一个
views/XX.go
和views/gen_XX.go
, gen 开头的文件在每次生成代码时都会覆写, 请不要在其中修改代码。而XX.go
则在文件存在时跳过写内容操作。因此添加新的 api 定义时, 只需从
gen_XX.go
复制新添加的方法定义到XX.go
中并去掉gen_
前缀即可。
生成 Swagger 文档
Swagger
文档是一种以特定格式(如 JSON
或 YAML
)编写的文件,用于对 RESTful API 进行详细的描述和说明。它提供了一种标准化的方式来定义 API 的端点、请求和响应的结构、参数、身份验证机制等信息,使得开发人员、测试人员、文档编写者以及其他相关人员能够全面了解 API 的功能和使用方法,从而更高效地进行开发、集成和测试等工作。
在 niuhe/.config.json5
文件 langs
配置中添加 docs
即可在生成文件时生成协议的 swagger
文档。
同时支持生成 openapi 3.0.1 格式的 openapi 文档, 在
langs
中添加openapi
即可生成
生成的文档位于 docs/swagger.json
, 同时生成了 niuhe/.docs.json5
自定义文件, 可在自定义文件中定制输出内容。
生成的 docs/swagger.json
文件可直接导入到 apifox 等支持 Swagger
协议的测试工具中进行测试和使用。也可读取 niuhe/.docs.json5
生成 url 输出到网页上。
示例
假设我们有一个简单的 API,包含以下内容:
with services():
GET('协议文档', '/api/hello/docs/')
通过如下代码即可将协议文档输出到网页上:http://localhost:19999/api/hello/docs/
// 协议文档
func (v *Hello) Docs_GET(c *niuhe.Context) {
docs, err := os.ReadFile("./docs/swagger.json")
if err != nil {
niuhe.LogError("read docs file failed: %v", err)
return
}
data := make(map[string]any)
json.Unmarshal(docs, &data)
c.JSON(http.StatusOK, data)
}
线上示例:
生态后端管理系统项目 admin-core-niuhe 通过插件生成的 Swagger.json 文件,导入 Apifox 后的访问地址为:https://s.apifox.cn/aeac2eb5-398b-4bca-8ff7-3e7bfce5be79,点击即可查看接口文档。
生成 typescript api 定义
在开发前端应用时,通常需要根据后端的 API 接口定义来生成相应的 TypeScript 类型。niuhe 插件提供了便捷的方式来自动生成这些类型定义文件。以下是如何生成 TypeScript API 定义的步骤:
1. 添加 ts
语言支持
首先,在 docs/.config.json5
的 langs
中添加 ts
。生成代码后会生成如下几个文件:
typings
├── api.ts - API 接口类型定义
├── event.d.ts - 部分公共属性定义
├── request.ts - 请求方法封装
└── types.d.ts - niuhe 中定义的类型定义
2. 自定义 typescript 配置
上述生成的四个文件中 event.d.ts
和 request.ts
和 内容是不会变的, 将其复制到目标位置即可。api.ts
和 types.d.ts
需要根据实际情况进行调整。因此在 docs/.config.json5
需要根据项目需要修改 typescript
配置。 typescript
定义如下:
interface TsConfig {
app: string
modes: string[]
types: string[]
api: string[]
enumjs: string[]
optional: boolean
tsenum: boolean
}
interface EntryInfo {
app: string // 项目名
...
typescript: TsConfig[] // ts 相关配置, 在 langs 中添加 ts 时生效
...
}
参数说明:
app
生成 ts 代码中的app
名, 不定义则保持同项目同名modes
生成对应的mode
, 如有api(前端网页项目)
和admin(后台管理项目)
两个mode
, 则可根据需要最小化生成代码。types
types.d.ts
存储路径, 支持相对路径和绝对路径, 支持多个api
api.ts
存储路径, 支持相对路径和绝对路径, 支持多个enumjs
js 版枚举类型定义, 需要在入口文件中引入, 如在main.ts
中引入import './enum.js'
optional
niuhe
中定义的optional
定义的参数生成代码时否生添加?
, 默认值为false
tsenum
types.d.ts
的interface
中字段为枚举时是否声明为对应枚举类型, 默认值为false
。当前是转为int/string
引入
enumjs
配置项原因: 在types.d.ts
中定义的enum
在代码中引用时编辑器和编译器都不会报错。但是在运行时会报is undefined
错误。此时需要在window
中挂载对应枚举值即可修复, 引入此配置项即解决此问题。
其他实现方案
通过插件的 template 能力, 也可自行定义 typescript
形式, 具体参考模板库中 typescript enum、typesctipt api、typescript interface 三个例子。
niuhe 接入 MCP, 让 API 文档也智能
API 文档的未来:MCP
,让协作像聊天一样简单. MCP
是 Model Context Protocol
(模型上下文协议)的缩写,是 2024 年 11 月 Claude 的公司 Anthropic 推出并开源的一个新标准。简单来说, 它就是让 AI 助手能够连接到各种第三方数据源的桥梁 ,包括你的内容库、业务工具和开发环境等等。说白了,就是让 AI 变得更聪明、回答更准确的一种方式!
利用 apifox-mcp-server
将在线文档、Swagger/OpenAPI
文件提供给 AI 使用, 从而让 AI 智能体能够更好地理解和使用你的 API, 从而提高工作效率。
niuhe
插件生成的 swagger.json
文档,能够完美借助 apifox-mcp-server
实现 MCP
功能。这意味着开发者可以轻松让 AI 智能体更深入、精准地理解和运用 API,大幅提升工作效率,让 API 开发与协作变得更加顺畅高效。
配置 MCP 客户端
前置条件
- 已安装
Node.js
环境(版本号 >= 18,推荐最新的 LTS 版本) - 任意一个支持
MCP
的 IDE:Cursor
VSCode + Cline
插件- 其它
基本步骤
-
确保你有
docs/swagger.json
本地路径或 url 地址 -
在 IDE 中配置
MCP
将下面 JSON 配置添加到 IDE 对应的 macOS/Linux
MCP
配置文件如下:
{
"mcpServers": {
"API 文档": {
"command": "npx",
"args": [
"-y",
"apifox-mcp-server@latest",
"--oas=<oas-url-or-path>"
]
}
}
}
windows MCP 配置文件如下:
{
"mcpServers": {
"API 文档": {
"command": "cmd",
"args": [
"/c",
"npx",
"-y",
"apifox-mcp-server@latest",
"--oas=http://{your domain}/api/hello/docs/"
]
}
}
}
其中
<oas-url-or-path>
可以是:
- 远程 URL,如:
http://{your domain}/api/hello/docs/
- 本地文件路径,如:
~/Projects/docs/swagger.json
通过上述操作,你就可以在 IDE 中使用 MCP 连接到你的 API 文档了。如有其他疑问,请查阅下列参考链接或加官方支持群 咨询。
参考链接
进阶操作
本章节主要介绍一些高级功能和使用技巧,帮助你更好地利用 niuhe 进行开发。这些功能包括:
- xorm 相关, 生成数据库表, 读写和 增删改查实现代码
- 将 idl 定义内容转换为其他语言协议
以上功能可让您更聚焦核心功能开发, 拓展插件支持的编程语言, 并提升您的开发效率。
插件支持的语言
本插件中, 我们将生成不同格式代码文件特性定义为语言。
在 niuhe/.config.json5
文件中,langs
字段支持的可选项有 go
,ts
,docs
,protocol
, route
, vite
和script
7个值,其中 go
为默认支持的语言。下面对不同语言的特性进行说明
go
生成基础的 Go
代码,包括 API 接口定义, 路由管理, 基础数据库的 xorm
功能。在 niuhe
文件中进行定义后,开发时专注于具体逻辑开发即可,无需关心路由、请求参数解析等细节。
ts
参考 生成 typescript api 定义. 生成 TypeScript
types
和 api 文件内容,interface 名称,字段名,字段说明, api 路由等同 niuhe
文件一一对应。生成代码后同 niuhe
文件保持同步。写前端代码时不用再对着协议文档手敲一份,减少了不必要的重复工作,同时避免了由于手动输入导致的错误。
docs
生成 Swagger 2.0
文档,方便 API 测试软件如 apifox(推荐), postman
等使用,在测试软件中通过文件/url 导入后即可直接进行 api 测试, 不需要再手动维护协议文档。生成的文档位于 docs/swagger.json
protocol
参考 生成其他协议, 生成简明扼要(同 swagger
而言)的 json
格式的协议文档, 方便开发者拓展为其他语言。生成的文档位于 docs/protocol.json
route
生成路由文件,方便开发者对 API 进行管理。如增加 api 级权限处理等。 生成的文档位于 src{appName}/app/{mode}/protos/gen_routes.go
。在 错误代码定义和检测 章节中, 展示了route
同 codes
结合实现更丰富功能例子。
vite
参考 xorm 代码生成, 在配置 niuhe/.model.niuhe
文件后,并添加 vite
语言支持时会通过 .model.niuhe
定义生成 vue3-element-admin 项目 对应的前端 curd
页面. 生成的页面位于项目根目录 vite
文件夹下,需要手动将内容复制到 vue3-element-admin 项目具体 .vue 文件中
script
生成将项目发布服务时需要的脚本文件和服务器配置文件, 如 nginx.conf
, supervisor.conf
等。可根据发布服务偏好选择是否添加 script
语言支持。
xorm 代码生成
在实际开发中, 每添加一个表时,一般就会有对应的增删改查操作, 而这些操作代码又高度相似,copy 代码时枯燥乏味。因此在插件中添加了 xorm
基本代码生成功能。让编码更聚焦核心功能。
如何使用
插件仅在 niuhe/.model.niuhe
中读取表定义并生成代码。
#alias 可选, 默认值为空, 用于指定表的别名
#mode 可选 默认值为 api,
#niuhe 可选, 默认值为 False, 在生成对应的 niuhe 是否覆盖已存在的 niuhe 文件内容
#dao 可选, 默认值为 False, 在生成对应的 dao 是否覆盖已存在的 dao 文件内容
#service 可选, 默认值为 False, 在生成对应的 service 是否覆盖已存在的 service 文件内容
#model 可选, 默认值为 False, 在生成对应的 model 是否覆盖已存在的 model 文件内容
#vite 可选, 默认值为 False, 在生成对应的 vite 是否覆盖已存在的 vite 文件内容, 需在 .config.json5 中 langs 中添加 "vite"
#这里是做示例用, 实际开发中直接写 class Config():即可
class Config(alias="系统配置", mode='api', niuhe=True, dao=True, service=True, model=True, vite=True):
'''系统配置表'''
name = required.String(desc='配置名称', index=True, search=True, len=255, notnull=True)# index 加索引, len varchar 最大长度, notnull 是否为空 search 分页查询时是否出现在参数中
value = required.Long(desc='配置值', search=True)
上述参数说明
参数 | 类型 | 默认值 | 必须 | 描述 |
---|---|---|---|---|
alias | str | '' | 可选 | 表的别名 |
mode | str | 'api' | 可选 | 生成代码的在哪个mode下 |
niuhe | bool | False | 可选 | 是否覆盖已存在的 niuhe 文件内容 |
dao | bool | False | 可选 | 是否覆盖已存在的 dao 文件内容 |
service | bool | False | 可选 | 是否覆盖已存在的 service 文件内容 |
model | bool | False | 可选 | 是否覆盖已存在的 model 文件内容 |
vite | bool | False | 可选 | 是否覆盖已存在的 vite 文件内容, 需要配置 .config.json5 中的 langs 添加 "vite" |
当首次生成代码时, 会生成以下几个文件(以 #app=demo
)
niuhe/api_config.niuhe
(需要手动include
到all.niuhe
中)src/demo/xorm/models/config.go
src/demo/xorm/daos/config_dao.go
src/demo/xorm/services/config_svc.go
src/demo/app/api/views/config_views.go
(include
后才会生成)src/demo/app/api/views/gen_config_views.go
(include
后才会生成)vite/api_config.vue
(需要在.config.json5
中langs
添加"vite"
)
除上述文件外, 如配置了 docs
, ts
等语言的生成,也会更新对应的文件内容. 各个生成的文件内容如下:
本节以下文件内容均为插件生成
niuhe/api_config.niuhe
class ConfigItem():
'''系统配置表'''
id = optional.Long(desc='id')
name = required.StringField(desc='配置名称')
value = required.LongField(desc='配置值')
create_at = optional.String(desc='创建时间')
update_at = optional.String(desc='更新时间')
class ConfigFormReq():
'''请求 Config 信息'''
id = required.Long()
class ConfigPageReq():
'''分页查询 Config 信息'''
page = required.Integer(desc='页码')
size = required.Integer(desc='每页数量')
value = required.LongField(desc='配置值')
class ConfigPageRsp():
'''分页查询 Config 信息'''
total = required.Long(desc='总数')
items = repeated.Message(cls=ConfigItem, desc='Config信息')
class ConfigDeleteReq():
'''批量删除 Config 信息'''
ids = repeated.Long(desc='记录id列表')
class ConfigNoneRsp():
'''Config 无数据返回'''
pass
with services():
GET('分页查询获取 Config 信息', '/api/config/page/', ConfigPageReq, ConfigPageRsp)
GET('查询获取 Config 信息', '/api/config/form/', ConfigFormReq, ConfigItem)
POST('添加 Config 信息', '/api/config/add/', ConfigItem, ConfigItem)
POST('更新 Config 信息', '/api/config/update/', ConfigItem, ConfigItem)
DELETE('删除 Config 信息', '/api/config/delete/', ConfigDeleteReq, ConfigNoneRsp)
model 表定义
src/demo/xorm/models/config.go
package models
// Generated by niuhe.idl
// 如要同步表结构, 需要手动将 Config 手动添加到 models.go 文件的 GetSyncModels 数组中
import (
"demo/app/api/protos"
"time"
)
// 系统配置表
type Config struct {
Id int64 `xorm:"NOT NULL PK AUTOINCR INT(11)"`
Name string `xorm:"VARCHAR(255) COMMENT('配置名称')"` // 配置名称
Value int64 `xorm:"INT COMMENT('配置值')"` // 配置值
CreateAt time.Time `xorm:"created"` // 创建时间
UpdateAt time.Time `xorm:"updated"` // 更新时间
DeleteAt time.Time `xorm:"deleted"` // 删除时间
}
func (row *Config) ToProto(item *protos.ConfigItem) *protos.ConfigItem {
if item == nil {
item = &protos.ConfigItem{}
}
item.Id = row.Id
item.Name = row.Name
item.Value = row.Value
item.CreateAt = row.CreateAt.Format(time.DateTime)
item.UpdateAt = row.UpdateAt.Format(time.DateTime)
return item
}
dao 表定义
src/demo/xorm/daos/config_dao.go
package daos
// Generated by niuhe.idl
import (
"demo/xorm/models"
"github.com/ma-guo/niuhe"
)
// 系统配置表
type _ConfigDao struct {
*Dao
}
// 系统配置表
func (dao *Dao) Config() *_ConfigDao {
return &_ConfigDao{Dao: dao}
}
// 根据ID获取数据
func (dao *_ConfigDao) GetByIds(ids ...int64) ([]*models.Config, error) {
rows := []*models.Config{}
session := dao.db()
err := session.In("id", ids).Desc("`id`").Find(&rows)
if err != nil {
niuhe.LogInfo("GetByIds Config error: %v", err)
return nil, err
}
return rows, nil
}
// 分页获取数据
func (dao *_ConfigDao) GetPage(page, size int, value int64) ([]*models.Config, int64, error) {
rows := make([]*models.Config, 0)
session := dao.db()
dao.Like(session, "`value`", value)
dao.Limit(session, page, size)
total, err := session.Desc("`id`").FindAndCount(&rows)
if err != nil {
niuhe.LogInfo("GetPage Config error: %v", err)
return nil, 0, err
}
return rows, total, nil
}
service 表定义
src/demo/xorm/services/config_svc.go
package services
// Generated by niuhe.idl
import (
"github.com/ma-guo/niuhe"
"demo/xorm/models"
)
// 系统配置表
type _ConfigSvc struct {
*_Svc
}
// 系统配置表
func (svc *_Svc) Config() *_ConfigSvc {
return &_ConfigSvc{svc}
}
// 获取单个数据
func (svc *_ConfigSvc) GetById(id int64) (*models.Config, bool, error) {
if id <= 0 {
return nil, false, nil
}
row := &models.Config{Id: id}
has, err := svc.dao().GetBy(row)
if err != nil {
niuhe.LogInfo("GetById Config error: %v", err)
}
return row, has, err
}
// 获取单个数据
func (svc *_ConfigSvc) GetBy(row *models.Config) (bool, error) {
has, err := svc.dao().GetBy(row)
if err != nil {
niuhe.LogInfo("GetBy Config error: %v", err)
}
return has, err
}
// 更新数据
func (svc *_ConfigSvc) Update(row *models.Config) (bool, error) {
has, err := svc.dao().Update(row.Id, row)
if err != nil {
niuhe.LogInfo("Update Config error: %v", err)
}
return has, err
}
// 插入数据
func (svc *_ConfigSvc) Insert(rows ...*models.Config) error {
if len(rows) == 0 {
return nil
}
// 2000条是经验值, 可根据自己需要更改
batchSize := 2000
for i := 0; i < len(rows); i += batchSize {
end := i + batchSize
if end > len(rows) {
end = len(rows)
}
_, err := svc.dao().Insert(rows[i:end])
if err != nil {
niuhe.LogInfo("Insert Config error: %v", err)
return err
}
}
return nil
}
// 删除数据
func (svc *_ConfigSvc) Delete(rows []*models.Config) error {
if len(rows) == 0 {
return nil
}
_, err := svc.dao().Delete(rows)
if err != nil {
niuhe.LogInfo("Delete Config error: %v", err)
}
return err
}
// 根据 id 获取 map 数据
func (svc *_ConfigSvc) GetByIds(ids ...int64) (map[int64]*models.Config, error) {
rowsMap := make(map[int64]*models.Config, 0)
if len(ids) == 0 {
return rowsMap, nil
}
rows, err := svc.dao().Config().GetByIds(ids...)
if err != nil {
niuhe.LogInfo("GetByIds Config error: %v", err)
return nil, err
}
for _, row := range rows {
rowsMap[row.Id] = row
}
return rowsMap, nil
}
// 分页获取数据
func (svc *_ConfigSvc) GetPage(page, size int, value int64) ([]*models.Config, int64, error) {
rows, total, err := svc.dao().Config().GetPage(page, size, value)
if err != nil {
niuhe.LogInfo("GetPage Config error: %v", err)
}
return rows, total, nil
}
config_views 定义
src/demo/xorm/models/config_views.go
package services
// Generated by niuhe.idl
import (
"github.com/ma-guo/niuhe"
"demo/xorm/models"
)
// 系统配置表
type _ConfigSvc struct {
*_Svc
}
// 系统配置表
func (svc *_Svc) Config() *_ConfigSvc {
return &_ConfigSvc{svc}
}
// 获取单个数据
func (svc *_ConfigSvc) GetById(id int64) (*models.Config, bool, error) {
if id <= 0 {
return nil, false, nil
}
row := &models.Config{Id: id}
has, err := svc.dao().GetBy(row)
if err != nil {
niuhe.LogInfo("GetById Config error: %v", err)
}
return row, has, err
}
// 获取单个数据
func (svc *_ConfigSvc) GetBy(row *models.Config) (bool, error) {
has, err := svc.dao().GetBy(row)
if err != nil {
niuhe.LogInfo("GetBy Config error: %v", err)
}
return has, err
}
// 更新数据
func (svc *_ConfigSvc) Update(row *models.Config) (bool, error) {
has, err := svc.dao().Update(row.Id, row)
if err != nil {
niuhe.LogInfo("Update Config error: %v", err)
}
return has, err
}
// 插入数据
func (svc *_ConfigSvc) Insert(rows ...*models.Config) error {
if len(rows) == 0 {
return nil
}
// 2000条是经验值, 可根据自己需要更改
batchSize := 2000
for i := 0; i < len(rows); i += batchSize {
end := i + batchSize
if end > len(rows) {
end = len(rows)
}
_, err := svc.dao().Insert(rows[i:end])
if err != nil {
niuhe.LogInfo("Insert Config error: %v", err)
return err
}
}
return nil
}
// 删除数据
func (svc *_ConfigSvc) Delete(rows []*models.Config) error {
if len(rows) == 0 {
return nil
}
_, err := svc.dao().Delete(rows)
if err != nil {
niuhe.LogInfo("Delete Config error: %v", err)
}
return err
}
// 根据 id 获取 map 数据
func (svc *_ConfigSvc) GetByIds(ids ...int64) (map[int64]*models.Config, error) {
rowsMap := make(map[int64]*models.Config, 0)
if len(ids) == 0 {
return rowsMap, nil
}
rows, err := svc.dao().Config().GetByIds(ids...)
if err != nil {
niuhe.LogInfo("GetByIds Config error: %v", err)
return nil, err
}
for _, row := range rows {
rowsMap[row.Id] = row
}
return rowsMap, nil
}
// 分页获取数据
func (svc *_ConfigSvc) GetPage(page, size int, value int64) ([]*models.Config, int64, error) {
rows, total, err := svc.dao().Config().GetPage(page, size, value)
if err != nil {
niuhe.LogInfo("GetPage Config error: %v", err)
}
return rows, total, nil
}
新增的 protos 定义
src/demo/api/protos/gen_protos.go
// 系统配置表
type ConfigItem struct {
Id int64 `json:"id" zpf_name:"id"` // id
Name string `json:"name" zpf_name:"name" zpf_reqd:"true"` // 配置名称
Value int64 `json:"value" zpf_name:"value" zpf_reqd:"true"` // 配置值
CreateAt string `json:"create_at" zpf_name:"create_at"` // 创建时间
UpdateAt string `json:"update_at" zpf_name:"update_at"` // 更新时间
}
// 请求,Config,信息
type ConfigFormReq struct {
Id int64 `json:"id" zpf_name:"id" zpf_reqd:"true"`
}
// 分页查询,Config,信息
type ConfigPageReq struct {
Page int `json:"page" zpf_name:"page" zpf_reqd:"true"` // 页码
Size int `json:"size" zpf_name:"size" zpf_reqd:"true"` // 每页数量
Value int64 `json:"value" zpf_name:"value" zpf_reqd:"true"` // 配置值
}
// 分页查询,Config,信息
type ConfigPageRsp struct {
Total int64 `json:"total" zpf_name:"total" zpf_reqd:"true"` // 总数
Items []*ConfigItem `json:"items" zpf_name:"items"` // Config信息
}
// 批量删除,Config,信息
type ConfigDeleteReq struct {
Ids []int64 `json:"ids" zpf_name:"ids"` // 记录id列表
}
// Config,无数据返回
type ConfigNoneRsp struct {
}
vite 定义
文件路径 vite/api_config.vue
。vite 需结合 vue3-element-admin 库使用, 是生成的增删改查 api 管理页面内容。
<template>
<div class="app-container">
<div class="search-container">
<el-form ref="queryFormRef" :model="queryParams" :inline="true">
<el-form-item prop="value" label="配置值">
<el-input v-model="queryParams.value" placeholder="配置值" clearable @keyup.enter="fetchPage" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="fetchPage"><i-ep-search />搜索</el-button>
<el-button @click="resetQuery"><i-ep-refresh />重置</el-button>
</el-form-item>
</el-form>
</div>
<el-card shadow="never" class="table-container">
<template #header>
<el-button @click="openDialogWithAdd()" type="success"><i-ep-plus />新增</el-button>
<el-button type="danger" :disabled="state.ids.length === 0"
@click="bantchDelete()"><i-ep-delete />删除</el-button>
</template>
<el-table ref="dataTableRef" v-loading="state.loading" :data="configItems" highlight-current-row border
@selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="ID" prop="id" align="center" />
<el-table-column label="配置名称" prop="name" align="center" />
<el-table-column label="配置值" prop="value" align="center" />
<el-table-column fixed="right" label="操作" width="140" align="center">
<template #default="{ row }">
<el-button type="primary" size="small" link @click="openDialogWithEdit(row.id)">
<i-ep-edit />编辑
</el-button>
<el-button type="primary" size="small" link @click="handleDelete(row.id)">
<i-ep-delete />删除
</el-button>
</template>
</el-table-column>
</el-table>
<pagination v-if="state.total > 0" v-model:total="state.total" v-model:page="queryParams.page"
v-model:limit="queryParams.size" @pagination="fetchPage" />
</el-card>
<!-- Config 表单弹窗 -->
<el-dialog v-model="state.dialogVisible" :title="state.dialogTitle" @close="closeDialog">
<el-form ref="configFormRef" :model="formData" :rules="rules" label-width="100px">
<el-form-item label="ID" prop="id" v-if="formData.id > 0">
<el-input v-model="formData.id" disabled placeholder="" />
</el-form-item>
<el-form-item prop="name" label="配置名称">
<el-input v-model="formData.name" placeholder="配置名称" clearable />
</el-form-item>
<el-form-item prop="value" label="配置值">
<el-input v-model="formData.value" placeholder="配置值" clearable />
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button type="primary" @click="handleSubmit">确 定</el-button>
<el-button @click="closeDialog">取 消</el-button>
</div>
</template>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { setConfigAdd } from "@/api/demo/api";
import { setConfigUpdate } from "@/api/demo/api";
import { deleteConfigDelete } from "@/api/demo/api";
import { getConfigForm } from "@/api/demo/api";
import { getConfigPage } from "@/api/demo/api";
defineOptions({
name: "config",
inheritAttrs: false,
});
const queryFormRef = ref(ElForm);
const configFormRef = ref(ElForm);
const configItems = ref<Demo.ConfigItem[]>();
const state = reactive({
loading: false,
total: 0,
ids: [] as number[],
dialogVisible: false,
dialogTitle: "",
});
const queryParams = reactive<Demo.ConfigPageReq>({
page: 1,
size: 10,
value: 0,
});
const formData = reactive<Demo.ConfigItem>({
id: 0,
name: "",
value: 0,
create_at: "",
update_at: ""
});
// 根据需要添加校验规则
const rules = reactive({
// name: [{ required: true, message: "本字段必填", trigger: "blur" }],
});
/** 查询 */
const fetchPage = async () => {
state.loading = true;
const rsp = await getConfigPage(queryParams);
state.loading = false;
if (rsp.result == 0) {
configItems.value = rsp.data.items;
state.total = rsp.data.total;
}
}
/** 重置查询 */
function resetQuery() {
queryFormRef.value.resetFields();
queryParams.page = 1;
fetchPage();
}
/** 行checkbox 选中事件 */
function handleSelectionChange(selection: any) {
state.ids = selection.map((item: any) => item.id);
}
/** 打开添加弹窗 */
function openDialogWithAdd() {
state.dialogVisible = true;
state.dialogTitle = "添加Config";
resetForm();
}
/** 打开编辑弹窗 */
const openDialogWithEdit = async (roleId: number) => {
state.dialogVisible = true;
state.dialogTitle = "修改Config";
state.loading = true;
const rsp = await getConfigForm({ id: roleId });
state.loading = false;
if (rsp.result == 0) {
Object.assign(formData, rsp.data);
}
}
/** 保存提交 */
function handleSubmit() {
configFormRef.value.validate((valid: any) => {
if (valid) {
if (formData.id) {
updateRowRecord();
} else {
addRowRecord();
}
}
});
}
/** 新增记录 */
const addRowRecord = async () => {
state.loading = true;
const rsp = await setConfigAdd(formData);
state.loading = false
if (rsp.result == 0) {
ElMessage.success("添加成功");
closeDialog();
resetQuery();
}
}
/** 修改记录 */
const updateRowRecord = async () => {
state.loading = true;
const rsp = await setConfigUpdate(formData);
state.loading = false
if (rsp.result == 0) {
ElMessage.success("修改成功");
closeDialog();
resetQuery();
}
}
/** 关闭表单弹窗 */
function closeDialog() {
state.dialogVisible = false;
resetForm();
}
/** 重置表单 */
function resetForm() {
const value = configFormRef.value;
if (value) {
value.resetFields();
value.clearValidate();
}
formData.id = 0;
formData.name = "";
formData.value = 0;
formData.create_at = "";
formData.update_at = "";
}
/** 删除 Config */
function handleDelete(id: number) {
ElMessageBox.confirm("确认删除已选中的数据项?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(async () => {
state.loading = true;
const rsp = await deleteConfigDelete({ ids: [id] });
state.loading = false;
if (rsp.result == 0) {
ElMessage.success("删除成功");
resetQuery();
}
});
}
/** 批量删除 */
const bantchDelete = () => {
if (state.ids.length <= 0) {
ElMessage.warning("请勾选删除项");
return;
}
ElMessageBox.confirm("确认删除已选中的数据项?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(async () => {
state.loading = true;
const rsp = await deleteConfigDelete({ ids: state.ids });
state.loading = false;
if (rsp.result == 0) {
ElMessage.success("删除成功");
}
resetQuery();
});
};
onMounted(() => {
fetchPage();
});
</script>
生成其他协议
在支持 MCP 的AI编辑器下, 建议采用
MCP
功能 方式来生成协议。目前推荐使用下一节的方式: 用 template 生成其他语言协议 来生成协议。 template 方式中包含了大量参考例子template模板库供使用
在开发过程中,我们可能需要生成不同语言的协议文件。niuhe插件支持 go, ts, vite 等协议代码生成。同时为了方便其他语言使用。将 niuhe 协议以 json 格式输出了完整的协议内容,可以很方便地被其他语言解析和使用。以下是如何生成其他语言协议的步骤:
1. 配置 .config.json5
首先,在 .config.json5
的 langs
中添加 protocol
配置项.
2. protocol 定义
其他语言协议实现若有疑问, 请加交流群 QQ [
971024252
]获取更多支持。 配置 protocol 后, 生成代码时会在docs
文件下生成protocol.json
文件。协议定义为:
declare namespace Protocol {
interface StringEnum {
value: string // 枚举值 - 数字也是 string
name: string // 字段名
desc: string // 描述
}
// 枚举类型
interface Enum {
mode: string
type: 'string' | 'integer'
name: string
desc: string
values: StringEnum[]
}
interface Field {
label: string// required, optional, repeated, mapping
type: string // 'integer', 'decimal', 'float', 'long', 'string', 'boolean', 'message', 'enum', 'stringenum','file' 'dict' 'any'
name: string // 字段名
desc: string // 描述
ref?: string // 引用的结构
deprecated?: boolean // 是否已经废弃
version?: string // 版本号
}
interface Message {
mode: string //api
name: string // 名称
desc: string // 描述
fields: Field[]
}
interface Route {
method: string // post, get[, put, patch, delete, head, options]
url: string // /api/sys/ts/
mode: string // api
view: string // sys
name: string // ts
desc: string // 描述
req?: Message
rsp?: Message
}
interface Docs {
app: string
/** api 路由信息 */
routes: Route[]
/** 数组 */
enums: {
[mode_name: string]: Enum
}
/** 消息类型 */
messages: {
[mode_name: string]: Message
}
}
}
go 语言解析为 kotlin 版例子
通过解析上述协议文件可得到请求方法, 路径, 入参和出参的详细定义。下面为 go 语言解析出 kotlin 版本的一个例子。
本例子代码位于 src/demo/protocol/init.go
package protocol
import (
"encoding/json"
"fmt"
"os"
"strings"
"unicode"
"github.com/ma-guo/niuhe"
)
type StringEnum struct {
Value string `json:"vallue"` // 枚举值 - 数字也是 string
Name string `json:"name"` // 字段名
Desc string `json:"desc"` // 描述
}
// 枚举类型
type Enum struct {
Mode string `json:"mode"`
Etype string `json:"type"` // 'string' | 'integer'
Name string `json:"name"`
Desc string `json:"desc"`
Values []StringEnum `json:"values"`
}
type Field struct {
Label string // required, optional, repeated, mapping
Type string `json:"type"` // 'integer', 'decimal', 'float', 'long', 'string', 'boolean', 'message', 'enum', 'stringenum','file' 'dict' 'any'
Name string `json:"name"` // 字段名
Desc string `json:"desc"` // 描述
Ref string `json:"ref"` // 引用的结构
}
type Message struct {
Mode string `json:"mode"` //api
Name string `json:"name"` // 名称
Desc string `json:"desc"` // 描述
Fields []*Field `json:"fields"`
}
type Route struct {
Method string `json:"method"` // post, get[, put, patch, delete, head, options]
Url string `json:"url"` // /api/sys/ts/
Mode string `json:"mode"` // api
View string `json:"view"`
Name string `json:"name"`
Desc string `json:"desc"` // 描述
Req *Message `json:"req"`
Rsp *Message `json:"rsp"`
}
type Docs struct {
App string `json:"app"`
/** api 路由信息 */
Routes []Route `json:"routes"`
/** 数组 */
Enums map[string]*Enum `json:"enums"`
/** 消息类型 */
Messages map[string]*Message `json:"messages"`
}
// 解析结果
type ParseResult struct {
Interfaces string
Beans string
}
func (doc *Docs) getKtType(meta *Field) string {
switch meta.Type {
case "integer":
return "Int"
case "decimal":
return "Float"
case "float":
return "Float"
case "long":
return "Long"
case "stringenum":
case "string":
return "String"
case "boolean":
return "Boolean"
case "message":
if meta.Ref != "" {
return doc.fmtName(meta.Ref)
}
return doc.fmtName(meta.Ref)
case "enum":
return "Int"
case "file":
return "Any?"
case "any":
return "Any?"
}
return "Any?"
}
func (doc *Docs) getKtLabel(meta *Field) string {
switch meta.Label {
case "required":
return doc.getKtType(meta)
case "optional":
return doc.getKtType(meta) + "?"
case "repeated":
return "List<" + doc.getKtType(meta) + ">?"
case "mapping":
return "Map<String, " + doc.getKtType(meta) + ">?"
}
return doc.getKtType(meta)
}
func getKtDefault(ntype string) string {
switch ntype {
case "integer":
return "0"
case "decimal":
return "0f"
case "float":
return "0f"
case "long":
return "0"
case "string":
return "\"\""
case "boolean":
return "false"
case "message":
return "null"
case "enum":
return "0"
case "file":
return "null"
case "any":
return "null"
}
return "null"
}
func (doc *Docs) fmtName(name string) string {
// 从 Hrhelper.StringEnum 返回 StringEnum
if strings.Contains(name, ".") {
return name[strings.Index(name, ".")+1:]
}
return name
}
// toCamelCase 函数用于将下划线分隔的字符串转换为驼峰命名法
func (doc *Docs) toCamelCase(s string) string {
// 按下划线分割字符串
parts := strings.Split(s, "_")
var result strings.Builder
for i, part := range parts {
if i == 0 {
// 第一个部分保持原样
result.WriteString(part)
} else {
// 后续部分首字母大写
for j, r := range part {
if j == 0 {
result.WriteRune(unicode.ToUpper(r))
} else {
result.WriteRune(r)
}
}
}
}
return result.String()
}
func (doc *Docs) buildBean(item *Message) string {
var lines strings.Builder
lines.WriteString("@Serializable\n")
lines.WriteString("data class " + doc.fmtName(item.Name) + "(\n")
for _, field := range item.Fields {
if field.Desc != "" {
lines.WriteString("\n\t/** " + field.Desc + " */\n")
}
lines.WriteString("\t@SerialName(\"" + field.Name + "\")\n")
lines.WriteString("\tval " + doc.toCamelCase(field.Name) + ": " + doc.getKtLabel(field) + " = " + getKtDefault(field.Type) + ",\n")
}
lines.WriteString(") {}\n")
return lines.String()
}
func (doc *Docs) buildApi(route Route) string {
var lines strings.Builder
lines.WriteString("\t/**\n")
if route.Desc != "" {
lines.WriteString("\t * " + route.Desc + "\n\t *\n")
}
req := route.Req
if len(req.Fields) > 0 {
lines.WriteString("\t * 请求参数:\n")
for _, value := range req.Fields {
lines.WriteString("\t * @param " + doc.toCamelCase(value.Name) + " " + value.Desc + "\n")
}
}
lines.WriteString("\t */\n")
lines.WriteString("\t@FormUrlEncoded\n")
lines.WriteString(fmt.Sprintf("\t@%v(\"%v\")\n", strings.ToUpper(route.Method), route.Url))
rspRef := route.Rsp
if len(req.Fields) > 0 {
lines.WriteString("\tsuspend fun " + doc.fmtMethodName(route) + "(\n")
for _, value := range req.Fields {
lines.WriteString("\t\t@Field(\"" + value.Name + "\") " + doc.toCamelCase(value.Name) + ": " + doc.getKtLabel(value) + ",\n")
}
lines.WriteString("\t): CommRsp<" + doc.fmtName(rspRef.Name) + ">\n")
} else {
lines.WriteString("\tsuspend fun " + doc.fmtMethodName(route) + "(): CommRsp<" + doc.fmtName(rspRef.Name) + ">\n")
}
return lines.String()
}
// 将 /api/user/info/, post 转换为 postUserInfo
func (doc *Docs) fmtMethodName(route Route) string {
method := strings.ToLower(route.Method)
tmps := []string{method, route.View, route.Name}
return doc.toCamelCase(strings.Join(tmps, "_"))
}
// 解析 protocol 文档
// docs 为 protocol.json 文件内容
func Parse(appName string, docs []byte) (*ParseResult, error) {
info := &Docs{}
err := json.Unmarshal(docs, info)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal protocol docs: %w", err)
}
var interfaces strings.Builder
interfaces.WriteString("\nimport kotlinx.serialization.SerialName\n")
interfaces.WriteString("interface " + appName + "{\n")
for _, route := range info.Routes {
interfaces.WriteString(info.buildApi(route))
}
interfaces.WriteString("}\n")
result := &ParseResult{
Interfaces: interfaces.String(),
}
var beans strings.Builder
beans.WriteString("\nimport kotlinx.serialization.SerialName\n")
beans.WriteString("import kotlinx.serialization.Serializable\n\n")
for _, msg := range info.Messages {
beans.WriteString(info.buildBean(msg))
}
result.Beans = beans.String()
return result, nil
}
例子调用
调用例子如下:
func ShowKtProtocol(docPath string) {
// docPath := "you protocol.json path"
docs, err := os.ReadFile(docPath)
if err != nil {
niuhe.LogInfo("read docs error: %v", err)
return
}
result, err := Parse("HrHelper", docs)
if err != nil {
niuhe.LogInfo("%v", err)
return
}
// 保存到对应的文件
niuhe.LogInfo("%v", result.Interfaces)
niuhe.LogInfo("%v", result.Beans)
}
最佳实践
可结合 niuhe/.config.json5 中的配置 endcmd:[] 在生成代码后执行某个命令将上述解析的结果保存到对应位置获得更佳体验
下一节提供了更简易地用模板方法实现转换为其他目标语言的例子。
用 template 生成其他语言协议
在前一章节 生成其他语言协议 里,我们展示了如何解析 protocol
协议来生成目标语言的代码。在这一节,我们将演示如何通过 handlebars 语法 来实现生成其他语言代码的方法。
本节的演示代码位于项目 niuhedemo 中。更多模板库, 请移步template 模板库
开启 template 功能
在 niuhe/.config.json5
文件里,我们可以通过配置 templates
选项来开启模板渲染功能。下面是 templates
配置项的声明:
interface TplConfig {
/** 模板类型, 可选为 message, enum, route */
type: 'message' | 'enum' | 'route'
modes: string[]
/** template 文件地址 */
template: string
/** 存放的目标地址 */
output: string
}
// 入口文件定义的信息
interface EntryInfo {
...
/** 是否显示烟花效果 */
fire?: boolean
/** template 渲染 */
templates?: TplConfig[]
}
生成代码
我们通过在 niuhe/.config.json5
中添加配置,来演示生成 typescript
脚本所需的类型、常量和 API 请求这三部分代码。其他语言的示例可以根据对应语言的特性,结合 handlebar
语法进行修改。
templates:[{
type: 'message', // 可选值有 message, route, enum 三个, 同 protocol 协议相同
template: './templates/ts_message.tpl', // 模板文件地址
output: './output/message.d.ts', // 输出文件地址
modes: ["api"], // 生成的 mode, url 第一部分
},{
type: 'route',
template: './templates/ts_route.tpl',
output: './output/route.ts',
modes: ["api"],
},{
type: 'enum',
template: './templates/ts_enum.tpl',
output: './output/enum.d.ts',
modes: ["api"],
}]
生成类型定义代码
./templates/ts_message.tpl 代码如下:
// Generated by niuhe.idl
// 此文件由 niuhe.idl 自动生成, 请勿手动修改
interface Rsp<T> {
result: number
data: T
message: string
}
declare namespace {{capitalize mod}} {
{{#each items}}
{{#if desc}}
/** {{desc}} */
{{/if}}
interface {{name}} {
{{#each fields}}
{{#if desc}}
/** {{desc}} */
{{/if}}
{{#if (isArray label)}}
{{ json }}: {{ convertType type }}[],
{{else}}
{{ json }}: {{ convertType type }},
{{/if}}
{{/each}}
}
{{/each}}
}
生成的 typescript
代码如下:
// Generated by niuhe.idl
// 此文件由 niuhe.idl 自动生成, 请勿手动修改
interface Rsp<T> {
result: number
data: T
message: string
}
declare namespace Demo {
/** 分页查询Config信息 */
interface ConfigPageReq {
/** 页码 */
page: number,
/** 每页数量 */
size: number,
/** 配置名称 */
name: string,
/** 配置值 */
value: number,
}
/** 系统配置表 */
interface ConfigItem {
/** id */
id: number,
/** 配置名称 */
name: string,
/** 配置值 */
value: number,
/** 创建时间 */
create_at: string,
/** 更新时间 */
update_at: string,
}
/** 分页查询Config信息 */
interface ConfigPageRsp {
/** 总数 */
total: number,
/** Config信息 */
items: ConfigItem[],
}
/** 请求Config信息 */
interface ConfigFormReq {
id: number,
}
/** 批量删除Config信息 */
interface ConfigDeleteReq {
/** 记录id列表 */
ids: number[],
}
/** Config无数据返回 */
interface ConfigNoneRsp {
}
/** 测试请求 */
interface HelloReq {
/** 用户名 */
name: string,
}
/** 测试响应 */
interface HelloRsp {
/** 问候语 */
greeting: string,
}
/** 空请求 */
interface NoneReq {
}
/** 空响应 */
interface NoneRsp {
}
/** RPC测试用例请求参数 */
interface XxxYyyReqMsg {
/** 用户名 */
name: string,
/** 密码 */
password: string,
}
/** RPC测试用例返回参数 */
interface XxxYyyRspMsg {
/** 用户open_id */
open_id: string,
/** 账户信息 */
account_info: string,
}
}
这里只展示了类型代码的生成,由于篇幅限制,常量和 API 代码请访问 示例项目查看。
添加模板代码助手
在插件里,我们添加了 capitalize
、isArray
和 cameCase
这三个代码助手。在上一小节的演示中,用到了 capitalize
和 isArray
,它们的定义如下:
// 首字母大写 Helper
handlebars.registerHelper('capitalize', (str: string): string => {
if (!str) {
return str;
}
if (str.length === 1) {
return str.toUpperCase();
}
return str.charAt(0).toUpperCase() + str.slice(1);
});
// 数组 Helper
handlebars.registerHelper('isArray', (label: string): boolean => {
return label === 'repeated';
});
// 驼峰命名 Helper
handlebars.registerHelper('cameCase', (text: string): string => {
return camelCase(text);
});
添加自定义代码助手
当通过模板功能成功生成一次代码后, 会创建 niuhe/.template.tpl.ts
文件(这是自定义代码助手文件入口, 也可手动创建)。其默认内容如下:
// templates 注入 例子。 不能使用 js 高级语法。
// 详细例子参考 https://handlebarsjs.com/zh/
handlebars.registerHelper('funName', function(args){
return args;
});
为了在 template
里更简洁、灵活地编写模板代码,我们可以根据需求添加更多代码助手。
在上面的演示代码中,使用到了自定义的 convertType
和 isInteger
两个代码助手,下面是演示代码中完整的 niuhe/.templates.tpl.js 内容:
// templates 注入 例子。 不能使用 js 高级语法。
// 插件官方说明文档: http://niuhe.zuxing.net/chapter4/section6.html
// handlebars详细例子参考 https://handlebarsjs.com/zh/
handlebars.registerHelper('funName', function(args){
return args;
});
handlebars.registerHelper('convertType', function (type) {
const typeMap = {
'string': 'string',
'stringenum': 'string',
'float': 'number',
'double': 'number',
'int': 'number',
'long': 'number',
'enum': 'number',
'integer': 'number',
'boolean': 'boolean',
'object': 'any',
'array': 'any[]',
'map': 'any',
};
return typeMap[type] || type;
});
handlebars.registerHelper('isInteger', function (type) {
return type === 'integer';
});
高效调试
在正确配置 niuhe/.config.json5
中的 templates
项后, 插件会在调用 handlebars
从模板文件编译目标文件前, 打印传入的数据内容(console.log({config, data})
)。其中 data
是待编译的具体数据结构和数值。 我们可以通过 帮助
-> 切换开发人员工具
-> 控制台
页面查看打印的数据信息。
完整的数据结构定义
在编写 *.tpl 文件时, 数据项(message
), 常量(enum
)和请求方法(route
) 定义结构如下。参考下面的定义,有助于我们高效、快速地编写模板代码。
declare namespace Tpl {
/**
* 字段定义属性, enum 比 message 多一个 value 字段
*/
interface TplField {
name: string;
/** 字段描述 */
desc?: string;
/** 类型 专属字段 */
type?: string;
/** json 名 */
json: string;
/** 引用的结构 */
ref?: TplMessage;
/** 标签, required, optional, repeated, mapping */
label: string;
/** 字段版本 */
version?: string;
/** 是否废弃 */
deprecated?: boolean
/** enum 专属字段 */
value?: string;
}
interface TplMessage {
mode: string
/* 类名 */
name: string;
/** 类描述 */
desc?: string;
/** 字段列表 */
fields: TplField[];
/** 是否废弃 */
deprecated?: boolean;
/** enum 专属字段, 为整数常量和字符串常量 */
type?: 'integer' | 'string';
}
interface TplRoute {
/** 如: api, url 第一段内容 */
mode: string
/** 路由方法, post, get, put, option... */
method: string;
/** api 路径 */
url: string;
/* 路由名 */
name: string;
/** 描述 */
desc?: string;
/** 返回结构 */
rsp?: TplMessage;
/** 请求结构 */
req?: TplMessage;
/** RPC定义返回的错误代码列表 */
codes: TplField[]
}
interface Template {
/** 数据项定义 */
messages: TplMessage[]
/** 常量定义 */
enums: TplMessage[]
/** 请求方法定义 */
routes: TplRoute[]
}
}
错误代码定义和检测
本功能需要
github.com/ma-guo/niuhe
版本>=v1.0.5
, niuhe 插件版本>=0.6.20
当前插件支持通过 RPC().codes(...)
方式定义返回的错误代码列表。如果不进行额外处理, 在后端返回意料之外(非错误码列表中)的错误码时,后端无法感知到这一情形。为了支持这种检测, 我们需要再多做一些工作。
插件支持
.codes(LanguageType.ZH_CN, LanguageType.ZH_TW, Item(100, '错误描述'))
格式添加错误码。当
codes
中包含Item
格式定义错误码时,将自动生成一个ErrorEnum
的ConstGroup
来定义错误码数据集, 此种方式更灵活。
利用下面的代码展示如何实现这一功能。
生成代码时, src/{mode}/app/views/init.go
文件内容如下:
package views
// Generated by niuhe.idl
import (
"github.com/ma-guo/niuhe"
)
var thisModule *niuhe.Module
func GetModule() *niuhe.Module {
if thisModule == nil {
thisModule = niuhe.NewModule("${model}")
// 需要在 .config.json5 中添加 langs: ["route"]
// 使用下面的代码, 当错误码同 RPC 方式定义的 .codes(...) 不同时回收到回调
// thisModule.AddRouteItem(protos.RouteItems...)
// thisModule.RegisterCodeNotify(func(code int, path string) {
// niuhe.LogError("code: %v with %v", code, path)
// })
}
return thisModule
}
`
我们只需要在 .config.json5
的 langs
中添加 "route"
, 然后将 GetModule
中被注释的三行代码打开即可快速实现检测这种非预期的错误码问题定位。
本功能优点
- 无需修改业务代码, 即可实现错误码检测
- 支持自定义错误码检测逻辑, 可主动发现意外错误码, 加快问题定位
endcmd 命令使用和讲解
在 niuhe/.config.json5
文件中预留了 endcmd:[]
配置项。这个配置允许你在生成代码后执行自定义的命令。以下是如何使用和讲解 endcmd
命令的步骤:
1. 命令详解
endcmd
为一字符串数组, 默认为空数组。一般第一个为命令名, 后续为参数, 如: go mod tidy
则定义为 ["go","mod","tidy"]
。 切勿将所有命令写在一起, 否则可能执行失败。
配置文件完整说明
在项目首次生成代码时, 会生成 niuhe/.config.json5
配置文件, 配置文件内包含了常用的配置项, 少部分个性化的配置项并没有包含其中。这里介绍完整配置项和配置项目说明
配置项 | 类型 | 默认值 | 说明 |
---|---|---|---|
app | string | "" | 应用名, 定时时将覆盖 #app=xxx 定义的值。 示例值: "admin" |
gomod | string | "" | 为空或未定义时同 app , 也可定义为其他名字, 如在 admin-core 项目中为 github.com/ma-guo/admin-core |
langs | string[] | [] | 语言类型, "go" 内定支持, 这里支持 "ts" ,"docs" ,"route" ,"protocol" , "vite" , "openapi" , "template" 。示例值: ["ts", "docs"] 。 参考插件支持的语言 |
tstypes | string[] | ["./typings/types.d.ts"] | 已废弃, langs 中支持 "ts" 时有效, 为生成的 ts 接口文件路径, 可定义多个, 如: tstypes:["full_types_path1", relative_types_path2"], 支持绝对路径和相对路径 |
tsapi | string[] | ["./typings/api.ts"] | 已废弃, langs 中支持 "ts" 时有效, 为生成的 ts 接口文件路径, 可定义多个, 如: tsapi:`["full_api_path1", relative_api_path2"], 支持绝对路径和相对路径 |
typescript | TsConfig[] | [] | langs 中支持 ts 时有效, ts生成相关的配置文件, 参考生成 typescript api 定义 |
typescript.app | string | "admin" | ts中的 namespace 名字, 不定义时同应用名, 在需要将 namespcace 定义为其他值时使用。示例值: "admin" |
typescript.modes | string[] | [] | 限定生成的 ts 中包含的 mode, 在大型项目中需要选择性生成最小化相关代码时使用。 示例值: ["api"] |
typescript.types | string[] | [./typings/types.d.ts] | 生成的 ts 接口文件路径, 可定义多个, 如: types:["full_types_path1", "relative_types_path2"], 支持绝对路径和相对路径 |
typescript.api | string[] | ["./typings/api.ts"] | 生成的 ts 接口文件路径, 可定义多个, 如: api:["full_api_path1", "relative_api_path2"], 支持绝对路径和相对路径 |
typescript.enumjs | string[] | [] | 生成的 js 枚举信息 文件路径, 可定义多个, 如: api:["full_enums_path1", "relative_enum2_path2"], 支持绝对路径和相对路径 |
typescript.tsenum | bool | false | niuhe中定义的字段 type 为 Enum/StringEnum 时在 types 中是生成为对应的 group 定义, 为 false 时转换为 int/string ,示例值:false |
typescript.optional | bool | false | niuhe中定义的字段 label 为 optional 时在 types 中是否要添加 ?,示例值:false |
vite | ViteConfig[] | [] | langs 中支持 vite 时有效, 前端页面生成相关的配置文件, 建议保持同 typescript 中同名配置保持一致。 参考 xorm 代码生成 |
vite.app | string | "" | ts代码中的引用的 namespace 名字, 不定义时同应用名。 建议同 typescript.app 保持一致。示例值:"admin" |
vite.tsenum | bool | false niuhe中定义的字段 type 为 Enum/StringEnum 时在 types 中是生成为对应的 group 定义, 为 false 时转换为 int/string | |
showlog | bool | false | 生成代码时是否生成日志, 打开时,日志在项目目录下 niuhe.log 中, 生成错误时可进行排查,示例值:"false" |
endcmd | string[] | [] | 生成代码后执行的命令, 默认为空, 一般第一个为命令名, 后续为参数, 如: go mod tidy 则定义为 ["go","mod","tidy"] |
fire | bool | false | 烟花效果,示例值:false |
templates | TplConfig[] | [] | langs 中支持template 时有效,使用模板方式生成其他语言时自定义配置, 参考 用 template 生成其他语言协议 |
templates.type | string | "" | 可选值为"message" ,"enum" 和"route" , 分别对应消息,枚举和 api。示例值:"message" |
templates.modes | string[] | [] | 限定生成的模板中包含的 mode , 在大型项目中需要选择性生成最小化相关代码时使用。示例值:["api"] |
templates.template | string | "" | 模板文件地址, 支持绝对和相对路径。示例值:"./templates/kt_enum.tpl" |
templates.output | string | "" | 模板生成代码后的保存地址, 支持绝对和相对路径。示例值:"./output/Enums.kt" |
escape | bool | false | 生成 templates 时是否转义, 默认不转义,示例值:false |
timer | bool | false | 打印本次生成代码各部分耗时日志, showlog 为 true 时打印在日志最后, 同时可在 帮助 ->切换开发人员工具 ->控制台 中查看。示例值:false |
viewprefix | string | "" | 生成的文件前缀, 同一个 view 会生成 ${view}_views.go 和 gen_${view}_views.go 两个文件, 存在多个 view 时会排序杂乱, 统一添加前缀便于将${view}_views.go 同gen_* 文件分离出来。特别地, 当 viewprefix 为 true /"true" 时, 其值同 mode |
配置项定义
// 生成 typescript 相关配置
interface TsConfig {
app: string
modes: string[]
types: string[]
api: string[]
/** 枚举的 js 文件名 */
enumjs: string[]
tsenum: boolean
optional: boolean
}
// 通过 .model.niuhe 生成前端代码配置
interface ViteConfig {
app: string
/** field 为 enum 的是否需要转换类型 */
tsenum: boolean
}
// templates 相关配置
interface TplConfig {
/** 模板类型, 可选为 message, enum, route */
type: 'message' | 'enum' | 'route'
modes: string[]
/** template 文件地址 */
template: string
/** 存放的目标地址 */
output: string
// 配置文件定义
interface EntryInfo {
/** 项目名, 定义时覆盖: `#app=admin` */
app: string //
/** go module 名字, 默认同 app 保持一直 */
gomod: string
/** 支持的语言, 默认支持 `go`, 可选为 `ts`, `docs`, `openapi`, `route`, 'protocol', 'vite, 以 逗号分割 */
langs: string[] //
/** @deprecated ts types.d.ts 自定义的地址 */
tstypes: string[]
/** @deprecated ts api.ts 自定义的地址 */
tsapi: string[]
typescript: TsConfig[]
vite: ViteConfig
/** 是否支持生成日志, 默认为 false */
showlog: boolean
/** 结束后执行的命令, 可选 */
endcmd: string[]
/** 是否显示烟花效果 */
fire?: boolean
/** render 模板 */
templates?: TplConfig[]
/** 生成 template 时是否转义, 默认不转义 */
escape?: boolean
/** 打印耗时 */
timer?: boolean
/** ${view}_views.go 文件前缀 */
viewprefix?: string
}
配套管理后台
本章节主要介绍如何使用 niuhe 插件生成配套的管理后台 admin-core和前端项目vue3-element-admin 的使用
admin-core是一款开箱即用的中后台解决方案,它集成了RBAC权限体系,快速实现用户/角色/菜单管理, 文件存储等基本功能, 在开发时几行代码接入即可实现功能集成。同时支持自定义扩展功能。
vue3-element-admin 是一个基于Vue3和Element Plus的完整后台功能组件,提供丰富的UI组件和布局方案,帮助开发者快速构建企业级后台应用。
本模板提供了在线演示(账号: admin / 123456), 点击链接可预览模板。
插件 xorm 代码生成 功能支持生成 vue3-element-admin 页面代码, 基于 xorm 的数据库操作代码等,实现增删改查功能。实现管理后台从后端到前端的基本实现,加快功能开发速度。
管理后台支持的功能清单如下:
功能模块 | 支持情况 | 备注 |
---|---|---|
用户管理 | ✅ | - |
菜单管理 | ✅ | - |
角色管理 | ✅ | - |
部门管理 | ✅ | - |
岗位管理 | ❌ | - |
接口管理 | ✅ | API级权限控制 |
日志管理 | ❌ | - |
字典管理 | ✅ | - |
OSX | ✅ | 针对各大云厂商云存储文件上传服务,根据系统配置前端自动上传 |
文件管理 | ✅ | 管理上传的文件 |
内容管理 | ✅ | 文章内容管理 |
希望让你早点下班,多点时间陪伴家人
免责声明
该方案提供的是“现况”,没有任何保证,表示或暗示,包括但不限于担保的质量,性能,不侵权,适销性或特定用途的适用性。在任何情况对于任何因使用本方案包装所产生的 任何直接性、间接性、偶发性、特殊性、惩罚性或任何结果的损害(包括但不限于替代商品或劳务之购用、使用损失、资料损失、利益损失、业务中断等等),admin-core
不负任何责任。不允许使用该框架做一切违反中华人民共和国法律的事情(包含软件但不限于软件)。
服务端 admin-core 引入
admin-core 是管理后台项目 vue3-element-admi 的后端支持项目。本身也是 niuhe 插件生成的项目. 可在项目中通过简单的设置来接入它
1. main 中引入
以 demo
项目为例, 当前 src/demo/main.go
内容为:
package main
// Generated by niuhe.idl
import (
apiViews "demo/app/api/views"
"demo/config"
"fmt"
"github.com/ma-guo/niuhe"
"os"
)
type baseBoot struct{}
type BaseBoot struct {
baseBoot
}
func (baseBoot) LoadConfig() error {
if len(os.Args) < 2 {
return fmt.Errorf("Usage: %s <config-path>", os.Args[0])
}
return config.LoadConfig(os.Args[1])
}
func (baseBoot) BeforeBoot(svr *niuhe.Server) {}
func (baseBoot) RegisterModules(svr *niuhe.Server) {
svr.RegisterModule(apiViews.GetModule())
}
func (baseBoot) Serve(svr *niuhe.Server) {
svr.Serve(config.Config.ServerAddr)
}
func main() {
boot := BaseBoot{}
if err := boot.LoadConfig(); err != nil {
panic(err)
}
svr := niuhe.NewServer()
boot.BeforeBoot(svr)
boot.RegisterModules(svr)
boot.Serve(svr)
}
接入步骤:
- 在 import 中添加依赖
adminBoot "github.com/ma-guo/admin-core/boot"
- 加载 admin-core 配置文件
- 修改
RegisterModules
方法, 注册 admin-core 模块 接入后代码如下
package main
// Generated by niuhe.idl
import (
apiViews "demo/app/api/views"
"demo/config"
"fmt"
"os"
adminBoot "github.com/ma-guo/admin-core/boot" // 第一步
"github.com/ma-guo/niuhe"
)
type baseBoot struct{}
type BaseBoot struct {
baseBoot
}
func (baseBoot) LoadConfig() error {
if len(os.Args) < 2 {
return fmt.Errorf("Usage: %s <config-path>", os.Args[0])
}
return config.LoadConfig(os.Args[1])
}
func (baseBoot) BeforeBoot(svr *niuhe.Server) {}
func (baseBoot) RegisterModules(svr *niuhe.Server) {
svr.RegisterModule(apiViews.GetModule())
}
func (baseBoot) Serve(svr *niuhe.Server) {
svr.Serve(config.Config.ServerAddr)
}
func main() {
boot := BaseBoot{}
if err := boot.LoadConfig(); err != nil {
panic(err)
}
// 第二步, 加载配置文件
admin := adminBoot.AdminBoot{}
if err := admin.LoadConfig(os.Args[1]); err != nil {
niuhe.LogInfo("admin config error: %v", err)
return
}
svr := niuhe.NewServer()
boot.BeforeBoot(svr)
// 第三步, 注册 admin-core 模块
admin.RegisterModules(svr)
boot.RegisterModules(svr)
boot.Serve(svr)
}
其他
接入后续在 src/demo
目录下运行 go mod tidy
和 go mod vendor
来下载依赖
登录验证 - 可选
引入 admin-core 后, 本项目的 api 是不会进行登录验证的, 如果需要登录验证, 还需要一些操作才行。添加认证步骤如下:
- 自定义方法需要加入 Bearea 认证的请参考 src/admincoretest/views/init.go 中的使用
- 需要修改 Bearea 认证盐请在配置文件中配置 secretkey 值
加入 Bearear 认证
在 src/demo/app/api/views/init.go
中,现代码如下:
package views
// Generated by niuhe.idl
import (
"github.com/ma-guo/niuhe"
)
var thisModule *niuhe.Module
func GetModule() *niuhe.Module {
if thisModule == nil {
thisModule = niuhe.NewModule("api")
}
return thisModule
}
将代码修改如下:
package views
// Generated by niuhe.idl
import (
coreViews "github.com/ma-guo/admin-core/app/v1/views" // 引入 admin-core 模块
"github.com/ma-guo/niuhe"
)
var thisModule *niuhe.Module
func GetModule() *niuhe.Module {
if thisModule == nil {
// thisModule = niuhe.NewModule("api")
coreViews.AddSkipUrl("/api/hellow/docs/") // 不需要认证的路径都加入到这里来
thisModule = niuhe.NewModuleWithProtocolFactoryFunc("api", func() niuhe.IApiProtocol {
return coreViews.GetProtocol() // 使用 coreViews 中定义的协议处理
})
}
return thisModule
}
总结
通过在 main.go 和 views/init.go 文件中的简单修改,即可将 admin-core 引入到项目中。
使用 API 级权限校验 - 可选
API 级校验接入后需要在后台进行配置对应权限, 同时服务端也需要进行一些简单接入。路由信息在运行 niuhe 插件生成的 API 后会自动添加到数据表中。使用权限校验需要两步处理
在 langs 中添加 route
在 src/niuhe/.config.json5
文件中的 langs
字段下添加路由信息 "route"
。配置后会在 src/{app}/app/api/protos/gen_routes.go
中生成项目定义的路由信息。
将路由信息接入加入到 protocol 中
通过以下几行代码, 可将路由信息接入到 protocol 中。
routes := []*coreProtos.RouteItem{}
for _, route := range protos.RouteItems {
routes = append(routes, &coreProtos.RouteItem{
Method: route.Method,
Path: route.Path,
Name: route.Name,
})
}
coreViews.GetProtocol().AddRoute("", routes)
接着上一节的内容, 结果 api 校验后的 views/init.go
文件内容如下:
package views
// Generated by niuhe.idl
import (
"demo/app/api/protos"
coreProtos "github.com/ma-guo/admin-core/app/v1/protos"
coreViews "github.com/ma-guo/admin-core/app/v1/views"
"github.com/ma-guo/niuhe"
)
var thisModule *niuhe.Module
func GetModule() *niuhe.Module {
if thisModule == nil {
// thisModule = niuhe.NewModule("api")
// api 级权限处理
routes := []*coreProtos.RouteItem{}
for _, route := range protos.RouteItems {
routes = append(routes, &coreProtos.RouteItem{
Method: route.Method,
Path: route.Path,
Name: route.Name,
})
}
coreViews.GetProtocol().AddRoute("", routes)
coreViews.AddSkipUrl("/api/hellow/docs/") // 不需要认证的路径都加入到这里来
thisModule = niuhe.NewModuleWithProtocolFactoryFunc("api", func() niuhe.IApiProtocol {
return coreViews.GetProtocol() // 使用 coreViews 中定义的协议处理
})
}
return thisModule
}
vue3-element-admin 项目搭建指南
简介
vue3-element-admin 是基于 Vue3 + Vite5+ TypeScript5 + Element-Plus + Pinia 等主流技术栈构建的免费开源的后台管理前端模板(配套后端源码 admin-core)
项目启动
# 克隆代码
git clone https://github.com/ma-guo/vue3-element-admin.git
# 切换目录
cd vue3-element-admin
# 安装 pnpm
npm install pnpm -g
# 安装依赖
pnpm install
# 启动运行
pnpm run dev
生成新页面代码
在 niuhe/.config.json5
配置项 langs
中添加 vite
, 结合 niuhe/.model.niuhe
配置, 在生成代码时会在 vite
目录下生成对应的 vue
文件。 将生成的文件内容复制到对应的 vue 文件中即可
自定义协议处理
本插件生成的入参和出参处理定义在 niuhe 库中, 具体代码为:
type IApiProtocol interface {
Read(*Context, reflect.Value) error
Write(*Context, reflect.Value, error) error
}
type DefaultApiProtocol struct{}
func (self DefaultApiProtocol) Read(c *Context, reqValue reflect.Value) error {
if err := zpform.ReadReflectedStructForm(c.Request, reqValue); err != nil {
return NewCommError(-1, err.Error())
}
return nil
}
func (self DefaultApiProtocol) Write(c *Context, rsp reflect.Value, err error) error {
if c._ignoreResult {
return nil
}
rspInst := rsp.Interface()
if _, ok := rspInst.(isCustomRoot); ok {
c.JSON(200, rspInst)
} else {
var response map[string]interface{}
if err != nil {
if commErr, ok := err.(ICommError); ok {
response = map[string]interface{}{
"result": commErr.GetCode(),
"message": commErr.GetMessage(),
}
if commErr.GetCode() == 0 {
response["data"] = rsp.Interface()
}
} else {
response = map[string]interface{}{
"result": -1,
"message": err.Error(),
}
}
} else {
response = map[string]interface{}{
"result": 0,
"data": rspInst,
}
}
c.JSON(200, response)
}
return nil
}
如果需要自行实现为其他协议, 可以参考上述代码进行自定义实现。
admin-core FAQ
admin-core
是一个基于 Vue3 + Element Plus 的后端管理系统模板,它提供了一套完整的后台解决方案。在使用过程中,可能会遇到一些常见问题。以下是一些常见问题的解答和解决方案。
首次登录密码问题
登录失败时会将请求的密码生成的加密字符串 log 出来,首次登录时将字符串替换到表中对应用户的密码字段即可。
OSS 路径问题
admin-core 中添加了文件存储功能, 默认为本地存储,本地存储需要在配置文件中添加 host
字段 和 fileprefix
字段, 当前 conf/demo.yaml
内容为:
serveraddr: :19999
loglevel: INFO
db:
showsql: false
debug: true
sync: true
main: user:pwd@tcp(host:port)/database_name?charset=utf8mb4
secretkey: 123456
fileprefix: xxx
host: http://localhost:19999
fileprefix
文件存储的前缀,secretkey
是bear
登录加密的salt
总结
通过以上步骤,您可以将 admin-core 引入到您的项目中,并根据实际需求进行定制化开发。希望这些信息对您有所帮助!
致谢
生成的 go 代码路由管理项目修改至 github.com/zp-server/niuhe, 在原库的基础上进行了微小修改以支持除 GET, POST
外的其他 http
方法。 入参的读取库 fork 至 github.com/ziipin-server/zpform, 而管理后台前端项目则 fork 至 https://github.com/youlaitech 。感谢这些库的作者和贡献者的无私贡献和开源精神。
相关代码库
核心库
- github.com/ma-guo/niuhe go 代码路由管理
- github.com/ma-guo/zpform 入参读取库
演示代码
- https://github.com/ma-guo/niuhe-mdbook 本教程相关演示代码
生态系统
- https://github.com/ma-guo/admin-core 管理后台后端项目
- https://github.com/ma-guo/vue3-element-admin 管理后台前端项目
- https://github.com/ma-guo/admin-core-niuhe admin-core 项目的
niuhe
IDL 定义项目
FAQ
感谢使用者, 希望在使用中提出宝贵的反馈和改进建议,下面二维码是我们的联系方式,期待您的加入。
交流群 QQ [971024252
] 点击链接加入群聊【niuhe插件支持】
niuhe IDL 语言关键字
应用相关
语法:
#app=demo
关键字 | 说明 |
---|---|
app | 通过 #app=demo 给应用命名 |
文件相关
语法:
include('api_user.niuhe')
关键字 | 说明 |
---|---|
include | 通过 include 引入定义的其他文件 |
常量组 ConstGroup 相关
语法:
class Lang(ConstGroup[,desc='', depr=True,ver='1.1'])
关键字 | 说明 | 关键字 | 说明 |
---|---|---|---|
class | 常量组开始标记 | ConstGroup | 将结构标记为常量组 |
desc | 常量组描述 | depr | 常量组标记为废弃 |
deprecated | 同 depr 将常量组标记为废弃 | ver | 声明版本 |
常量相关
语法
ZH_CN=Item(1, "中文")
或ZH_CN=Item(value=1, desc="中文", depr=True, ver='1.1')
关键字 | 说明 | 关键字 | 说明 |
---|---|---|---|
Item | 常量标记 | ConstGroup | 将结构标记为常量组 |
value | 指定常量的值 | desc | 常量组描述 |
depr | 常量组标记为废弃 | deprecated | 同 depr 将常量组标记为废弃 |
ver | 声明版本 | name | 未定义常量名时重命名 |
类相关
语法:
class User([desc='用户', depr=True, ver='1.1', used=True]
):
关键字 | 说明 | 关键字 | 说明 |
---|---|---|---|
class | 类开始标记 | Message | 将结构标记为类,可不写 |
desc | 常量组描述, 也可写为下一行的注释 | depr | 常量组标记为废弃 |
deprecated | 同 depr 将常量组标记为废弃 | ver | 声明版本 |
used | 将类标记为已被引用,确保在代码中会生成本类相关代码 |
类字段相关
语法:
name=label.type(attrs)
例子:avatar = optional.String(desc='头像', depr=True, ver='1.1')
类字段 label 部分
关键字 | 说明 | 关键字 | 说明 |
---|---|---|---|
required | 作为请求参数时本参数必填 | optional | 作为请求结构时本参数可选 |
repeated | 表示生成的成员为数组 | mapping | 表示生成的成员为 map 形式, 内部可填充任意值 |
类字段 type 部分
关键字 | 说明 | 关键字 | 说明 |
---|---|---|---|
Integer | 字段类型为 int | Long | 字段类型为 int64 |
Float | 字段类型为 float64 | Decimal | 字段类型为 float64 |
String | 字段类型为 string | Boolean | 字段类型为 boolean |
Enum | 字段类型为整数常量 | StringEnum | 字段类型为字符串常量 |
Message | 字段类型为类 | File | 字段类型文件, 读取 header 中对应的文件 |
Any | 字段类型map[string]any |
类字段 attrs 部分
关键字 | 说明 | 关键字 | 说明 |
---|---|---|---|
desc | 字段描述 | depr | 常量组标记为废弃 |
deprecated | 同 depr 将常量组标记为废弃 | ver | 声明版本 |
json | 重命令json 名 | omit | 值为空序列化时丢弃本字段 |
demo | 字段示例值 | cls | 类型为Message 时指定具体的类 |
group | 类型为 Enum/StringEnum 时指定常量组枚举 | reg | type 为 String 时验证其正则表达式 |
minlen | type 为 String 时验证其最小长度 | maxlen | type 为 String 时验证其最小长度 |
minnum | type 数字时验证其最小值 | maxnum | type 数字时验证其最大值 |
format | 指定其对应结构的 Parse 方法格式,如 format="2006-01-02" |
attrs
中reg
,minlen
,maxlen
,minnum
,maxnum
的属性请参考 zpform struct 定义
路由相关
示例
with services():
GET('某天数据', '/api/stat/day/',StatDayReq, StatDayRsp, author='xxx', status=tested)
RPC('统计充值情况', get='/api/stat/charge/',author='xxx', status=tested).args(
team_id = optional.Long(desc='id'),
).returns(
total = required.Message(cls=StatChargeItem, desc='总充值数据'),
).codes(
comm.LangEnum.ZH_CN, Item(100, '权限错误'),Item(101),
)
关键字 | 说明 | 关键字 | 说明 |
---|---|---|---|
with | 路由开始标记 | services | 路由开始标记 |
POST | http 请求为 POST | GET | http 请求为GET |
PUT | http 请求为PUT | PATCH | http 请求为PATCH |
DELETE | http 请求为DELETE | HEAD | http 请求为HEAD |
OPTIONS | http 请求为OPTIONS | RPC | RPC 方式定义路由 |
author | 指定接口责任人/作者 | status | 指定接口状态 |
RPC 定义相关
关键字 | 说明 | 关键字 | 说明 |
---|---|---|---|
url | 指定方法为GET 和POST | args | 定义请求参数 |
returns | 定义返回参数 | codes | 定义约束错误码集合 |
XORM 代码生成相关
代码示例
class Config(alias="系统配置", mode='api', niuhe=True, dao=True, service=True, model=True, vite=True):
'''系统配置表'''
name = required.String(desc='配置名称', index=True, search=True, len=255, notnull=True)
xorm 表相关
关键字 | 说明 | 关键字 | 说明 |
---|---|---|---|
class | 开始标记 | alias | 表文字说明 |
mode | 定义 CURD url 中的 mode 字段 | niuhe | 在生成对应的 niuhe 是否覆盖已存在的 niuhe 文件内容 |
dao | 在生成对应的 dao 是否覆盖已存在的 dao 文件内容 | service | 在生成对应的 service 是否覆盖已存在的 service 文件内容 |
model | 在生成对应的 model 是否覆盖已存在的 model 文件内容 | vite | 在生成对应的 vite 是否覆盖已存在的 vite 文件内容, 需在 .config.json5 中 langs 中添加 "vite"` |
xorm 字段相关
语法:
name=label.type(attrs)
label
和 type
部分同类中的字段, 这里仅列出 attrs
部分
关键字 | 说明 | 关键字 | 说明 |
---|---|---|---|
desc | 字段描述 | group | 类型为 Enum/StringEnum 时指定常量组枚举 |
json | 重命令json 名 | omit | 值为空序列化时丢弃本字段 |
demo | 字段示例值 | ver | 字段版本号 |
index | 将字段标记为索引 | len | 指定字段长度 |
notnull | 字段标记为非空 | date | 字段类型为 time.Time |
default | 指定字段默认值 | search | 字段是否出现在分页搜索参数中 |
template 模板库
用 template 生成其他语言协议 章节介绍了用模板生成目标语言的方法。为了方便大家使用、提高效率,这里收集了一些语言的模板库。使用时,大家可以根据自己的需求稍作修改就能用。
如果您有想分享的模板,希望收录到这里,可以加入 QQ 交流群:971024252
联系我们。
写模板文件时, Vscode 内联为
HTML
语言, 可获得更佳编码体验
模板列表
生成 Kotlin service 例子
.templates.tpl.js
中添加 handlebars 自定义钩子
// templates 注入 例子。 不能使用 js 高级语法。
// 插件官方说明文档: http://niuhe.zuxing.net/chapter4/section6.html
// handlebars详细例子参考 https://handlebarsjs.com/zh/
handlebars.registerHelper("convertKtType", function (type) {
const typeMap = {
string: "String",
stringenum: "String",
number: "Int",
float: "Float",
double: "Double",
int: "Int",
long: "Long",
enum: "Int",
integer: "Int",
boolean: "Boolean",
object: "Any",
array: "List<Any>",
map: "Map<String, Any>",
};
return typeMap[type] || type || "--";
});
// 大写
handlebars.registerHelper("upperCase", function (args) {
if(!args) {
return args;
}
return args.toUpperCase();
});
handlebars.registerHelper("formType", function (method) {
if(method === "POST" || method === "post") {
return "@Field";
}
return "@Query";
});
handlebars.registerHelper("shouldUrlEncode", function (method) {
if(method === "POST" || method === "post") {
return this.req && this.req.fields.length>0;
}
return false;
});
template 编写
package your_package
import your_package.model.*
import retrofit2.Response
import retrofit2.http.Field
import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST
import retrofit2.http.GET
import retrofit2.http.Query
{{!-- 模板使用到的自定义钩子: convertKtType, shouldEncode, formType, upperCase }} --}}
interface ApiService {
{{#each items}}
{{#if desc}}
/** {{desc}} */
{{/if}}
{{#if (shouldUrlEncode method)}}
@FormUrlEncoded
{{/if}}
@{{upperCase method}}("{{url}}")
suspend fun {{name}}(
{{#each req.fields}}
{{#if desc}}
/** {{desc}} */
{{/if}}
{{#if (isArray label)}}
{{formType ../method}}("{{ json }}") {{ name }}: List<{{ convertKtType type }}>,
{{else}}
{{formType ../method}}("{{ json }}") {{ name }}: {{ convertKtType type }},
{{/if}}
{{/each}}
): Response<Rsp<{{rsp.name}}>>
{{/each}}
}
.config.json5 参考配置
{
templates: [{
modes: ["api"],
template: "./templates/kt_route.tpl",
type: "route",
output: "./output/ApiService.kt",
}]
}
生成 Kotlin model 例子
.templates.tpl.js
中添加 handlebars 自定义钩子
// templates 注入 例子。 不能使用 js 高级语法。
// 插件官方说明文档: http://niuhe.zuxing.net/chapter4/section6.html
// handlebars详细例子参考 https://handlebarsjs.com/zh/
handlebars.registerHelper("convertKtType", function (type) {
const typeMap = {
string: "String",
stringenum: "String",
number: "Int",
float: "Float",
double: "Double",
int: "Int",
long: "Long",
enum: "Int",
integer: "Int",
boolean: "Boolean",
object: "Any",
array: "List<Any>",
map: "Map<String, Any>",
};
return typeMap[type] || type || "--";
});
handlebars.registerHelper("ktClass", function (fields) {
if (!fields || fields.length == 0) {
return "class";
}
return "data class";
});
handlebars.registerHelper("shouldSerial", function () {
return this.name !== this.json
});
template 内容
model.tpl 需要生成请求和返回以及引用到的文件定义。 you_package 需修改为你的目标包名
package your_package
import kotlinx.serialization.SerialName
import com.google.gson.annotations.SerializedName
{{!-- 模板使用到的自定义钩子: ktClass, convertKtType, isArray(内置),shouldSerial }} --}}
data class Rsp<T>(
val result: Int,
val data: T?,
val message: String?,
) {
/** 请求成功 */
fun isSuccess(): Boolean {
return result == 0
}
}
{{#each items}}
{{#if desc}}
/** {{desc}} */
{{/if}}
{{ktClass fields}} {{name}}(
{{#each fields}}
{{#if desc}}
/** {{desc}} */
{{/if}}
{{#if (shouldSerial)}}
@SerializedName("{{ json }}")
@SerialName("{{ json }}")
{{/if}}
{{#if (isArray label)}}
val {{ name }}: List<{{ convertKtType type }}>,
{{else}}
val {{ name }}: {{ convertKtType type }},
{{/if}}
{{/each}}
)
{{/each}}
.config.json5 参考配置
{
templates: [{
modes: ["api"],
template: "./templates/kt_message.tpl",
type: "message",
output: "./output/Models.kt",
}]
}
生成 Kotlin enum 例子
.templates.tpl.js
中添加 handlebars 自定义钩子
// templates 注入 例子。 不能使用 js 高级语法。
// 插件官方说明文档: http://niuhe.zuxing.net/chapter4/section6.html
// handlebars详细例子参考 https://handlebarsjs.com/zh/
handlebars.registerHelper("isInteger", function () {
return this.type === "integer";
});
handlebars.registerHelper("delimiter", function (fields, index) {
if(fields && fields.length-1 == index) {
return ";";
}
return ",";
});
template 内容
enum.tpl 需要生成请求和返回以及引用到的文件定义。 you_package 需修改为你的目标包名
package your_package
// Generated by niuhe.idl
// 此文件由 niuhe.idl 自动生成, 请勿手动修改
{{!-- 模板使用到的自定义钩子: isInteger, delimiter }} --}}
{{#each items}}
{{#if desc}}
/** {{desc}} */
{{/if}}
{{#if (isInteger)}}
enum class {{name}}(val value: Int, val desc: String) {
{{#each fields}}
/** {{desc}} */
{{ name }}({{value}}, "{{desc}}"){{delimiter ../fields @index}}
{{/each}}
companion object {
fun fromValue(value: Int): {{name}}? {
return values().find { it.value == value }
}
}
}
{{else}}
enum class {{name}}(val value: String, val desc: String) {
{{#each fields}}
/** {{desc}} */
{{ name }}("{{value}}", "{{value}}"){{delimiter ../fields @index}}
{{/each}}
companion object {
fun fromValue(value: String): {{name}}? {
return values().find { it.value == value }
}
}
}
{{/if}}
{{/each}}
.config.json5 参考配置
{
templates: [{
modes: ["api"],
template: "./templates/kt_enum.tpl",
type: "enum",
output: "./output/Enums.kt",
}]
}
生成代码例子
niuhe 定义
class StateEnum(ConstGroup):
'''状态'''
NORMAL = Item(1, '正常')
DISABLED = Item(2, '禁用')
class AppEnum(ConstGroup):
'''应用'''
WEIXIN = Item("weixin", "微信")
QYWX = Item("qywx", "企业微信")
kotlin 生成结果
/** 状态 */
enum class StateEnum(val value: Int, val desc: String) {
/** 正常 */
NORMAL(1, "正常"),
/** 禁用 */
DISABLED(2, "禁用");
companion object {
fun fromValue(value: Int): StateEnum? {
return values().find { it.value == value }
}
}
}
/** 应用 */
enum class AppEnum(val value: String, val desc: String) {
/** 微信 */
WEIXIN("weixin", "微信"),
/** 企业微信 */
QYWX("qywx", "企业微信");
companion object {
fun fromValue(value: String): AppEnum? {
return values().find { it.value == value }
}
}
}
生成 typescript enum 例子
生成 ts 常量不需要注入 handlebars 钩子, 下面常量模板
// Generated by niuhe.idl
// 此文件由 niuhe.idl 自动生成, 请勿手动修改
{{#each items}}
/** {{desc}} */
declare enum {{name}} {
{{#each fields}}
/** {{desc}} */
{{ name }} = {{value}},
{{/each}}
}
{{/each}}
.config.json5 参考配置
{
templates: [{
modes: ["api"],
template: "./templates/ts_enums.tpl",
type: "enum",
output: "./output/enums.d.ts",
}]
}
生成 typesctipy api 请求 例子
./request 为自定义的 api 工具类, 可在 langs
中添加"ts"
后生成一个参考例子。
template 编写
// Generated by niuhe.idl
// 此文件由 niuhe.idl 自动生成, 请勿手动修改
import { ajax_get, ajax_post, ajax_any } from "./request";
{{#each items}}
{{# if req}}
/**
* {{desc}}
* @path {{method}} {{url}}
* @return {{capitalize ../mod}}.{{rsp.name}}
{{#if codes}}
* @codes 错误码如下
{{#each codes}}
* - {{name}}({{value}}) {{desc}}
{{/each}}
{{/if}}
*/
export const {{name}} = (data: {{capitalize ../mod}}.{{req.name}}): Promise<Rsp<{{capitalize ../mod}}.{{rsp.name}}>> => {
return ajax_any("{{method}}", "{{url}}", data);
}
{{/if}}
{{/each}}
.config.json5 参考配置
{
templates: [{
modes: ["api"],
template: "./templates/ts_route.tpl",
type: "route",
output: "./output/route.ts",
}]
}
生成 typescript interface 例子
.templates.tpl.js
中添加 handlebars 自定义钩子
// templates 注入 例子。 不能使用 js 高级语法。
// 插件官方说明文档: http://niuhe.zuxing.net/chapter4/section6.html
// handlebars详细例子参考 https://handlebarsjs.com/zh/
handlebars.registerHelper('convertType', function (type) {
const typeMap = {
'string': 'string',
'stringenum': 'string',
'number': 'number',
'float': 'number',
'double': 'number',
'int': 'number',
'long': 'number',
'enum': 'number',
'integer': 'number',
'boolean': 'boolean',
'object': 'any',
'array': 'any[]',
'map': 'any',
};
return typeMap[type] || type;
});
template 编写
// Generated by niuhe.idl
// 此文件由 niuhe.idl 自动生成, 请勿手动修改
interface Rsp<T> {
result: number
data: T
message: string
}
declare namespace {{capitalize mod}} {
{{#each items}}
{{#if desc}}
/** {{desc}} */
{{/if}}
interface {{name}} {
{{#each fields}}
{{#if desc}}
/** {{desc}} */
{{/if}}
{{#if (isArray label)}}
{{ json }}: {{ convertType type }}[],
{{else}}
{{ json }}: {{ convertType type }},
{{/if}}
{{/each}}
}
{{/each}}
}
.config.json5 参考配置
{
templates: [{
modes: ["api"],
template: "./templates/ts_messages.tpl",
type: "message",
output: "./output/messages.d.ts",
}]
}