likes
comments
collection
share

再也不想写接口文档啦

作者站长头像
站长
· 阅读数 11

前言

我有一个朋友,他叫李晓得。在一个阳光明媚的下午,忙碌了半天的李师傅坐在电脑面前眉头紧皱。隔壁B哥拍了拍他的肩膀说:“三点几啦,饮茶先啦。”,李晓得无奈得转了转头:“接口文档还没写完呢,前端又要开始催啦。”B哥:“有没有搞错,赶紧得,麻溜得,不要耽误下午的摸鱼时光。”,李晓得:“......”

快速生成接口文档

都3202年了,不会还要用Word来写接口文档吧,写完太阳都下山了。先不说耗费大量精力调格式,最重要的是每次修改都要重新给前端开发提交一份,前端开发光是文档都要塞满磁盘了好吧!

好消息!好消息!3202年市场上早就存在基于标准的OpenAPI规范进行设计的接口文档生成工具了,那就是Swagger。

那就简单了,就从安装swagger开始吧(以go语言的gin框架生成为例,go的版本为1.17版本以上)。

使用下面的命令下载swag:

go install github.com/swaggo/swag/cmd/swag@v1.8.11 //该文章编写时latest版本为@v1.8.12,由于12版本存在一些错误,故采用11版本

然后验证是否安装成功:

$ swag -v
swag version v1.8.12

现在我们存在一个简单的gin项目,项目目录如下:

|-internal
    |-middleware
        |-middleware.go
    |-routes
        |-api
            |-v1
                |-article.go
        |-router.go
|-main.go
|-go.mod

article.go存放的是我们接口controller的逻辑代码,我们在此写入swagger注解:

// internal/routes/api/v1/article.go 关于注解格式,这里只展示比较常用的注解说明,详细可以查看github上的文档说明:https://github.com/swaggo/swag/blob/master/README_zh-CN.md#api%E6%93%8D%E4%BD%9C
// @Tags 每个API操作的标签列表,以逗号分隔。
// @Summary 摘要
// @Accept API可以使用的MIME类型的列表。比如:JSON。请注意,Accept 仅影响具有请求正文的操作,例如 POST、PUT 和 PATCH。
// @Produce API可以生成的MIME类型的列表。可以简单理解为响应类型,比如:JSON
// @Param 参数格式。空格分隔,从左到右依次为:参数名 入参类型 数据类型 是否必填 注释
// @Success 成功响应。空格分隔,从左到右依次为:状态码 参数类型 数据类型 注释
// @Failure 故障响应。空格分隔,从左到右依次为:状态码 参数类型 数据类型 注释
// @Router 路由定义。空格分隔,从左到右依次为:路径 [http方法]

package v1

import "github.com/gin-gonic/gin"

type Article struct{}

type JSONResult struct {
    Code    int         `json:"code"`    //状态码
    Message string      `json:"message"` //状态信息
    Data    interface{} `json:"data"`    //数据
}

type InfoRes struct {
    Id         int    `json:"id"`          //文章id
    Title      string `json:"title"`       //文章标题
    Author     string `json:"author"`      //作者
    Content    string `json:"content"`     //文章内容
    CreateTime string `json:"create_time"` //创建时间
    UpdateTime string `json:"update_time"` //更新时间
}

type CreateParam struct {
    Title   string `json:"title"`   //文章标题
    Author  string `json:"author"`  //作者
    Content string `json:"content"` //文章内容
}

type CreateRes struct {
    Id int `json:"id"` //文章id
}

type UpdateParam struct {
    Title   string `json:"title"`   //文章标题
    Author  string `json:"author"`  //作者
    Content string `json:"content"` //文章内容
}

type UpdateRes struct {
    Id int `json:"id"` //文章id
}

type DeleteRes struct {
    Id int `json:"id"` //文章id
}

// Info
// @Tags 文章管理
// @Summary  获取单篇文章
// @Produce json
// @Param id path int true "文章id"
// @Success 200 {object} JSONResult{data=InfoRes} "响应内容"
// @Router /api/v1/articles/{id} [get]
func (a Article) Info(c *gin.Context) {}

// Create
// @Tags 文章管理
// @Summary  创建文章
// @Accept json
// @Produce json
// @Param data body CreateParam true "请求参数"
// @Success 200 {object} JSONResult{data=CreateRes} "响应内容"
// @Router /api/v1/articles [post]
func (a Article) Create(c *gin.Context) {}

