改造 Swagger UI 页面,实现集中查看多个项目的接口文档

凌云 关注

收藏于 : 2018-11-13 09:43   被转藏 : 1   

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"> ,页面上显示的就只有相对路径了,不太好看

 阅读文章全部内容  
点击查看
文章点评
相关文章
凌云 关注

文章收藏:9211

TA的最新收藏