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

keithmork · 发布于 2017年07月20日 · 最后由 keithmork 回复于 2017年07月26日 · 1219 次阅读
本帖已被设为精华帖!

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

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 2 条回复
104 seveniruby 将本帖设为了精华贴 07月20日 21:01
10693 keithmork 解析和增量导出 Swagger 的 JSON 文件 中提及了此贴 07月21日 09:27
C6cbef

Swagger丑成狗,还在用...

10693
C6cbefhero 回复

是很丑,在代码里的注解更丑,之前我们用apidocjs,但不方便返回实体类内容,转战swagger了

还有什么好介绍没?

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册