☰
SwaggerUI接口文档导出实战:从OpenAPI规范到离线PDF/HTML
2026/10/13 3:56:34 网站建设 项目流程

上个月接了个很磨人的需求:项目要跟合作方做接口对接,对方文档工程师开口就要一份可存档、可离线查看的接口文档。我一开始觉得很简单,把测试环境跑着的 SwaggerUI 地址发过去就行,结果对方说没有内网访问权限,在线页面只能“演示”,不能作为正式交付物。

这个问题很多人都会遇到。日常开发里 SwaggerUI 打开就能看接口,已经足够,可一旦涉及对外交付、项目验收、文档归档,就必须把 SwaggerUI 背后那份通用 API 规范文件导出,再做二次加工。整个流程看似只是“导出”,实际牵扯到规范理解、工具选型和自动化落地。这篇文章是我这次实操的记录,包含拿到规范文件的几种路径、离线 HTML、PDF、Markdown 的生成方法,以及我踩过的一些坑,希望能给同样被“导文档”卡住的人一些参考。

1. 先想清楚:SwaggerUI导出到底导的是什么

1.1 你看到的SwaggerUI只是一个渲染壳

如果你把 SwaggerUI 当成“接口文档本身”,那导出这件事很容易走偏。最简单也最容易翻车的操作,是用浏览器直接“另存为”整个页面。结果通常是一个空壳 HTML,打开后什么都没有,因为 SwaggerUI 只是一个前端渲染程序,保存网页时它根本不会把接口数据一起保存下来。

SwaggerUI 的逻辑很简单:它通过一个地址加载一份 OpenAPI 规范文件,这份文件可以是 JSON 或 YAML,里面记录了所有接口路径、请求参数、响应结构、鉴权方式等信息。然后 SwaggerUI 再根据这份规范渲染出左侧接口列表、右侧参数说明、Try it out 调试按钮。所以 SwaggerUI 里所有字段,都来自这份原始文件,页面本身不保存任何业务数据。

想通这一点,导出思路就清晰了:真正要导出的不是网页,而是网页背后那份 OpenAPI 规范文件。只要拿到这份文件,后续生成 HTML、PDF、Markdown,甚至 Postman 集合和客户端代码,都只是格式转换的问题。这也是整个导出项目最核心的底层逻辑。

1.2 规范文件是万能的“中间态”

OpenAPI 规范文件有一个好处:它是纯数据,和展示层完全解耦。我们可以把它理解成一张“母版”,所有导出产物都从它生成。只要母版足够准确,各端拿到的文档就不会出现“页面显示一套、PDF 里是另一套”的情况。

但这里有个前提:规范文件的版本要搞清楚。常见的有 OpenAPI 2.0(也叫 Swagger 2.0)和 OpenAPI 3.0.x、3.1.x。2.0 和 3.0 在结构上有明显差异,比如 3.0 用components/schemas放数据模型,2.0 用的是definitions。很多导出工具只支持其中一种版本,拿错版本去转换,会报莫名其妙的错误。导出前第一件事,就是先确认项目里到底用的是哪个版本。

拿到规范文件后,可以根据不同交付场景做多种导出:

交付对象推荐导出形式常用工具说明
合作方存档PDFChromium 无头打印内容固定,适合签字归档
前端、测试日常查看离线 HTML 静态站ReDoc、本地 SwaggerUI打开即用,界面友好
内部二次编辑MarkdownWiddershins + Pandoc方便其他人改动和补充
联调调试Postman 集合Postman 导入直接发请求,省去手工配参数
前后端代码生成客户端 SDKOpenAPI Generator基于同一份 spec 生成代码,减少偏差

如果你不理解这层“中间态”,很容易被工具带着走,最后发现导出的是残缺文档。理解了规范文件的地位,后续所有操作都顺理成章。

1.3 按交付对象选型,不追求一个工具通吃

我见过有人花了一整天研究某个导出插件,最后发现它根本不适合自己的项目。工具选型的原则很简单:先问清楚文档给谁看、用在哪。

