|
| 1 | +# v0.2.1 - 2026-03-24 |
| 2 | + |
| 3 | +## Highlights |
| 4 | + |
| 5 | +- **OPENAPI-012**: 新增 `jsonPublicPath` 配置项,解决反向代理剥离路径前缀场景下 Scalar "Try it out" 请求发往错误地址的问题 |
| 6 | +- **BUG-033**: 修复 `doc-endpoints.ts` 中 Scalar HTML spec URL 与路由注册路径强耦合的缺陷 |
| 7 | +- **DOCS-003/004**: 补充反向代理部署文档,修正多处字段名错误 |
| 8 | + |
| 9 | +--- |
| 10 | + |
| 11 | +## New Features |
| 12 | + |
| 13 | +### OPENAPI-012: `jsonPublicPath` — Scalar HTML spec URL 与路由注册路径解耦 |
| 14 | + |
| 15 | +**背景**: |
| 16 | + |
| 17 | +`vext` 的 OpenAPI 模块此前用同一个路径(`jsonPath`)同时控制两件事: |
| 18 | + |
| 19 | +1. vext 内部路由注册路径(`app.adapter.registerRoute("GET", specPath, ...)`) |
| 20 | +2. Scalar HTML 中 `Scalar.createApiReference` 的 `url` 字段 |
| 21 | + |
| 22 | +当应用部署在 Nginx 反向代理后、且代理**剥离**路径前缀时(`proxy_pass` 末尾带 `/`),两个路径需要不同的值: |
| 23 | + |
| 24 | +- vext 内部路由:`/openapi.json`(代理已剥离前缀,vext 收到的是干净路径) |
| 25 | +- Scalar HTML 引用:`/admin/openapi.json`(浏览器需要带前缀才能命中 Nginx 规则) |
| 26 | + |
| 27 | +由于两者耦合,原先只能二选一,导致要么路由404,要么 Scalar 获取 spec 失败。 |
| 28 | + |
| 29 | +**变更内容**: |
| 30 | + |
| 31 | +新增 `jsonPublicPath` 配置字段,专门控制 Scalar HTML 中引用 spec 的 URL,与路由注册路径完全独立: |
| 32 | + |
| 33 | +```typescript |
| 34 | +// config/production.ts |
| 35 | +export default { |
| 36 | + openapi: { |
| 37 | + enabled: true, |
| 38 | + jsonPath: '/openapi.json', // vext 内部路由(Nginx 剥离前缀后收到 /openapi.json) |
| 39 | + docsPath: '/docs', |
| 40 | + jsonPublicPath: '/admin/openapi.json', // Scalar HTML 引用地址(浏览器侧完整路径) |
| 41 | + }, |
| 42 | +}; |
| 43 | +``` |
| 44 | + |
| 45 | +请求链路(代理剥离前缀场景): |
| 46 | + |
| 47 | +``` |
| 48 | +浏览器 GET /admin/docs |
| 49 | + → Nginx 剥离 /admin → vext GET /docs → 返回 Scalar HTML(url = /admin/openapi.json) |
| 50 | + → Scalar fetch /admin/openapi.json |
| 51 | + → Nginx 剥离 /admin → vext GET /openapi.json → 返回 spec ✅ |
| 52 | +``` |
| 53 | + |
| 54 | +未设置 `jsonPublicPath` 时,默认值与 `jsonPath` 相同,行为与升级前完全一致(向后兼容)。 |
| 55 | + |
| 56 | +**新增文件/字段**: |
| 57 | + |
| 58 | +| 位置 | 变更 | |
| 59 | +|------|------| |
| 60 | +| `src/types/app.ts` → `VextOpenAPIConfig` | 新增 `jsonPublicPath?: string` | |
| 61 | +| `src/lib/openapi/types.ts` → `DocEndpointsConfig` | 新增 `specPublicPath?: string` | |
| 62 | +| `src/lib/openapi/doc-endpoints.ts` | `generateScalarHTML` 改用 `specPublicPath ?? specPath` | |
| 63 | +| `src/lib/bootstrap.ts` | 透传 `jsonPublicPath` → `specPublicPath` | |
| 64 | +| `src/lib/dev/dev-bootstrap.ts` | 同上 | |
| 65 | +| `src/lib/dev/route-reloader.ts` | 同上 | |
| 66 | + |
| 67 | +--- |
| 68 | + |
| 69 | +## Bug Fixes |
| 70 | + |
| 71 | +### BUG-033: 反向代理前缀剥离场景 Scalar "Try it out" 404 |
| 72 | + |
| 73 | +**问题描述**: |
| 74 | + |
| 75 | +应用部署在 Nginx `/admin/` 代理前缀下(代理剥离前缀),访问 `https://example.com/admin/docs` 时,Scalar UI 尝试 fetch `/openapi.json`(绝对路径),浏览器请求发往 `https://example.com/openapi.json`,绕过了 Nginx 的 `/admin/` 规则,返回 404。 |
| 76 | + |
| 77 | +**根因**: |
| 78 | + |
| 79 | +`doc-endpoints.ts` 中 `generateScalarHTML(specPath, ...)` 直接将内部路由注册路径(`/openapi.json`)写入 Scalar HTML 的 `url` 字段。绝对路径在浏览器中始终相对于 origin 解析,无法感知代理前缀。 |
| 80 | + |
| 81 | +**修复**: |
| 82 | + |
| 83 | +引入 `specPublicPath` 字段(由 `jsonPublicPath` 配置驱动),在生成 Scalar HTML 时使用 `specPublicPath ?? specPath`,使两者可以独立配置: |
| 84 | + |
| 85 | +```typescript |
| 86 | +// doc-endpoints.ts (修复后) |
| 87 | +const specPublicPath = config.specPublicPath ?? specPath; |
| 88 | +// ... |
| 89 | +const html = generateScalarHTML(specPublicPath, scalarConfig); // ← 用公开路径 |
| 90 | +app.adapter.registerRoute("GET", specPath, [...]); // ← 用内部路由 |
| 91 | +``` |
| 92 | + |
| 93 | +**影响范围**: |
| 94 | + |
| 95 | +- 仅影响配置了反向代理前缀剥离的部署场景 |
| 96 | +- 未配置 `jsonPublicPath` 时行为与修复前完全一致(向后兼容) |
| 97 | + |
| 98 | +--- |
| 99 | + |
| 100 | +## Documentation |
| 101 | + |
| 102 | +### DOCS-003: openapi.md 新增反向代理部署章节 |
| 103 | + |
| 104 | +在 `website/docs/guide/openapi.md` 的"自定义文档路径"章节下新增: |
| 105 | + |
| 106 | +- **反向代理路径前缀场景** — 详细说明代理**剥离前缀**和**透传前缀**两种策略的配置方式 |
| 107 | + - 情况一(剥离前缀):`jsonPath` 保持默认,配置 `jsonPublicPath` 为带前缀的公开路径,附完整请求链路图 |
| 108 | + - 情况二(透传前缀):配置 `jsonPath` / `docsPath` 为带前缀的路径,无需配置 `jsonPublicPath` |
| 109 | + - 两种情况对比表格 |
| 110 | +- **`servers` 字段说明** — 解释 `servers` 是写入 OpenAPI spec 本身的元数据,仅影响 Scalar "Try it out" 的目标地址;默认值 `"/"` 跟随当前域名,多数情况无需配置;需要多环境切换下拉框时才显式配置 |
| 111 | + |
| 112 | +### DOCS-004: 修正多处字段名错误 |
| 113 | + |
| 114 | +| 文件 | 问题 | 修复 | |
| 115 | +|------|------|------| |
| 116 | +| `website/docs/guide/configuration.md` | 表格和代码示例中用了不存在的 `specPath` 字段 | 修正为 `jsonPath`,新增 `jsonPublicPath` 行 | |
| 117 | +| `website/docs/guide/openapi.md` | 全局配置示例中用了 `specPath` | 修正为 `jsonPath`,注释说明 `jsonPublicPath` | |
| 118 | +| `website/docs/api/config.md` | `VextOpenAPIConfig` 表格缺少 `jsonPublicPath` 行 | 在 `jsonPath` 行后新增 | |
| 119 | + |
| 120 | +--- |
| 121 | + |
| 122 | +## Validation |
| 123 | + |
| 124 | +在 `vext-test` 项目中通过端到端验证(`verify-openapi-public-path.mjs`): |
| 125 | + |
| 126 | +``` |
| 127 | +[PASS] GET /openapi.json → 200 OK(vext 内部路由注册正常) |
| 128 | +[PASS] GET /docs → 200 OK(文档页面路由正常) |
| 129 | +[PASS] Scalar HTML 中 spec url = /proxy/openapi.json ✅(jsonPublicPath 生效) |
| 130 | +[PASS] Scalar HTML 中 url 字段不含内部路径 /openapi.json(路由路径与公开路径已解耦) |
| 131 | +[PASS] openapi 版本字段: 3.0.3 |
| 132 | +[PASS] info.title: "VextJS Test API" |
| 133 | +[PASS] paths 包含 128 个路由 |
| 134 | +[PASS] servers[0].url: "http://127.0.0.1:3000" |
| 135 | +[PASS] OpenAPI spec 内容中不含 /proxy/openapi.json(spec 与 Scalar 配置正确隔离) |
| 136 | +
|
| 137 | +通过: 9 / 失败: 0 |
| 138 | +``` |
| 139 | + |
| 140 | +全量单元测试:**2329 tests passed**(`npm test`) |
| 141 | + |
| 142 | +TypeScript 类型检查:**0 errors**(`tsc --noEmit`) |
| 143 | + |
| 144 | +--- |
| 145 | + |
| 146 | +## Migration Guide |
| 147 | + |
| 148 | +### 无破坏性变更 |
| 149 | + |
| 150 | +`jsonPublicPath` 是纯新增可选字段,默认值为 `jsonPath` 的值,升级后行为与 v0.2.0 完全一致。 |
| 151 | + |
| 152 | +### 受影响场景:反向代理剥离前缀部署 |
| 153 | + |
| 154 | +如果你的部署满足以下条件: |
| 155 | + |
| 156 | +1. 应用通过 Nginx(或其他反向代理)暴露,且代理**剥离**路径前缀 |
| 157 | +2. 访问 `/admin/docs` 后 Scalar 无法加载 spec(浏览器控制台报 404) |
| 158 | + |
| 159 | +则需要添加 `jsonPublicPath` 配置: |
| 160 | + |
| 161 | +```typescript |
| 162 | +// config/production.ts(或对应环境配置文件) |
| 163 | +export default { |
| 164 | + openapi: { |
| 165 | + enabled: true, |
| 166 | + jsonPublicPath: '/admin/openapi.json', // 替换为你的实际代理前缀 |
| 167 | + }, |
| 168 | +}; |
| 169 | +``` |
| 170 | + |
| 171 | +其他配置无需修改。 |
0 commit comments