文章内容更新请以 WGrape GitHub博客 : 基于数据Mock的接口治理方案设计与实现 为准
前言
本文原创,著作权归WGrape所有,未经授权,严禁转载
一、背景
在《关于接口文档高效治理方案的研究和思考》一文中已经详细介绍了高效管理接口文档的重要性,更多内容可以阅读原文。这里就不再解释为什么需要,而是重点关注怎么做。
在本文接下来的内容中,会为大家分享一种轻量级的创新型方案,它不但可以简单高效实现接口文档的自动生成,而且可以满足前端人员Mock接口的需要。这就是基于数据Mock的接口治理方案,请慢慢往下看,大约会花费8分钟的时间。
二、目标与调研
1、自动生成接口文档
接口治理的第一个目标就是实现自动生成接口文档。后端人员只需在开发接口的同时按照规范编写相应逻辑,即可在CI阶段自动生成接口文档。
2、满足Mock接口的需要
接口治理的第二个目标就是满足前端人员Mock接口的需要。无论是开发、联调、测试、上线后的哪一个阶段,前端都可以根据自己的需要,随时Mock后端的接口。
3、设计调研
(1) 接口文档的组成
我们知道,无论是什么类型的接口文档,其必须具备以下组成元素
- 接口地址 :调用的接口地址
- 接口参数 :参数名称、类型、含义
- 接口返回 :接口的返回结构,包括每个字段的含义
不过有些信息比如参数is_new
表示是否为新用户,这些信息都是属于程序无法自动获取的自定义内容,它们还是需要人力提供的,只是提供的方式可能有所不同。
比如在各大接口文档自动生成工具中,都是通过注解的方式提供,如下所示
(2) 接口文档的托管
一般情况下,自动生成接口文档的平台会提供私有部署方案,接口文档就会被托管在私有的服务器上。这种方式看起来很方便,但是部署和维护的成本也都是不可忽视的。能不能不依赖托管服务器呢 ?
经过调研发现Gitlab Wiki
功能可以符合预期要求
- 不需要托管服务器
- 通过提供的API可以快速实现自动更新
三、什么是数据Mock
Mock直译为伪造,表示虚假的含义。在计算机技术中,数据Mock指通过伪造数据,实现预期目标。它最广泛的应用领域主要在接口Mock中。如下代码所示,通过返回接口虚假数据,让接口处于可用状态,方便前端调试调用
func GetUser() *model.RespBase {
response := model.RespBase{
Code: 0,
Data: model.QueryGetUserResp{
Uid: 88328876,
Name: "Peter",
Age: 45,
Gender: 1,
City: "New York",
},
}
return &response
}
四、方案设计
1、设计思想
基于数据Mock,实现对接口的治理
2、接口文档抽象
在第二节《目标与调研》中我们分析了接口文档的组成,那么自动生成接口文档的第一步,就是需要先把接口文档抽象到程序中。
在如下程序中,定义的APIMock
结构体即为一个接口的接口文档的抽象,也就是说整个接口文档会由[]APIMock
组成,即多个APINock
组成。
// RequestExplainItem 请求参数释义
type RequestExplainItem struct {
Field string `json:"field"`
Type string `json:"type"`
IsMust bool `json:"is_must"`
Mark string `json:"mark"`
}
// ResponseExplainItem 响应结构释义
type ResponseExplainItem struct {
Field string `json:"field"`
Type string `json:"type"`
Mark string `json:"mark"`
}
// APIMock 接口文档抽象结构
type APIMock struct {
Title string `json:"title"`
Description string `json:"description"`
Route string `json:"route"`
Request interface{} `json:"request"`
RequestExplain []RequestExplainItem `json:"request_explain"`
Response interface{} `json:"response"`
ResponseExplain []ResponseExplainItem `json:"response_explain"`
}
举个例子,如果开发了一个/api/get_user
接口,那么创建的APIMock
对象如下所示
apiMock := apimock.APIMock{
Title: "获取用户接口",
Description: "获取用户接口",
Route: "/api/get_user",
Request: request,
RequestExplain: []apimock.RequestExplainItem{
{
Field: "uid",
Type: "int",
IsMust: true,
Mark: "用户id",
},
}
Response: response,
ResponseExplain: []apimock.ResponseExplainItem{
{
Field: "data.uid",
Type: "int",
Mark: "用户ID",
},
{
Field: "data.name",
Type: "string",
Mark: "用户昵称",
},
},
}
3、Request对象和Response对象
在APIMock
的对象结构体中,有一个Request
对象和一个Response
对象。这两个对象是对接口请求参数和响应结构的抽象。 为什么需要定义这两个对象呢,下面会从两个方面来阐述原因
(1) 作为接口定义的一部分
接口定义是指定义接口的路由,请求参数和响应结构。 在PHP等弱类型语言中,接口定义这个概念的应用不多,因为所有接口参数和响应结构用$array = array()
这样的数组类型就能满足需要。但是对于Go等强类型语言来说,接口定义是一个接口的必要组成。
// QueryGetUserReq 接口请求参数结构
type QueryGetUserReq struct {
Uid int `json:"uid"`
}
// QueryGetUserReq 接口请求响应结构
type QueryGetUserResp struct {
Uid int `json:"uid"`
Name string `json:"name"`
}
(2) 作为接口文档的一部分
很多情况下,接口的请求参数和响应结构都是比较复杂的嵌套结构(如下图所示)。在使用Wiki维护接口文档的时代,经常需要靠人力去编写和修改这些结构,这是一件非常不友好且极其低效率的事情。
为了解决这个问题,Request
对象和Response
对象的重要性在此就体现出来了,直接使用json.Marshal()
编码就能获取到接口文档中的这些请求参数和响应结构。
4、Mock请求与非Mock请求
如果当前接口请求地址中有mock_app
和mock_token
这两个参数且校验通过,则说明这个请求为Mock请求。
CURL -X GET 'https://test.api.com/api/get_user?mock_app=test&mock_token=avhsuekfiabs'
(1) 是Mock请求
如果当前请求是Mock请求,那么调用Mock逻辑返回Mock数据。如下所示直接New
一个自定义的MockResponse
对象返回即可。
func returnMockGetUser() Response {
response := model.RespBase{
Code: 0,
Data: model.QueryGetUserResp{
Uid: 88328876,
Name: "Peter",
},
}
}
(2) 不是Mock请求
如果当前请求不是Mock请求,那么执行正常业务逻辑,返回正常业务数据。
5、生成Markdown格式文档
在获取到[]APIMock
结构后,通过循环遍历拼接Markdown
格式的方式,即可在代码仓库下生成自定义结构的接口文档。
// 生成文档内容
for index, apiMock = range apiMockList {
// 接口描述部分
contentList = append(contentList, "\n### (1) Description\n")
contentList = append(contentList, apiMock.Description+"\n")
// 接口地址部分
contentList = append(contentList, "\n### (2) URL\n")
contentList = append(contentList, apiMock.Route+"\n")
}
// 写入apidoc.md文件
var content = strings.Join(contentList, "")
os.WriteFile("apidoc.md", result, 0666)
6、自动更新至Gitlab
在本地生成自定义结构的接口文档后,把它更新至Gitlab会包括两部分,第一是指随着代码的提交而更新Gitlab仓库中的apidoc.md
文件,第二是指通过Rest API更新Gitlab Wiki
。
这样无论是仓库中的apidoc.md
文件,还是Gitlab Wiki
,都可以自由的选择使用。
7、集成至CI
如果项目支持CI,可以非常方便的把接口文档自动生成和更新至Gitlab的这两个逻辑都集成至CI中,可以参考CIManager项目的.gitlab/apidoc_gen.sh
8、总结
简单总结下,这种设计方案主要包括4个关键点
- 根据定义的
APIMock/Request/Response
等对象,巧妙利用Json解析等技术自动生成文接口档 - 定义的
Response
还可以用来作为Mock接口的返回 - 通过CI把接口文档自动更新至Gitlab
- 参数控制是Mock请求还是真实请求
五、实现过程
本设计方案的实现过程,请实现请参考项目apimock。假设现在你有一个项目,目录结构如下所示,或直接查看原项目代码
其中,所有接口的基础返回结构只有code/data/is_mock
三个字段
type RespBase struct {
Code int `json:"code"`
Data interface{} `json:"data"`
IsMock bool `json:"is_mock"`
}
现在需要新增一个/api/get_user
接口,如何在你的项目中使用此方案呢 ?
六、如何使用
1、填充Handler
首先找到Handler文件/server/handler.go
,然后在接口对应的Handler中判断当前请求是否为Mock请求,如果是Mock请求,则返回Mock数据,如调用mock.GetUser()
方法。否则就调用正常的业务逻辑,如调用service.GetUser(uidInt)
方法。
func GetUserHandler(w http.ResponseWriter, r *http.Request) {
var isMock = mock.IsMockRequest(r)
uidString := r.URL.Query().Get("uid")
uidInt, err := strconv.ParseInt(uidString, 10, 64)
if err != nil {
ResponseError(w, isMock, 100)
return
}
if isMock {
ResponseOk(w, isMock, mock.GetUser())
return
} else {
ResponseOk(w, isMock, service.GetUser(uidInt))
return
}
}
2、定义接口的请求和响应结构体
在定义mock.GetUser()
函数前,需要先在/model/model.go
文件中定义接口的请求结构体和响应结构体,如下所示。
type QueryGetUserReq struct {
Uid int64 `json:"uid"`
}
type QueryGetUserResp struct {
Uid int64 `json:"uid"`
Name string `json:"name"`
Age uint8 `json:"age"`
Gender uint8 `json:"gender"`
City string `json:"city"`
}
3、定义mock.GetUser()函数
在定义完接口的请求和响应结构体后,先在根目录下创建/mock/mock.go
文件,之后就可以开始按照如下步骤,在/mock/mock.go
文件中定义mock.GetUser()
函数了。
func GetUser() *apimock.APIMock {
// 1、创建请求对象
request := model.QueryGetUserReq{
Uid: 87654321,
}
// 2、创建响应对象
response := model.RespBase{
Code: 0,
Data: model.QueryGetUserResp{
Uid: 88328876,
Name: "Peter",
Age: 45,
Gender: 1,
City: "New York",
},
}
// 3、创建apiMock对象
apiMock := apimock.APIMock{
// 接口名称
Title: "获取用户接口",
// 接口描述
Description: "获取用户接口",
// 接口路由
Route: "/api/get_user",
// 请求对象和请求对象的字段释义
Request: request,
RequestExplain: []apimock.RequestExplainItem{
{
Field: "uid", // 字段名称
Type: "int", // 字段类型
IsMust: true, // 是否必填
Mark: "用户id", // 字段备注
},
},
// 响应对象和响应对象的字段释义
Response: response,
ResponseExplain: []apimock.ResponseExplainItem{
{
Field: "data.uid",
Type: "int",
Mark: "用户ID",
},
{
Field: "data.name",
Type: "string",
Mark: "用户昵称",
},
{
Field: "data.age",
Type: "int",
Mark: "用户年龄",
},
{
Field: "data.gender",
Type: "int",
Mark: "用户性别, 1女, 2男",
},
{
Field: "data.city",
Type: "string",
Mark: "用户所在城市",
},
},
}
return &apiMock
}
4、定义TestAPIDocGen函数
在mock
目录下创建mock.go
文件对应的/mock/mock_test.go
单元测试文件,然后定义TestAPIDocGen()
函数
func TestAPIDocGen(t *testing.T) {
var apiMockList []*apimock.APIMock
// 生成所有的apiMock,并生成Markdown格式的字符串文档
apiMock := GetUser()
apiMockList = append(apiMockList, apiMock)
result, err := apimock.GenerateAPIDocument(apimock.APIDDocumentFormatMarkdown, apiMockList)
if err != nil {
t.Fail()
return
}
// 文档写入至本地文件中
if err = os.WriteFile("../apidoc.md", result, 0666); err != nil {
t.Fail()
return
}
}
这样,使用go test
时即可自动在本地生成apidoc.md
接口文档了。
5、完成
恭喜,到这一步已经完成了所有需要的操作。你可以选择自己执行/mock/mock_test.go
中的TestAPIDocGen()
函数更新接口文档,也可以选择由CI自动完成Gitlab Wiki
的更新
七、总结
本文主要设计并实现了一种基于数据Mock的接口治理方案,不但解决了接口文档自动生成的问题,而且还可以满足前端人员的Mock接口需求。值得一提的是,这种设计更是一种通用的解决方案,它不局限于Go语言,在其他语言中也可以适用,非常简洁,通过轻量的方式即可实现。