// Update
// @Tags 文章管理
// @Summary  更新单篇文章
// @Produce json
// @Param id path int true "文章id"
// @Param data body UpdateParam true "请求参数"
// @Success 200 {object} JSONResult{data=UpdateRes} "响应内容"
// @Router /api/v1/articles/{id} [put]
func (a Article) Update(c *gin.Context) {}

// Delete
// @Tags 文章管理
// @Summary  删除单篇文章
// @Produce json
// @Param id path int true "文章id"
// @Success 200 {object} JSONResult{data=DeleteRes} "响应内容"
// @Router /api/v1/articles/{id} [delete]
func (a Article) Delete(c *gin.Context) {}

还可以从main.go编写针对项目的注释:

// main.go 

// @title 文章系统
// @version 1.0
// @description 一个简单的文章demo
func main() {
    //...
}

这个时候就可以在项目根目录执行下面的命令生成doc文件了:

swag init

执行后会发现根目录下的docs目录下创建了docs.go、swagger.json及swagger.yaml文件了。

最后,在/internal/routes/router.go中加入swagger路由:

package routes

import (
    "demo/internal/middleware"
    v1 "demo/internal/routes/api/v1"
    "github.com/gin-gonic/gin"
    swaggerFiles "github.com/swaggo/files"
    ginSwagger "github.com/swaggo/gin-swagger"

    _ "demo/docs"
)

func InitRoute() *gin.Engine {
    router := gin.Default()
    router.Use(gin.Recovery())
    router.Use(middleware.CorsMiddleware())

    apiV1 := router.Group("/api/v1")
    {
        article := new(v1.Article)
        {
            apiV1.GET("/articles/:id", article.Info)
            apiV1.POST("/articles", article.Create)
            apiV1.PUT("/articles/:id", article.Update)
            apiV1.DELETE("/articles/:id", article.Delete)
        }
    }
    router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
    return router
}

大功告成!运行项目,在浏览器中输入http://127.0.0.1:8080/swagger/index.html,就可以从网页上看到生成好的接口文档啦!前端开发看到直呼内行!

再也不想写接口文档啦

B哥:"厉害!厉害!不过你不会忘记我们是用yapi作为接口文档工具吧,你这样做不合群,离进入下一批毕业名单不远啦。"

李晓得:“过分啦,为啥世界上就不能只有一种接口文档工具。”

B哥:“如果有,那一定是收费的。”

李晓得:“.......”

或许可以尝试一下用YAPI?

没关系,YAPI完全兼容swagger文档,完全可以利用yapi提供的接口自动上传啊!

再也不想写接口文档啦

很好,现在开始编写一个函数实现swagger数据上传:

func YAPISync() (err error) {
    b, err := os.ReadFile("./docs/swagger.json")
    if err != nil {
        return
    }
    url := "http://127.0.0.1:3001/api/open/import_data"
    contentType := "application/x-www-form-urlencoded"
    client := &http.Client{Timeout: 5 * time.Second}
    //这里token是yapi空间中的token 注意,需要使用参数merge=good(智能合并模式)
    resp, err := client.Post(url, contentType,
        strings.NewReader("type=swagger&merge=good&token=d8e8f5a7faa79ac7ed0fb112597684bf1151312a18057c740f1d4347e625f735&json="+string(b)))
    if err != nil {
        return
    }
    defer resp.Body.Close()
    return
}

执行函数,可以发现swagger接口已经完美同步到了yapi中:

再也不想写接口文档啦

再来尝试一下接口修改,修改一下article.go中的参数,增加封面参数返回:

type InfoRes struct {
    Id         int    `json:"id"`          //文章id
    Title      string `json:"title"`       //文章标题(new)
    Author     string `json:"author"`      //作者
    Cover      string `json:"cover"`       //封面
    Content    string `json:"content"`     //文章内容
    CreateTime string `json:"create_time"` //创建时间
    UpdateTime string `json:"update_time"` //更新时间
}

这时候由于前面使用的merge是智能合并模式,yapi已经自动识别到修改了哪里,同步给前端开发的信息就一目了然啦:

再也不想写接口文档啦 B哥:”你作弊!你这完全是靠yapi给的智能合并模式来检查你的接口改动,不行,下周我就申请换接口文档工具,看你怎么办!“

李晓得:”怎么办,凉拌!“

有好用的工具不用,偏偏要选择难用的?

