再也不想写接口文档啦
前言
我有一个朋友,他叫李晓得。在一个阳光明媚的下午,忙碌了半天的李师傅坐在电脑面前眉头紧皱。隔壁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哥:“写得好!我选择付费工具。”
“......”
本故事纯属虚构,如有雷同,奖励你一杯奶茶
转载自:https://juejin.cn/post/7226325773941948476