如果是给测试同学日常联调用,一个本地部署的 SwaggerUI 就够,他们需要的是 Try it out 这种真实发请求的能力。如果是给合作方做正式交付,ReDoc 生成的单文件 HTML 更专业,界面干净,但缺少在线调试能力。如果是走合同和验收流程,PDF 是必需项,而且要做成可归档的版本,最好带上接口总数、修订记录、生成日期这些元信息。

这三种场景经常同时存在。所以实际操作中,我不会只选一个工具,而是先导出规范文件,再根据交付对象分别生成 HTML、PDF 和 Markdown。工具组合使用,比单打独斗可靠得多。

2. 从SwaggerUI里抽出OpenAPI规范文件:三个必经动作

2.1 用浏览器开发者工具定位真正的spec请求

如果你是第一次接触这个项目,不知道规范文件存在哪里,最快的办法是从浏览器入手。打开 SwaggerUI 页面,按 F12 进入开发者工具,切到 Network(网络)面板,刷新一下页面,然后把请求类型筛成 XHR 或 Fetch。找名字里包含api-docs、openapi、swagger的请求,点开看响应内容,如果看到大段 JSON,里面有关键字openapi或swagger,那这条请求就是规范文件的地址。

常见的地址形如/v3/api-docs、/v2/api-docs或/swagger/v1/swagger.json。不同后端框架路径不一样,但也有规律可循。我整理了一个简单的对照:

后端情况常见 spec 路径
Java 新体系,使用 SpringDoc/v3/api-docs,注意带 context-path
Java 老项目,使用旧版 Swagger 2.0/v2/api-docs
.NET 系常见配置/swagger/v1/swagger.json
经过网关或自定义前缀以 Network 面板实际请求为准

用 Network 面板定位是最稳的,因为它看到的是 SwaggerUI 实际请求的地址,不会因为加了 context-path、groupId 等参数而漏掉。

2.2 用 curl 把spec文件完整保存下来

定位到地址后,不要只复制页面内容,建议直接在命令行里拉取。我平时用的是 curl,配合 jq 做格式校验。以/v3/api-docs为例:

# 查看 OpenAPI 版本信息,确认是 3.0 还是 2.0 curl -sS http://127.0.0.1:8080/v3/api-docs | jq '.openapi' # 直接把 JSON 保存到本地 curl -sS http://127.0.0.1:8080/v3/api-docs -o openapi.json # 如果服务端支持 YAML,也可以直接保存 YAML curl -sS http://127.0.0.1:8080/v3/api-docs.yaml -o openapi.yaml

如果服务端有鉴权,需要在请求头里带上 token,比如登录后从接口返回里拿到的访问令牌:

curl -sS -H "Authorization: Bearer <token>" \ http://127.0.0.1:8080/v3/api-docs -o openapi.json

这里有个很容易被坑的点:如果 SwaggerUI 页面是在浏览器里能正常打开,但命令行里 curl 访问是 404,多半是地址里少了前缀。尤其是项目配了 context-path 或者网关路由,SwaggerUI 页面里的请求地址会带完整前缀,直接拿/v3/api-docs去请求自然不行。建议以 Network 面板里那条真实请求的 URL 为准,不要自己脑补路径。

2.3 校验和整理:别拿坏spec当源头

拿到 spec 文件后,一定要校验一下,不然后面导出时错误一堆。可以上传到在线的 Swagger Editor 里看有没有红报,也可以用命令行工具做本地校验:

npx @apidevtools/swagger-cli validate openapi.yaml

如果文件是拆成多个 YAML/JSON 片段引用的,导出前建议先合并成一个文件。直接用bundle命令:

npx @apidevtools/swagger-cli bundle openapi.yaml -o bundled-openapi.yaml

这样做是为了防止导出工具遇到$ref指向外部文件时找不到路径。很多导出工具只吃单个文件,你给它一个带外部引用的目录结构,它可能生成一半就断了。这个步骤看着琐碎,但能省掉后面大量排查时间。

2.4 规格文件保存后的即时检查

保存下来不是结束,还要做一次简单的完整性检查。我通常关注三块内容:接口数量、请求方法、数据模型是否完整。命令行直接统计:

