多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Go项目Swagger文档开启与自定义UI模板实践指南

Go项目Swagger文档开启与自定义UI模板实践指南 最近有同学在群里问Go 项目里怎么把 Swagger 文档打开顺便把默认那套蓝白 UI 换成自己团队的风格。这个问题拆开其实是两步先让接口文档跑起来再把文档页面外层模板改掉。这篇就沿着这个顺序写以 Gin swaggo/gin-swagger 的组合为例讲讲怎么快速开启 Swagger、默认模板藏在哪个环节以及怎么用本地自定义模板彻底换掉它。适合正在给 Go 项目接 API 文档或者对内部系统统一 UI 有要求的同学。整套流程跑通之后你会得到一个完全由自己控制的本地文档页面不依赖外部 CDN还能顺手把标题、logo、折叠状态、授权参数全部按团队习惯调好。1. 先搞清楚你说的 Swagger 到底指哪一层很多人把 Swagger 当成“接口文档”这个整体其实它至少包含三层东西OpenAPI 规范文件、负责渲染的 Swagger UI、以及生成规范或代码的工具链。Go 项目里“开启 Swagger”通常是把 OpenAPI 规范文件用中间件暴露出来再配一个 Swagger UI 页面而“更换模板”听起来像是换 UI 皮肤实际要动的可能是 HTML 结构、CSS、JS 资源甚至是后端返回页面的模板字符串。我见过两种常见的 Go 接入方式先分清楚会少踩很多坑。1.1 为什么我推荐 swaggo 而不是 go-swaggerGo 社区里两条路线经常被搞混一条是swaggo/swag通过解析 Go 源码里的注释来生成 OpenAPI 文档再搭配gin-swagger暴露页面另一条是go-swagger/go-swagger它更偏 API-First从一个swagger.yaml规范文件反过来生成服务端和客户端代码。对于大多数已经写好的业务服务最省事的方案是前者。你不需要为了文档去调整项目结构只用在现有 Handler 上方补注释跑一条命令就能生成文档运行时再挂一个路由就行。后者适合新项目设计阶段但侵入性明显更强模板定制也主要集中在代码生成模板上和浏览器里看到的页面模板不是一回事。所以下面全部以 swaggo 生态为例。如果你后续改用 go-swagger会发现“更换模板”变成了swagger generate的--template参数解决的是代码生成样式问题不是页面样式问题。1.2 “开 Swagger”和“换模板”的动作拆解开启 Swagger 可以拆成三步在代码里写注解、跑swag init生成docs/swagger.json、在路由里注册中间件。完成后浏览器访问/swagger/index.html默认页面就出来了。更换模板的动作就不一样它发生在“中间件已经返回页面”这个环节。默认情况下gin-swagger会在内存里拼一段写死的 HTML再把它返回给浏览器浏览器再去加载 swagger-ui 的 JS 和 CSS。你想换模板要么改这段 HTML要么把 swagger-ui 的静态资源整体换成自己的要么直接复制一份 swagger-ui 到项目里重新改。把这些层次想明白后面每一步就不会迷糊。2. 从零开启 Swagger先把默认页面跑起来在碰模板之前先把默认页面跑通。我默认你已经有 Go 环境和 Gin 项目版本至少是 Go 1.16 以上因为后面要用的embed是这个版本才稳定的。2.1 初始化项目与安装依赖项目初始化命令mkdir demo-api cd demo-api go mod init demo-api安装依赖go get -u github.com/gin-gonic/gin go get -u github.com/swaggo/swag/cmd/swag go get -u github.com/swaggo/gin-swagger go get -u github.com/swaggo/files注意swag是一个命令行工具不是运行库最好用go install github.com/swaggo/swag/cmd/swaglatest装到$GOPATH/bin里。要确认命令是否可用直接跑swag -v如果提示找不到先检查$GOPATH/bin是否在PATH环境变量里。这一步卡住的人很多因为go get版本不同装出来的二进制位置不一样。2.2 用注释描述接口Swagger 注解的触发点有两个一个是main.go里的全局信息一个是每个 Handler 上面的接口信息。先看全局信息比如我在main.go顶部写package main // title 示例项目 API // version 1.0.0 // description 这是一个用于演示 Swagger 接入与模板替换的项目 // host localhost:8080 // BasePath /api/v1 func main() { r : gin.Default() // ... _ r.Run(:8080) }host和BasePath生成的文档会在页面左上角显示生产环境记得改成真实域名和统一前缀。再看一个典型的 Handler 注解// Summary 获取用户信息 // Description 根据用户 ID 返回昵称和头像地址 // Tags 用户 // Produce json // Security ApiKeyAuth // Param id path int true 用户 ID // Success 200 {object} model.User // Failure 404 {object} model.ErrorResponse // Router /users/{id} [get] func GetUser(c *gin.Context) { // 业务逻辑 }这些注解不是随便写的swag解析时会严格对应字段。Param必须写清楚参数位置是path、query还是body{object}后面的类型需要能被包扫描到。如果model.User定义在另一个包注释里要写完整包名路径。2.3 生成 docs 目录在项目根目录执行swag init -g main.go -o docs-g main.go指定入口文件-o docs指定输出目录。成功后会生成三个文件docs/swagger.json、docs/swagger.yaml、docs/docs.go。其中docs.go是给程序读取用的。这一步如果提示“cannot find type definition”多半是某个{object}引用的结构体路径没写对或者swag没有找到对应包。我喜欢在执行前先go build ./...确认代码能编过这样解析报错时能快速排除语法问题。2.4 注册 Swagger 路由在需要挂载的路由文件里写import ( github.com/gin-gonic/gin ginSwagger github.com/swaggo/gin-swagger swaggerFiles github.com/swaggo/files _ demo-api/docs ) func RegisterSwagger(r *gin.Engine) { r.GET(/swagger/*any, ginSwagger.WrapHandler(swaggerFiles.Handler)) }这里必须匿名导入demo-api/docs因为docs.go里有init()会注册变量不导入就无法读取生成的文档数据。启动服务后访问http://localhost:8080/swagger/index.html默认的 Swagger UI 页面就出来了。能看到页面只是第一步我们要的“更换模板”还没开始。2.5 不改模板的基础玩法用配置项调页面参数如果暂时不想动模板又想调整默认页面行为gin-swagger提供了不少配置项。最常用的是这几个r.GET(/swagger/*any, ginSwagger.WrapHandler(swaggerFiles.Handler, ginSwagger.URL(http://localhost:8080/swagger/doc.json), ginSwagger.DocExpansion(none), ginSwagger.PersistAuthorization(true), ginSwagger.DefaultModelsExpandDepth(-1), ))URL指定文档加载地址DocExpansion控制接口默认展开还是收起可选值有list、none、fullPersistAuthorization让页面刷新后保留鉴权 tokenDefaultModelsExpandDepth(-1)可以直接把底部的 Models 区域隐藏掉。这些配置最终会变成 Swagger UI 的 JavaScript 参数属于“给默认页面换配置”和换模板不完全一样。如果你只需要改标题、收起接口、隐藏模型这套方案就够了完全不用碰源码。3. 更换模板前先看默认模板的“零件”在哪明白了基础玩法下一步就是真正换模板。这里有个核心问题默认模板到底藏在哪里很多人在项目里找不到因为它在依赖包里。3.1 swaggo/files 与 gin-swagger 的分工从依赖关系上看swaggerFiles.Handler负责提供静态资源文件它的底层是embed.FS把 swagger-ui 的 HTML、JS、CSS 都封装在github.com/swaggo/files包里。ginSwagger.WrapHandler做的事情更简单拿到请求后把一段内置的index.html字符串渲染出来返回给浏览器。浏览器再根据这段 HTML 里的资源引用去/swagger路径下请求 JS、CSS。也就是说默认模板由两部分组成后端返回的 HTML 内容以及前端静态资源文件。你只改其中一个页面都可能出现样式错乱或功能缺失。3.2 默认模板的三大可替换点第一是 HTML 结构。默认模板给 Swagger UI 一个挂载点div idswagger-ui然后在script里设置SwaggerUIBundle的初始化参数。你想改标题、logo、布局都要动这一层。第二是静态资源。默认页面加载的 JS、CSS 可以来自依赖包内嵌文件也可以来自本地目录甚至可以指向 CDN。内网环境访问不到外网 CDN 时这就是最大的坑。第三是初始化参数。docExpansion、persistAuthorization、deepLinking这些配置直接决定页面交互体验换模板时很容易被忽略。3.3 替换模板的四种方案对比我用一张表把常见方案的取舍列出来方便你按场景选方案实现方式隔离性维护成本适用场景配置项微调只用 gin-swagger 提供的参数高依赖包不受影响极低只想收起接口、隐藏模型、保留鉴权修改依赖缓存里的资源直接去 GOPATH 或 vendor 里改 files 包低重新go mod download就丢失低但不可控本地临时看效果不推荐提交本地静态目录 自定义 HTML把 swagger-ui dist 拷贝到项目自己写页面高完全可控中需要维护资源文件内网部署、团队统一 UI、深度定制复制 gin-swagger 模板源码修改把模板字符串和 WrapHandler 逻辑复制到项目里高彻底摆脱依赖较高依赖包升级后要手动同步需要修改后端输出 HTML 的极少数场景我的建议很直接要真换模板就选第三种。把 swagger-ui 的静态资源放进项目里用go:embed打包进二进制再写一个简单的静态文件路由。这样 CDN 问题、项目结构问题、团队协作问题都能一起解决。4. 实操用 go:embed 把自定义 swagger-ui 模板打进二进制下面是一套我实际用过的方案目标是让/swagger/index.html变成我们自己写的页面同时保留doc.json的动态加载能力。4.1 把 swagger-ui 的 dist 资源拷进项目先去官方发行渠道下载 swagger-ui 的dist目录或者从依赖包github.com/swaggo/files的源码里把静态资源解出来。我建议直接拿官方 dist版本和项目搭配合适就行。把dist目录里的内容放到项目web/swagger-ui下目录结构大致这样web/swagger-ui/ ├── index.html ├── swagger-ui.css ├── swagger-ui-bundle.js ├── swagger-ui-standalone-preset.js ├── favicon-16x16.png ├── favicon-32x32.png └── ...如果 dist 里有oauth2-redirect.html也一并留着OAuth2 流程会用到。4.2 改 index.html标题、样式、资源引用打开web/swagger-ui/index.html重点改几个地方。第一是标题title示例项目 API 文档/title第二是资源引用。官方 dist 的index.html可能引用./swagger-ui-bundle.js这类相对路径这没问题。如果你之前的模板直接引用了外部 CDN 地址务必换成相对路径link relstylesheet href./swagger-ui.css script src./swagger-ui-bundle.js/script第三是初始化参数。找到SwaggerUIBundle的配置文件改成想要的行为。比如我一般这么配script window.onload function () { const ui SwaggerUIBundle({ url: /swagger/doc.json, dom_id: #swagger-ui, deepLinking: true, docExpansion: none, persistAuthorization: true, presets: [SwaggerUIBundle.presets.apis, SwaggerUIBundle.standalonePreset], layout: BaseLayout }) window.ui ui } /script这里url用的是相对路径/swagger/doc.json而不是http://localhost:8080/...。因为不同环境域名不一样写死 localhost 会导致部署后文档加载失败。如果还想改页面顶部样式可以在swagger-ui.css后面追加一段覆盖样式.swagger-ui .topbar { background-color: #1f2d3d; } .swagger-ui .topbar .download-url-wrapper { display: none; }隐藏掉顶部下载入口可以让页面看起来更像内部系统。4.3 用 go:embed 嵌入静态文件不推荐用http.Dir(./web/swagger-ui)这种方式因为部署二进制时还需要额外带资源目录很不方便。直接用embed.FS把资源打进二进制。在项目里新建embed.gopackage main import embed //go:embed all:web/swagger-ui var swaggerUI embed.FS注意我写了all:web/swagger-ui而不是web/swagger-ui/*。前者会把swagger-ui目录下所有子目录都递归打包进去后者只能打包一层文件。官方 dist 里可能有子目录或者嵌套资源保险起见用all:前缀。4.4 注册可以“以假乱真”的路由有了swaggerUI这个embed.FS接下来在 Gin 里挂静态服务。embed.FS不能直接传给 Gin需要先用io/fs.Sub切出子目录再转成http.FSimport ( embed io/fs net/http github.com/gin-gonic/gin ) //go:embed all:web/swagger-ui var swaggerUI embed.FS func RegisterCustomSwagger(r *gin.Engine) { subFS, err : fs.Sub(swaggerUI, web/swagger-ui) if err ! nil { panic(err) } // doc.json 必须优先注册Gin 会先匹配具体路由 r.GET(/swagger/doc.json, func(c *gin.Context) { c.File(docs/swagger.json) }) // 静态页面挂在 /swagger 下访问 /swagger 会自动跳转到 /swagger/index.html r.StaticFS(/swagger, http.FS(subFS)) }这里有个小细节r.StaticFS(/swagger, ...)会在访问/swagger时自动重定向到/swagger/index.html正好符合大家的使用习惯。而/swagger/doc.json因为注册在StaticFS之前会优先生效。这样既能提供页面又能动态返回 Swagger 生成的文档数据。如果你想再挂一个纯资源目录也可以同时加r.StaticFS(/swagger-assets, http.FS(subFS))然后把 index.html 里的脚本路径改成/swagger-assets/swagger-ui-bundle.js。这样做的好处是页面路径和资源路径完全分离后续配 Nginx 更干净。不过一般情况下直接挂/swagger就够了。4.5 让默认页面支持自定义鉴权很多内部接口文档需要登录后才能看到内容。一个常见的做法是在反向代理层做鉴权但如果你想在模板层面实现可以在SwaggerUIBundle初始化前先请求一次会话接口拿到 token 后塞进authorizationsscript async function getToken() { const res await fetch(/api/v1/auth/token) const data await res.json() return data.token } window.onload async function () { const token await getToken() const ui SwaggerUIBundle({ url: /swagger/doc.json, dom_id: #swagger-ui, deepLinking: true, docExpansion: none, persistAuthorization: true, presets: [SwaggerUIBundle.presets.apis], layout: BaseLayout, authorizations: { BearerAuth: { value: Bearer token } } }) window.ui ui } /script这种方法适合临时验证正式的敏感接口应该配合网关或更严密的鉴权策略。模板里直接把 token 写进前端毕竟只做了展示用途不要让它承担过多安全责任。4.6 构建并验证完成代码后执行go mod tidy go build -o demo-api . ./demo-api然后访问curl http://localhost:8080/swagger/doc.json curl http://localhost:8080/swagger/index.html第一个请求应该返回 JSON 文档第二个请求返回我们修改后的 HTML。如果这两个都正常说明自建 Swagger 已经完整跑起来了。5. 常见问题与排查技巧实录这块内容是多次踩坑后的记录建议收藏。几乎每个问题我都亲手遇到过尤其是第一次切自定义模板的时候。5.1 页面出现“Failed to load API definition”这个报错通常是doc.json没访问到或者访问到的是错误格式。排查步骤很简单先直接访问/swagger/doc.json浏览器里看到 JSON 就继续下一步如果看到 404检查路由注册顺序和docs包是否被匿名导入。如果是 200 但页面仍报错多半是 Swagger 版本和 swagger-ui 版本不兼容检查swag生成的文档格式是不是 OpenAPI 3.0而 swagger-ui 版本太老优先升级swagger-ui资源版本。5.2 文档数据是旧的新接口没出现swag init不是实时监控没修改注解后重新执行文档不会自动更新。我习惯把命令固定成swag init -g main.go -o docs --parseDependency --parseInternal--parseDependency会解析依赖包里的注解--parseInternal会解析内部包。如果接口分散在多个模块这两个参数能避免“部分接口没生成”的问题。5.3 模板资源加载不出来样式全乱页面能打开但 CSS 或 JS 是空白的第一反应看浏览器 Network 面板。404 的原因是资源路径不对常见于 index.html 里用了绝对路径但没带前缀或者embed目录层级没切对。这时可以打印一下fs.Sub后的目录结构确认文件路径是否和代码里一致。5.4 改完模板页面不变是一直在缓存吗Swagger UI 静态资源很容易被浏览器缓存。开发调试时打开开发者工具的 Network勾选 Disable cache然后强制刷新。如果是部署环境可以在index.html里的资源地址后面加版本参数script src./swagger-ui-bundle.js?v20240101/script后端也可以给静态资源设置Cache-Control: no-cache避免线上更新后用户拿着旧页面反复报错。5.5 内网环境下外部 CDN 永远加载不出来这是切换本地模板最常见的动机。确认下面三点index.html 中没有http://开头的外部资源所有 js、css、图标都放到了本地目录go:embed打包时确实包含了这些文件。如果只替换了 HTML忘拷 CSS 文件页面会非常难看。5.6 浏览器访问 /swagger 时重定向登录页如果你在 Nginx 层做了统一登录/swagger可能被拦截。两个思路给 Swagger 页面单独开一个内部跳板路径或者把静态页面同样纳入鉴权体系再在模板里接入统一 token。前者适合开发环境后者适合正式环境。看你们团队的安全策略决定。5.7 同一项目想挂多套 Swagger 文档网关类项目经常需要给不同模块分别看文档。Gin 里可以用ginSwagger.InstanceName(admin)的方式注册多个实例同时为每个部分单独生成 doc.json。自定义模板方案里你可以把模板复制出多份分别指定不同的url指向不同路由。例如admin/doc.json和order/doc.json再映射到不同 HTML 页面结构很清晰。6. 写在最后的实际操作体会个人经验里最值得分享的一条不要一上来就想改模板先评估自己是不是只需要配置项。很多团队抱怨“默认 UI 太丑”实际只是想要隐藏模型、收起接口、换标题这些用gin-swagger自带参数三分钟就搞定没必要引入一整套本地资源。真的到了要统一品牌风格、放内部 logo、隐藏某些入口的时候再用本地静态目录方案也不迟。我通常在项目根目录建一个web/文件夹与代码、配置文件分开资源归资源代码归代码。go:embed打包后部署非常省心单二进制文件带着文档一起走不需要额外拷贝资源目录。还有个小技巧每次升级 swagger-ui 资源版本后记得在浏览器里完整过一遍“接口列表展开、参数填写、调用请求”三个流程。UI 升级可能带来接口交互变化比如某些版本默认隐藏了 Try it out 按钮或者授权弹窗行为不一样。只有实测过才算真正换好模板。
返回列表