Swagger是个前后端协作的利器,解析代码里的注解生成JSON文件,通过Swagger UI生成网页版的接口文档,可以在上面做简单的接口调试
缺点是有多少个后台服务就有多少个这样的网址,对采用微服务的项目来说有个地方集中看几十上百个服务的文档是刚需
运维同事想到了给Swagger UI的网页加个选单,用JS操作页面本来就有的输入框和按钮切换JSON数据源
我后来又改成支持遍历目录,配合 另一个解析和对比swagger json、增量导出接口文档的脚本 使用
于是就有了以下的东西:
1级菜单选后台服务,2级菜单选json文件(如果目录下只有1个会自动选中)
步骤
https://github.com/swagger-api/swagger-ui
下载Swagger UI,只需要 dist 文件夹,单独拿出来扔进web server里就能用
NodeJS + Express写个简单的服务器
把dist复制到工程目录下(我把它改名成 public),初始化工程
npm init # 偷懒的话一路回车就行 npm i express -- save npm i cors -- save # 解决 CORS 问题的神器 # eslint 之类语法检查的就不说了
新建 index.js
文件,作为默认的程序入口
工程结构如下
我们约定swagger导出的JSON文件都放 json/ 目录下, 子目录以后台服务命名,各服务的json分别放进对应的目录里:
用GET方法访问 http://<host>:3000/json
就会遍历 json/ 下面各目录里的json文件,会返回以下json字符串:
{ "<dir1>" : [< file1 >, < file2 >], "<dir2>" : ...}
简单写一下
const path = require ( ' path ' ); const fs = require ( ' fs ' ); const express = require ( ' express ' ); const cors = require ( ' cors ' ); const isDir = ( dir ) => { try { const stat = fs . statSync ( dir ); return stat . isDirectory (); } catch ( err ) { return false ; } }; const isFile = ( file ) => { try { const stat = fs . statSync ( file ); return stat . isFile (); } catch ( err ) { return false ; } }; const app = express (); const jsonDir = path . join ( __dirname , ' json ' ); app . use ( cors ()); app . use ( express . static ( path . join ( __dirname , ' public ' ))); app . use ( ' / apidoc ' , express . static ( path . join ( __dirname , ' public ' ))); app . use ( ' / json ' , express . static ( jsonDir )); app . get ( ' / json ' , ( req , res ) => { const swaggerJsons = {}; const serviceNames = fs . readdirSync ( jsonDir , ' utf - 8 ' ); serviceNames . forEach (( serviceName ) => { const dirPath = path . join ( jsonDir , serviceName ); if ( isDir ( dirPath )) { const files = fs . readdirSync ( dirPath , ' utf - 8 ' ); const jsonFiles = []; files . forEach (( filename ) => { const filePath = path . join ( dirPath , filename ); if ( isFile ( filePath ) && filename . indexOf ( ' . json ' ) >= 0 ) { jsonFiles . push ( filename ); } }); swaggerJsons [ serviceName ] = jsonFiles ; } }); res . send ( JSON . stringify ( swaggerJsons )); }); app . listen ( 3000 , () => { console . log ( ' Listening on port 3000 ! ' ); });
运行
node index . js
浏览器访问 http://<host>:3000/
或 http://<host>:3000/apidoc
就会打开Swagger UI的接口文档网页
修改Swagger的页面文件
修改 dist 下的 index.html,加上二级关联菜单
<body>
顶部:
< div id = "select-json-src" style = "width: 100%; max-width: 87.5rem; margin: 0 auto; padding: 0 1.25rem; line-height: 3.75rem;" > < label > 请选择项目: < select id = "dropdown-parent" onchange = "getResponseJson('http://localhost:3000/json', addChildOptions)" ></ select > < select id = "dropdown-child" onchange = "changeJsonSrc();" ></ select > </ label > </ div >
底部的 <script>
下加上需要的函数
var parent = document . getElementById ( ' dropdown - parent ' ); var child = document . getElementById ( ' dropdown - child ' ); function getResponseJson ( url , callback ) { var xmlHttp = new XMLHttpRequest (); xmlHttp . onreadystatechange = function () { if ( xmlHttp . readyState === 4 ) { callback ( JSON . parse ( xmlHttp . responseText )); } }; xmlHttp . open ( ' GET ' , url , true ); // async xmlHttp . send ( null ); } function clearOptions ( dropDown ) { var length = dropDown . options . length ; if ( length > 0 ) { for ( var i = 0 ; i < length ; i ++) { dropDown . options . remove ( 0 ); } } } function addOption ( dropDown , text , value ) { var option = document . createElement ( ' option ' ); option . text = text ; option . value = value ; dropDown . options . add ( option ); } function changeJsonSrc () { var urlBox = document . getElementsByClassName ( ' download - url - input ' )[ 0 ]; var btn = document . getElementsByClassName ( ' download - url - button ' )[ 0 ]; urlBox . value = child . options [ child . selectedIndex ]. value ; btn . click (); } function updateServices ( respJson ) { if ( respJson ) { var services = Object . keys ( respJson ); clearOptions ( parent ); addOption ( parent , '请选择 ... ' , '' ); services . forEach ( function ( service ) { addOption ( parent , service , service ); }); } } function addChildOptions ( respJson ) { if ( respJson ) { clearOptions ( child ); var service = parent . value ; var jsonFiles = respJson [ service ]; if ( jsonFiles && jsonFiles . length ) { jsonFiles . forEach ( function ( file ) { addOption ( child , file , ' http: //localhost:3000/json/' + service + '/' + file); }); } if ( child . options . length > 0 ) { child . selectedIndex = 0 ; changeJsonSrc (); } } }
修改下面的 window.onload
函数,顶部加上:
getResponseJson ( ' http: //localhost:3000/json', updateServices);
再把下面 SwaggerUIBundle
默认的url改成空: url: "",
【坑】替换 swagger-ui-standalone-preset.js
然后就会碰到一个大坑,用js触发页面上 Explore 按钮,会忽略文本框里的输入,总是用SwaggerUIBundle里设置的url
动手能力强的人可以自己尝试改掉 swagger-ui-standalone-preset.js,但文件是压缩过的,美化了那堆abcdefg的变量名也还是没法看
GitHub上能搜到issue里说3.0.8的旧版本是没问题的,备份一下找个没问题的版本替换掉这文件
测试过没问题之后替换一下网页里的url,放到开发服务器上
json文件可以在构建服务时用脚本从swagger url( http://<host>:<port>/v2/api-docs
)下载到指定目录(简单点用curl,复杂点见 这里 )
另外swagger的json文件还能导进postman,用途很多,就看想象力了
PS:隐藏 @ ApiImplicitParam 引起的错误信息
如果项目是Java + Spring Boot + Swagger,springfox-swagger2 和 springfox-swagger-ui 插件建议升到2.7.0+
旧版本的 @ApiImplicitParam
没有 dataTypeClass
参数,方法的参数类型是String、Integer、Enum等,总之不是你自己定义注解了 @ApiModel
的类,生成的网页就会类似以下的错误:
Could not resolve reference because of: Could not resolve pointer: /definitions/String does not exist in document
如果实在不想升,修改下 dist 下的 index.html,在 <head> <style>
下加段css把错误信息藏起来:
pre . errors - wrapper { visibility: hidden ; height: 0 ; }
PS:不推荐用 <base>
指定base URL
如果在 <head>
加了 <base href="xxx">
,页面上显示的就只有相对路径了,不太好看