# 查看路径数量 jq '.paths | length' openapi.json # 查看数据模型数量 jq '.components.schemas | length' openapi.json # 列出所有接口路径,快速浏览 jq -r '.paths | keys[]' openapi.json

如果路径数量比预期少了几个,或者某些页面能看到的接口在文件里找不到,就要回头检查是不是存在多份规范文件、接口分组不同、或者 spec 是动态生成的。规范文件不完整,后面所有导出产物都不可能完整。

3. 离线HTML文档站:给前端和测试一套可点开就用的静态页

3.1 方案A:本地化部署SwaggerUI

拿到规范文件后,最容易想到的做法是把 SwaggerUI 跑到本地。SwaggerUI 本身是纯前端资源,只需要把它的静态文件包下载下来,放到任意 Web 服务器里,再让页面去加载本地的 openapi.json 即可。

具体步骤很简单:下载对应版本的 dist 包,解压到/usr/share/nginx/html目录下,然后把 openapi.json 也放进同一个目录。接着修改 dist 包里的swagger-initializer.js,把原来的远程地址改成相对路径:

window.onload = function() { window.ui = SwaggerUIBundle({ url: "openapi.json", dom_id: "#swagger-ui", deepLinking: true, presets: [ SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset ], plugins: [ SwaggerUIBundle.plugins.DownloadUrl ], layout: "StandaloneLayout" }); };

注意这里的url一定要写相对路径,不要写http://127.0.0.1:8080这种绝对地址。否则合作方打开 HTML 时会去访问你本地的服务,跨域加不可达,文档直接空白。

nginx 配置只需一个最简单站点:

server { listen 8080; server_name _; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } }

这个方案适合给测试团队当内部工具用。他们打开页面后可以点 Try it out,实际向接口发请求。前提是合作方或测试方能够访问到接口服务本身,否则调试功能依然是摆设。

3.2 方案B:用ReDoc生成单文件HTML

如果只是要给合作方看文档,不需要在线调试能力,我更推荐 ReDoc。它生成的 HTML 是一个单文件,样式比 SwaggerUI 清晰很多,接口分组、数据模型展示都更专业,而且可以直接通过邮件发出去,不需要部署服务器。

生成命令很简单,前提是本机装了 Node.js:

npx @redocly/cli build-docs openapi.yaml -o api-doc.html

如果想控制文档标题,可以加--title参数:

npx @redocly/cli build-docs openapi.yaml -o api-doc.html --title "对外合作接口文档"

我之前在老项目里还用过旧版命令npx redoc-cli bundle openapi.yaml -o index.html,效果也差不多。ReDoc 单文件 HTML 是把规范文件内容直接打包进 HTML 里的,离线打开完全没问题。但要注意,ReDoc 不支持像 SwaggerUI 那样的在线发请求按钮,更多是“看文档”而不是“调试接口”。如果合作方要的是既能看又能调,那还是走本地 SwaggerUI 方案。

3.3 不推荐直接wget整个SwaggerUI页面

有人会想,既然 SwaggerUI 是静态网页,那我直接把线上 SwaggerUI 页面抓下来不就行了?理论上可行,实际操作漏洞很多。SwaggerUI 页面的 JS、CSS 可能来自 CDN,抓取时容易被漏掉;即使页面能打开,它内部还是要通过网络请求加载规范文件,而规范文件地址很可能指向一个你不希望暴露、或者对方访问不到的内网地址。

我在一个项目里试过用 wget 镜像整个 SwaggerUI 目录:

wget --mirror --page-requisites --convert-links http://127.0.0.1:8080/swagger-ui/

抓下来的文件夹能打开,但图片样式偶尔会加载不全,最重要的 Try it out 请求也发不出去,因为浏览器会沿着配置里的接口地址发起真实请求。整体体验和完整交付差很远。所以这个方案我只建议用于快速内部存档,不建议作为正式交付手段。

3.4 静态文档站部署的小细节

离线文档站部署起来容易,但有些细节会影响体验。第一,所有引用路径尽量用相对路径,不要写域名或带端口;第二,json 和 yaml 这类文件建议开启 gzip 压缩,文档变大后打开速度会差很多;第三,如果 HTML 文件经常被重新生成,建议在 nginx 里设置短缓存,避免合作方老是看到旧版本。

一个能直接用的配置片段:

location ~* \.(json|yaml|html)$ { gzip on; expires 1d; }

这个配置不会让文档失效变慢,但能显著减少重复下载流量。若合作协议里要求文档版本可追溯,最好在生成 HTML 时把版本号写进标题,文件名也带日期,比如api-doc-20240801.html,比通用名字更稳妥。

4. PDF与Markdown离线件:交付和归档两手准备

4.1 用Chromium无头模式把HTML转成PDF

HTML 静态站虽然方便,但正式交付时对方往往还是想要 PDF,便于存档、打印、走合同流程。最省力的方案,是先把文档生成一个完整的 HTML,再用无头浏览器打印成 PDF。这样排版所见即所得,不会出现转换工具导致的样式断裂。

我用的是 Chromium 的无头模式。命令如下:

chromium --headless --disable-gpu --no-sandbox \ --virtual-time-budget=10000 \ --print-to-pdf=api-doc.pdf api-doc.html

这里的api-doc.html建议用 ReDoc 生成的单文件 HTML,因为它已经是完整网页,无头浏览器加载后直接渲染即可。--virtual-time-budget=10000是给页面一点时间完成异步渲染,避免有些脚本还没执行完就输出 PDF。如果你的主进程叫 google-chrome 或 chrome,把命令前面的chromium换成对应名称即可。

这个方案遇到最多的问题是中文字体。如果服务器上没有安装中文字体,PDF 里中文会变成方框或乱码。解决办法是在服务器上安装常用中文字体,比如文泉驿、思源黑体,安装完之后再跑命令。不要指望无头浏览器的默认字体能自动处理中文。

4.2 用Widdershins和Pandoc生成Markdown与Word

如果要给文档做二次编辑,或者希望把接口文档转成 Word 版本让商务同事改措辞,Markdown 是很好的中间格式。Widdershins 是一个专门把 OpenAPI 转成 Markdown 的工具,用法不算复杂:

npx widdershins --language_tabs 'javascript:JavaScript' 'shell:cURL' \ openapi.yaml -o api-document.md

生成之后的 Markdown 可以直接交给 Pandoc 转成 Word:

pandoc api-document.md -o api-document.docx

这样对方拿到的是一个可编辑文档,而非只能看的 PDF。这套链路的好处是,如果接口有改动,重新跑一遍就能更新文档,不用手工在 Word 里改参数。但也要注意,Widdershins 生成的 Markdown 是基于 spec 里的描述信息,如果后端代码里没写example或描述,Markdown 里对应位置会空着,所以源头注释质量直接决定导出质量。

4.3 Java体系内的一体化方案:swagger2markup + asciidoctor

如果你所在团队本身就在用 Asciidoc 写技术文档,可以考虑用 swagger2markup 把 spec 转成 Asciidoc,再借助 asciidoctor 插件生成 PDF。这个方案能跟团队现有文档流程整合,但配置量明显更大。Maven 里大概是这样:

<plugin> <groupId>io.github.swagger2markup</groupId> <artifactId>swagger2markup-maven-plugin</artifactId> <version>1.3.3</version> <configuration> <inputFile>${project.basedir}/src/docs/openapi.yaml</inputFile> <outputDir>${project.build.directory}/asciidoc</outputDir> <config> <swagger2markup.markupLanguage>ASCIIDOC</swagger2markup.markupLanguage> <swagger2markup.pathsGroupedBy>TAGS</swagger2markup.pathsGroupedBy> </config> </configuration> </plugin>

然后配合 asciidoctor-maven-plugin 生成 PDF。我这里不展开写全部配置,因为如果你是独立项目,临时搭这套会非常痛苦;但如果你本来就有 Asciidoc 文档构建流水线,把 Swagger 转换接进来会很自然。工具没有绝对的好坏,匹配团队现有体系才是关键。

4.4 导出前的文档内容检查清单

生成 PDF 和 Markdown 之前,我建议先对 spec 文件做一轮内容自检,避免交付件里出现“响应字段缺失”“示例空白”这类尴尬情况。我按这个顺序检查:

  • info.title和info.version是否正确,这是 PDF 封面用的关键信息。
  • 所有接口是否有summary或description,不能只给个路径。
  • 请求参数是否标注了必填、类型、默认值。
  • 响应对象是否引用了完整的 schema,而不是空对象。
  • 鉴权方式是否写清楚,比如 header 名称、token 获取方式。
  • 关键 POST 接口有没有填写example,否则合作方根本不知道请求体长什么样。

这些内容在导出后很难靠工具补全,必须在源头维护。我的习惯是把这条检查清单贴在团队的接口规范里,每次发版前按清单过一遍。

5. 把导出做成流水线:自动化、版本化和多格式分发

5.1 用一张脚本把导出过程串起来

第一次手工导出成功后,我很快意识到不能每次都用命令行手动敲。接口一多,导出频率一高,漏步骤是常态。所以我把整个流程写成了一个脚本,从拉取 spec 到生成 HTML、PDF、Markdown,一步到位:

#!/usr/bin/env bash set -euo pipefail SPEC_URL="${SPEC_URL:-http://127.0.0.1:8080/v3/api-docs}" OUT_DIR="${OUT_DIR:-./export}" mkdir -p "$OUT_DIR" # 1. 拉取规范文件 curl -fsS "$SPEC_URL" -o "$OUT_DIR/openapi.json" # 2. 快速检查版本 jq '.openapi, .info.version' "$OUT_DIR/openapi.json" # 3. 生成 ReDoc 风格 HTML npx @redocly/cli build-docs "$OUT_DIR/openapi.json" \ -o "$OUT_DIR/api-doc.html" --title "对外接口文档" # 4. 无头浏览器打印 PDF chromium --headless --disable-gpu --no-sandbox \ --virtual-time-budget=10000 \ --print-to-pdf="$OUT_DIR/api-doc.pdf" "$OUT_DIR/api-doc.html" # 5. 生成 Markdown,便于二次编辑 npx widdershins --language_tabs 'javascript:JavaScript' 'shell:cURL' \ "$OUT_DIR/openapi.json" -o "$OUT_DIR/api-document.md" # 6. 打包 tar -czf "api-doc-$(date +%Y%m%d).tar.gz" "$OUT_DIR"

把这段脚本放到项目仓库里,每次接口大版本变更后手动执行一次,十几秒就能拿到全套交付件。脚本最前面用set -euo pipefail,任何一个环节失败都会终止,不会让你拿着一个半成品文档发出去。

5.2 集成到持续集成流水线里

如果团队已经有持续集成流水线,建议把导出脚本作为一个独立任务挂进去,在每次构建后自动生成文档产物,再自动归档。这样能保证文档跟着代码走,不会出现代码改了但文档还是上周版本的情况。

我的一般做法是:在流水线里新增一个docs阶段,调用前面那个脚本,然后把导出目录作为产物保留下来。既可以下载查看,也可以进一步上传到内网文档站点。有一点必须注意:不要在流水线日志里打印完整 spec 内容,尤其是带鉴权信息时,日志会长期保存,属于潜在风险。这条建议不是什么规范文档里的教条,是我在实际操作中被安全巡检提醒过之后留下的教训。

5.3 基于同一份spec生成SDK和Mock服务

导出不一定只是给别人看文档。基于同一份 OpenAPI 规范文件,还可以用 OpenAPI Generator 生成客户端代码,让前端同学拿到类型定义、API 封装,不用再手工对齐字段。一个典型例子:

npx @openapitools/openapi-generator-cli generate \ -i openapi.json \ -g typescript-fetch \ -o ./out/typescript-client

另外,用 Prism 之类的 Mock 工具可以基于 spec 启动一个模拟服务,在前端只需要契约、后端接口还没就绪时先用假数据联调。这类工具和文档导出是同一件事的两面:规范文件是源头,导出、生成代码、起 Mock 都只是它的下游产物。

