Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

基于数据Mock的接口治理方案设计与实现 #233

Closed
WGrape opened this issue Aug 1, 2022 · 1 comment
Closed

基于数据Mock的接口治理方案设计与实现 #233

WGrape opened this issue Aug 1, 2022 · 1 comment

Comments

@WGrape
Copy link
Owner

WGrape commented Aug 1, 2022

前言

本文原创,著作权归WGrape所有,未经授权,严禁转载

一、背景

《关于接口文档高效治理方案的研究和思考》一文中已经详细介绍了高效管理接口文档的重要性,更多内容可以阅读原文。这里就不再解释为什么需要,而是重点关注怎么做。

在本文接下来的内容中,会为大家分享一种轻量级的创新型方案,它不但可以简单高效实现接口文档的自动生成,而且可以满足前端人员Mock接口的需要。这就是基于数据Mock的接口治理方案,请慢慢往下看,大约会花费8分钟的时间。

二、目标与调研

1、自动生成接口文档

接口治理的第一个目标就是实现自动生成接口文档。后端人员只需在开发接口的同时按照规范编写相应逻辑,即可在CI阶段自动生成接口文档。

2、满足Mock接口的需要

接口治理的第二个目标就是满足前端人员Mock接口的需要。无论是开发、联调、测试、上线后的哪一个阶段,前端都可以根据自己的需要,随时Mock后端的接口。

3、设计调研

(1) 接口文档的组成

我们知道,无论是什么类型的接口文档,其必须具备以下组成元素

  • 接口地址 :调用的接口地址
  • 接口参数 :参数名称、类型、含义
  • 接口返回 :接口的返回结构,包括每个字段的含义

不过有些信息比如参数is_new表示是否为新用户,这些信息都是属于程序无法自动获取的自定义内容,它们还是需要人力提供的,只是提供的方式可能有所不同。

比如在各大接口文档自动生成工具中,都是通过注解的方式提供,如下所示

image

(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()编码就能获取到接口文档中的这些请求参数和响应结构。

image

4、Mock请求与非Mock请求

如果当前接口请求地址中有mock_appmock_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,都可以自由的选择使用。

image

截屏2022-08-26 11 21 27

7、集成至CI

如果项目支持CI,可以非常方便的把接口文档自动生成和更新至Gitlab的这两个逻辑都集成至CI中,可以参考CIManager项目的.gitlab/apidoc_gen.sh

8、总结

简单总结下,这种设计方案主要包括4个关键点

  • 根据定义的APIMock/Request/Response等对象,巧妙利用Json解析等技术自动生成文接口档
  • 定义的Response还可以用来作为Mock接口的返回
  • 通过CI把接口文档自动更新至Gitlab
  • 参数控制是Mock请求还是真实请求

五、实现过程

本设计方案的实现过程,请实现请参考项目apimock。假设现在你有一个项目,目录结构如下所示,或直接查看原项目代码

image

其中,所有接口的基础返回结构只有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接口需求。

  • 根据定义的APIMock/Request/Response等对象,巧妙利用Json解析等技术自动生成文接口档
  • 定义的Response还可以用来作为Mock接口的返回
  • 通过CI把接口文档自动更新至Gitlab
  • 参数控制是Mock请求还是真实请求

更值得一提的是,这种设计更是一种通用的解决方案,它不局限于Go语言,在其他语言中也可以适用,非常简洁,通过轻量的方式即可实现。

@WGrape
Copy link
Owner Author

WGrape commented Aug 26, 2022

简单总结下,就是可以实现如下功能

  • 根据定义的APIMock/Request/Response等对象,巧妙利用Json解析等技术自动生成文接口档
  • 定义的Response还可以用来作为Mock接口的返回
  • 通过CI把接口文档自动更新至Gitlab
  • 参数控制是Mock请求还是真实请求

@WGrape WGrape closed this as completed Aug 26, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant