# specimen **Repository Path**: NullPE/specimen ## Basic Information - **Project Name**: specimen - **Description**: Drop an OpenAPI file - **Primary Language**: Unknown - **License**: Apache-2.0 - **Default Branch**: master - **Homepage**: http://110.40.181.218:47200/ - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 0 - **Created**: 2026-09-23 - **Last Updated**: 2026-09-24 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # Specimen 一个**静态**的 OpenAPI 阅览器:把一份 `.yaml` / `.yml` 拖进浏览器,立刻能用三副面孔读它 —— Redoc(安静地读)、Scalar(现代、可试请求)、Swagger UI(调试最全)。 - **没有后端。** 文件从不离开浏览器:不起服务接收上传、不代理请求、不转存。 - **只有一层壳。** 界面归引擎,外壳只负责拖入、解析、分派、以及在顶栏说清楚"这是哪份文件"。 - **同一时刻只有一个引擎活着。** 切走即拆掉(见 [ADR-0002](docs/adr/0002-mount-on-demand.md))。 ## 跑起来 ```bash pnpm install pnpm dev # http://127.0.0.1:5198/ ``` 产物与部署: ```bash pnpm build # → dist/,纯静态文件 python3 -m http.server 8080 --directory dist # 本机验产物(也可 pnpm serve) ``` **线上**: —— 服务器上只有 nginx 发静态文件,没有 Node、没有构建、没有服务端逻辑。 云侧防火墙**按端口放行**:只开 47200;80 未开(公网访问它得到的是云侧边缘的 `503`,本机 80 根本没有监听)。 ```bash ./scripts/deploy.sh # 本地构建 → rsync 上传 → 远端自检 ``` 部署脚本是幂等的:首次会装 nginx 并写站点配置,此后只更新文件。构建后会自动**预压缩**(`scripts/precompress.mjs` 生成 `.gz`),由 nginx 的 `gzip_static` 直接发——nginx 动态 gzip 的默认级别是 1,换成构建时的 level 9 全站省 15%,且 CPU 花在构建而不是每个访客的每次请求上。**不做 brotli**:服务器上没有 `ngx_brotli`,为 10% 去加第三方包源不划算。 量一次传输代价: ```bash BASE=http://110.40.181.218:47200/ bash scripts/diagnostics/measure-transfer.sh ``` 它报的是**链路上的字节**(curl 的 `%{size_download}`)与 TTFB。别用 `fetch().arrayBuffer()` 去量——那是解压后的大小,会把 69 KB 的 CSS 读成 460 KB(踩过)。站点挂在**根路径**、**端口 47200**、纯 HTTP(只有 IP 没有域名,签不了证书)。端口可用 `SPECIMEN_PORT=…` 覆盖。 为什么不是 80:这台机器不是"对外服务",高而偏的端口能省掉绝大多数扫描噪音(已避开 32768–60999 的临时端口范围,免得和出站连接随机撞车),`Server:` 头也收成了光秃秃的 `nginx`。**但要说清**:端口号只挡扫描器,挡不住有心的人——这是少些噪音,不是藏起来。 这台机器上只有这一份站点,不必和别的东西分路径——**子路径那版被否掉了**:`alias` + `try_files` 的兜底回指自身,nginx 会报 `rewrite or internal redirection cycle`,裸文件全是 500。 其余命令:`pnpm test`(接缝处的单元测试)、`pnpm smoke`(真浏览器走完整三幕,见下)、`pnpm lint`、`pnpm type-check`。 ## 它接受什么 `.yaml` / `.yml` / `.json`,内容须是 **OpenAPI 3.0 或 3.1**。扩展名不设限(认的是内容),JSON 也走得通(YAML 是 JSON 的超集)。 **换下一份**有三种做法,走的是同一条路(先卸载、再挂载):顶栏右端的 `eject` 按钮、`Escape` 键、或者直接把新文件拖上来。退出只清这份文档,**记住你上次选的引擎**,也不问你"确定吗"——点错了再拖一次即可。 **Swagger 2.0 会被认出来但不渲染** —— 说清版本,让人自己去升级。 **`$ref` 只支持文档内部**(`#/components/...`)。指向别的文件或外网 URL 的引用渲染不出来:我们不做 workspace、不做后端(见 [ADR-0001](docs/adr/0001-static-no-backend.md))。 ## 三个引擎的脾气(都是实测,不是转述) | | Redoc 2.5.4 | Scalar 1.71.0 | Swagger UI 5.33.0 | | --- | --- | --- | --- | | 怎么喂 | `RedocStandalone spec={对象}` | `createApiReference(el, { content: 对象 })` | `SwaggerUIBundle({ spec: 对象 })` | | 喂字符串 | ✗ 会被当成 URL 去 fetch | ✓ | ✗ **静默什么都不做** | | 喂 `blob:` URL | ✗ `lstatSync is not a function` | ✓ | ✓ | | 卸载 | React root `unmount()` | 有 `destroy()` | **没有**,只能清空容器 | | 默认联网点 | 搜索索引起一个 blob Worker | — | `validatorUrl` 指向 `validator.swagger.io` | 所以:**一律给对象**,一个输入路径喂三家;Redoc 走 npm 包时必须自备 React(那个包不导出 `Redoc.init`,只有 React 组件);Swagger UI 显式 `validatorUrl: null`。 ## 三个引擎的"试请求" - Redoc 社区版**没有**试请求面板 —— 别在那副面孔上找。 - Scalar 与 Swagger UI 会从浏览器**直接**发请求:目标没开 CORS 就会被拦,页面是 HTTPS 而目标 API 是 HTTP 会被混合内容拦。这是浏览器的规矩,我们不代理、不绕行。 - 页面本身**不替引擎联网**:默认只加载随产物发出去的资源(字体也是本地的)。 ## 自检 `pnpm smoke` 用真的 Chrome 走完整三幕:拖第一份进来 → 三副面孔各截一张 → **退出** → **再拖第二份**进来。它记录: - 每个引擎在画布里渲染出多少文本与元素(空画布会立刻暴露), - 退出后画布是否真的从 DOM 里消失、印章是否换名、控件是否归位, - 第二份文件是否渲染出来、印章是否写着它的名字, - 控制台错误,以及**取回成功**与**尝试外出**两类请求(外部取回必须为零;每次外出的尝试都必须被指名是 CSP 挡下的), - 截图落在 `shots/`(不入库)。 诊断脚本在 `scripts/diagnostics/`:`trace-seed` / `trace-chunks` / `trace-preload` / `trace-import` / `trace-mount` / `trace-logo`。它们是排"引擎挂不上""请求从哪来"这类问题时真正省时间的东西,故留在仓库里。 ## 页面自己不联网 三个引擎都有默认的外出行为,全部在**策略层**堵死: | 引擎 | 默认会去哪 | 怎么堵 | | --- | --- | --- | | Redoc | `cdn.redoc.ly` 上的 logo(地址硬编码在组件里) | `index.html` 的 CSP `img-src 'self' data: blob:` | | Redoc | 搜索索引会起一个 blob Worker | 适配器 `disableSearch: true` | | Swagger UI | `validatorUrl` 指向 `validator.swagger.io` | 适配器 `validatorUrl: null` | 字体经 `@fontsource` 随产物打包,不连外部 CDN。**别用 CSS 去藏外链**——实测 Chrome 照样把图取回来;隐藏得住眼睛,隐藏不住网络。 ## 文档 - [CONTEXT.md](CONTEXT.md) —— 这个项目的词汇表(只收词)。 - [docs/adr](docs/adr) —— 两条真正的取舍:静态无后端、按需挂载。 - [docs/spec.md](docs/spec.md) —— 规格副本;**权威版在 morphic 仓库的 issue tracker 里**(`.scratch/openapi-preview/spec.md`)。