没有智能合并,难道就没法察觉到接口的改动了吗?或许可以尝试造一下轮子? (这不是正常的想法

或许可以改变一下思路,swaggo本身就实现了swagger的解析与输出,是否可以借用它的实现来做自定义?

来到github.com/swaggo/swag/cmd/main.go目录,先来了解一下swag init是怎么实现的:

//swag init的调用方法
func initAction(ctx *cli.Context) error {
    // 略
    return gen.New().Build(&gen.Config{
        //...
    })
}


// Build builds swagger json file  for given searchDir and mainAPIFile. Returns json.
func (g *Gen) Build(config *Config) error {
    // 略
    p := swag.New(
        swag.SetParseDependency(config.ParseDependency),
        swag.SetMarkdownFileDirectory(config.MarkdownFilesDir),
        swag.SetDebugger(config.Debugger),
        swag.SetExcludedDirsAndFiles(config.Excludes),
        swag.SetParseExtension(config.ParseExtension),
        swag.SetCodeExamplesDirectory(config.CodeExampleFilesDir),
        swag.SetStrict(config.Strict),
        swag.SetOverrides(overrides),
        swag.ParseUsingGoList(config.ParseGoList),
        swag.SetTags(config.Tags),
        swag.SetCollectionFormat(config.CollectionFormat),
    )
    if err := p.ParseAPIMultiSearchDir(searchDirs, config.MainAPIFile, config.ParseDepth); err != nil {
        return err
    }
    // swagger对象
    swagger := p.GetSwagger()
    // 创建目录
    if err := os.MkdirAll(config.OutputDir, os.ModePerm); err != nil {
        return err
    }
    // 输出到文件
    for _, outputType := range config.OutputTypes {
        outputType = strings.ToLower(strings.TrimSpace(outputType))
        if typeWriter, ok := g.outputTypeMap[outputType]; ok {
            if err := typeWriter(config, swagger); err != nil {
                return err
            }
        } else {
            log.Printf("output type '%s' not supported", outputType)
        }
    }

    return nil
}

很好,这里已经找到了需要的swagger对象,既然是从swagger对象写入文件,那么从文件就可以读取到对象中,再尝试读取需要的信息:

type CoreInfo struct {
    Title       string `json:"title"`       //标题
    Description string `json:"description"` //描述
    Version     string `json:"version"`     //版本
    Paths       []Path `json:"paths"`       //api接口
}
type Path struct {
    Url        string           `json:"url"`        //路径
    Method     string           `json:"method"`     //http方法
    Consumes   []string         `json:"consumes"`   //请求类型
    Produces   []string         `json:"produces"`   //响应类型
    Tags       []string         `json:"tags"`       //标签
    Summary    string           `json:"summary"`    //描述
    Parameters []spec.Parameter `json:"parameters"` //请求参数
    Responses  *spec.Responses  `json:"responses"`  //返回
}

func OtherSync() (err error) {
    b, err := os.ReadFile("./docs/swagger.json")
    if err != nil {
        return
    }
    var actual spec.Swagger
    err = json.Unmarshal(b, &actual)
    if err != nil {
        return
    }
    var info CoreInfo
    if actual.Info != nil {
        info.Title = actual.Info.Title
        info.Description = actual.Info.Description
        info.Version = actual.Info.Version
    }
    var paths = make([]Path, 0)
    for k, v := range actual.Paths.Paths {
        getPathInfo(k, v.Get, http.MethodGet, &paths)
        getPathInfo(k, v.Post, http.MethodPost, &paths)
        getPathInfo(k, v.Put, http.MethodPut, &paths)
        getPathInfo(k, v.Patch, http.MethodPatch, &paths)
        getPathInfo(k, v.Delete, http.MethodDelete, &paths)
    }
    info.Paths = paths
    fmt.Println(info)
    return
}

func getPathInfo(url string, operation *spec.Operation, method string, paths *[]Path) {
    if operation != nil {
        *paths = append(*paths, Path{
            Url:        url,
            Method:     method,
            Consumes:   operation.Consumes,
            Produces:   operation.Produces,
            Tags:       operation.Tags,
            Summary:    operation.Summary,
            Parameters: operation.Parameters,
            Responses:  operation.Responses,
        })
    }
}

至此,已经把swagger.json文件的解析成为自己想要的数据形式,后续就可以根据这个数据对比出接口的变化啦。

B哥:“写得好!我选择付费工具。”

“......”

本故事纯属虚构,如有雷同,奖励你一杯奶茶