5.4 spec文件也要做版本管理

OpenAPI 规范文件不应该只存在于服务器上,建议纳入 Git 仓库统一管理。每次接口变更都会在 diff 里显式体现,比谁都说不清文档什么时候改过要强得多。我见过很多项目把接口变化记录在聊天记录里,出问题后无从追溯,如果 spec 入库,至少能快速找出某个字段是什么时候被加进去的。

配合命令:

# 查看本次变更了哪些接口 git diff --stat openapi.yaml

如果你需要更工具化的对比,可以用 openapi-diff 类的工具直接比较两个版本之间的 breaking change。版本管理的核心不是“存文件”,而是让文档改动历史可查、可回滚。

6. 常见问题排查与避坑指南

6.1 典型问题速查表

我把实际操作中遇到过的问题整理成一张表,方便你对照排查:

问题现象常见原因解决方案
导出的 HTML 打开是空白用“另存为”保存了网页,而不是规范文件在 Network 面板定位 api-docs 请求,curl 保存规范文件
页面能打开,但 spec 请求 404URL 缺少 context-path 或 group 参数以 Network 面板的真实请求地址为准
curl 保存后中文乱码终端编码问题,或命令行工具被别名替代用 curl.exe,直接-o保存文件,不经过重定向
ReDoc 构建失败spec 里有跨文件$ref引用先用 swagger-cli bundle 合并成单文件
PDF 中文字体乱码渲染设备缺少中文字体安装思源黑体等中文字体后重新生成
文档里请求示例为空后端没有给参数填写 example在接口注解或 spec 中补充 example 字段
对方反馈无法在线调试使用了 ReDoc 静态站改用本地 SwaggerUI 部署,保证接口可达

这张表不能覆盖所有情况,但覆盖了绝大多数导文档时“看起来很怪”的现象。如果遇到新问题,我的建议是先回到源头验证,看规范文件本身健不健康,不要直接猜转换工具。

6.2 导出实战中我踩过的几个坑

第一次做导出时,我踩过最大的坑是以为 SwaggerUI 页面能直接“打印成 PDF”。用浏览器打印功能确实能跑,但左侧导航栏每页重复出现,右侧内容被截断,导出结果惨不忍睹。后来改成 ReDoc 单文件 HTML,再配合无头浏览器打印,这个问题才彻底解决。所以不要嫌多一道转换,HTML 到 PDF 这一步其实比“页面直接打印”可靠得多。

另一个坑是 Windows 命令行下 curl 的编码问题。在 PowerShell 里,curl默认被别名成Invoke-WebRequest,行为差异很大,特别是保存 JSON 时编码不对,中文直接乱码。后来统一改用curl.exe并加上-o保存,才稳定下来。如果你的环境是 Windows,建议直接使用 Git Bash 或 WSL,能省掉很多不必要的折腾。

还有一个被忽视的问题:多人协作时,有人会在 SwaggerUI 页面左侧看到多个分组,但导出只有一个接口列表,数量对不上。原因通常是后端配置了多个 Docket 或分组,每个分组对应一份独立 spec。这时候你就需要把所有分组的 spec 都导出,再决定按分组生成多份文档,还是用工具合并成一份。我当时没注意,差点把一份只有一半接口的文档发出去。

6.3 让导出流程在团队里真正落地

文档导出这件事,一个人有一次成功经验不算完,只有让团队其他人也能稳定操作才叫落地。我把上面的脚本、检查清单和常见问题都写进项目文档后,后续再有人需要导出文档,直接照着执行就可以,不用再翻聊天记录找命令。

我个人体会是,不要把导出当一次性的临时任务。表面上你只是发一份 PDF,实际上你是在维护一份接口交付契约。契约一旦乱了,前端、测试、合作方都会跟着吃苦。尽早把规范文件作为一等公民管理,在设计、编码、测试环节都顺手看一眼文档生成是否正常,比最后交付那天拼命补救要轻松得多。如果这篇记录能让你少踩一个坑,那这次导出实战就没有白折腾。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询