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

Keith Mo · 2017年07月20日 · 最后由 向导 回复于 2019年03月19日 · 6019 次阅读
本帖已被设为精华帖!

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

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

Swagger 丑成狗,还在用...

Jay_ 回复

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

还有什么好介绍没?


我不懂 Java,以上全是我编的,我实在编不下去了……😆

遇到这个坑了 怎么搞啊

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;
window.ui.specActions.updateUrl(urlBox.value); //添加这里可以
btn.click();
}

simple 专栏文章:[精华帖] 社区历年精华帖分类归总 中提及了此贴 12月13日 20:49
simple [精彩盘点] TesterHome 社区 2018 年 度精华帖 中提及了此贴 01月07日 12:08

index.html 文件下有个


const ui = SwaggerUIBundle({
        urls: [{name:'',url:''}]
],
        dom_id: '#swagger-ui',
        deepLinking: true,
        presets: [
          SwaggerUIBundle.presets.apis,
          SwaggerUIStandalonePreset
        ],
        plugins: [
          SwaggerUIBundle.plugins.DownloadUrl
        ],
        layout: "StandaloneLayout"
      })

只要 urls 有内容里面就会变成可选框了,然后 url 链接目标的 v2/api-docs 即可,如果有跨域问题可以用 php 写个 get 小脚本

<?php
$url = $_GET['url'];
$result = file_get_contents($url);
echo $result;
?>

./get-data.php?url= 即可

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