Medusa API Reference 站点架构解析:从 OAS 管线到 Next.js 混合渲染的完整实现
【免费下载链接】medusaThe world's most flexible commerce platform for agents and developers项目地址: https://gitcode.com/GitHub_Trending/me/medusa
导读
本文以 Medusa 开源仓库中的 www/apps/api-reference/CLAUDE.md 为核心文档,系统拆解 Medusa 官方 API Reference 站点的工程架构——这是一个基于 Next.js App Router、部署于 Cloudflare(OpenNext)的 REST API 文档应用,同时渲染 Store 与 Admin 两套 REST API 参考。读完本文,你将掌握:OAS 规范如何从 API 路由类型自动生成并沉淀为提交产物、URL 与 slug 的单一事实来源设计、标签页懒加载与"整标签滚动渲染"的混合渲染模型、深链滚动与滚动监听锁的协同机制,以及本地开发、测试、构建与重新生成规范的完整操作流程。
应用概览与两个核心事实
API Reference 应用(www/apps/api-reference)是一个 Next.js(App Router)站点,渲染 MedusaStore与Admin两套 REST API 参考文档。它通过 OpenNext 部署到 Cloudflare,并以basePath/api对外服务(如https://docs.medusajs.com/api/store)。
理解该应用的架构,必须先接受两个核心事实:
- 规范结构完全由源码生成:规范的 structure(parameters、request/response bodies、schemas、security)全部由 packages/medusa/src/api 中 API 路由的请求/响应类型生成,项目中不存在任何
@oas注释。 - 只有描述(descriptions)可以手工编辑:
specs/目录下除描述外的所有内容、以及整个generated/目录,都必须通过重新生成获得,绝不手工编辑(参见重新生成一节)。
Pipeline:从 OAS 到公开文档
整个文档生成链路是一套明确的单向管线,文档中给出了完整流程图:
packages/medusa/src/api (API route request/response types = source of truth) │ OAS CLI (www/utils, `yarn generate:oas`) + automated "Updated API Reference" job ▼ apps/api-reference/specs/{area}/ ← committed spec input for this app ├── openapi.yaml base doc: info, tags (name, description, x-associatedSchema), security ├── openapi.full.yaml fully dereferenced doc (used by the download route) ├── paths/*.yaml one file per endpoint (method → operation) ├── components/schemas/ referenced schemas └── code_samples/ x-codeSamples │ `yarn prep` → scripts/prepare.mjs ▼ generated/*.mjs ← committed build artifacts ├── api-ref-paths.mjs paths + old-hash→new-path redirects + intro sections (scripts/generate-specs-manifest.mjs) ├── specs-tag-index.mjs { area: { tagSlug: [pathFile,...] } } (lazy-load lookup) ├── specs-sitemap-data.mjs ordered tags + operation ids per area └── generated-{store,admin}-sidebar.mjs full sidebar tree (build-scripts → get-api-ref-sidebar-children.ts) │ next build / next dev ▼ Rendered pages (dynamic SSR) + client hydration各环节在仓库中的实际落点
① 源头:API 路由类型(单一事实来源)。所有 OAS 内容的源头是 packages/medusa/src/api 下的 636 个路由文件(www之外、monorepo 根下的packages/medusa包)。仓库中不存在任何手工编写的 OpenAPI 注释,这一点可以从该目录没有任何@oas风格的 JSDoc 注解得到印证。
② 规范输入:specs/目录。以当前仓库为准,www/apps/api-reference/specs 下实际包含:
store/与admin/两个区域,各自含有openapi.yaml、openapi.full.yaml;paths/目录按端点一个文件(store67 个、admin269 个路径文件);components/下 610 个被引用的 schema 文件;code_samples/中的x-codeSamples代码示例(store131 个、admin777 个文件);versions/目录保存归档版本(当前包含 2.15.2、2.15.5、2.17.1、2.17.2、2.18.0、2.20.0 六个历史版本的openapi.yaml/openapi.full.yaml)。
③ 构建产物:generated/目录。www/apps/api-reference/generated 实际提交了api-ref-paths.mjs、specs-tag-index.mjs、specs-sitemap-data.mjs、generated-admin-sidebar.mjs、generated-store-sidebar.mjs、intro-content.mjs等文件,与文档描述一一对应。yarn prep实际执行 scripts/prepare.mjs,它依次调用generateSpecsPathsManifest()(来自 scripts/generate-specs-manifest.mjs)与generateSplitSidebars()(来自build-scripts包,内部消费get-api-ref-sidebar-children.ts),从而产出路径清单、标签索引与两张完整侧边栏树。
④ 渲染:SSR + 客户端水合。最终由next build/next dev渲染为动态 SSR 页面,客户端再水合交互逻辑。
URL 结构与布局(Layout)驱动的路由设计
真实的页面路径
站点不使用 hash 锚点,全部是真实路径(无 hash),统一挂在basePath/api之下:
| Page | Path | Rendered by |
|---|---|---|
| Area intro / index | /api/{area} | app/[area]/page.tsx |
| Intro section | /api/{area}/{section} | app/[area]/[section]/layout.tsx |
| Tag | /api/{area}/{tag} | app/[area]/[section]/layout.tsx |
| Operation | /api/{area}/{tag}/{operation} | app/[area]/[section]/layout.tsx |
| Tag schema | /api/{area}/{tag}/schema | app/[area]/[section]/layout.tsx |
其中{area}只能是store或admin。Intro sections 与 tags共享同一个[section]段。
为什么内容放在 Layout 而不是 Page
这是本文档最关键的一个设计决策:section 内容(intro MDX,或某个 tag 及其全部 operations 组成的<Tags>)由[section]/layout.tsx渲染,而不是 page 渲染。Layout 的解析顺序是:先用getIntroSection解析 intro slug,若命中则按 intro section 处理;否则当作 tag(getTagBySlug)。只有 tag 才拥有第三段[operation]。
源码验证:app/[area]/[section]/layout.tsx 中,Layout 先调用getBaseSpecs(area)获取基础规范,再依次判断getIntroSection(area, section)与getTagBySlug(data, section),未命中则notFound();随后根据区域选择StoreContent或AdminContent(即 markdown/store.mdx 与 markdown/admin.mdx),并包裹BaseSpecsProvider、AreaProvider、PageTitleProvider;intro 分支渲染<ScrollToSection>+ 完整 MDX,tag 分支渲染<Tags tags={[tag]} />。与此同时,app/[area]/[section]/page.tsx 与 app/[area]/[section]/[operation]/page.tsx 仅仅return null——它们存在的唯一目的是支撑路由结构并输出generateMetadata(例如generateMetadata中生成${title} - Medusa ${area} API Reference这样的页面标题)。
这么做的收益:在 tag(/carts)与其 operation(/carts/get-a-cart)之间导航时,只交换空的 page 段,Layout——以及挂载的<Tags>及其已加载的 operations——被完整保留,浏览器只需滚动,而不会重新挂载/重载整个 tag。滚动到活动 operation 由客户端Tags/Operation对活动路径的响应来处理。
动态渲染与可索引性
所有路由都声明了export const dynamic = "force-dynamic"(见 layout.tsx)。每个请求都会实时渲染,与 R2/自请求(self-fetch)的数据模型相匹配;因此每个 URL 都是一份真实的、服务端渲染的文档,并且带有各自独立的generateMetadata——这是该站点能被搜索引擎完整索引的基础。
Slug 逻辑:单一事实来源
一次计算、处处读取
所有 slug 由 www/packages/docs-utils/src/api-ref-paths.ts 中的共享辅助函数一次性计算,并物化为generated/api-ref-paths.mjs。站点、侧边栏、sitemap、重定向映射、TSDoc codemod 全部从该产物读取——绝不在别处临时重算 slug。
各 slug 的推导规则:
- Intro slug=
getApiRefIntroSlug(heading)(即旧的getSectionId([heading])),例如Authentication→authentication。实现上刻意与旧的 hash 锚点保持一致,使历史#authentication链接可以平滑映射到新路径/authentication。 - Tag slug=
getApiRefTagSlug(tag.name),例如Gift Cards→gift-cards。 - Operation slug=
getApiRefOperationSlug(op),优先级依次为x-sidebar-summary→summary→operationId,再经slugify(value.trim().toLowerCase(), { strict: true })处理,例如Get a Cart→get-a-cart、Add Line Item→add-line-item。strict: true会剔除 URL 不安全字符(撇号、括号等),如Change Cart's Customer→change-carts-customer。 - Tag 内去重:
getApiRefTagOperationSlugs在单个 tag 范围内保证唯一,冲突时追加-2、-3等确定性数字后缀,且保留字schema(API_REF_SCHEMA_SLUG)被预占用——这正是 tag schema 页面路径/api/{area}/{tag}/schema的来源。去重编号与渲染顺序保持一致:manifest 生成脚本 generate-specs-manifest.mjs 中镜像了前端的compareOperations排序(GET→POST→DELETE→其余,再按 summary 字典序),确保-2/-3后缀落在正确的操作上。 - 路径构建=
getApiRefPath({ area, section, operationSlug? })→/store/carts/get-a-cart(不含 basePath;next/link会自动补/api前缀)。
Operation slug 由 summary 派生带来的工程约束
因为 operation slug 是summary 派生的,编辑 summary 就会改变 URL。为此系统内置了两道保险:
apiRefRedirects(旧 hash → 新路径的映射)在每次构建时重新生成,自动吸收重命名;- 如需固定 slug,可显式设置
x-sidebar-summary。
此外,生成器会对 intro 与 tag 之间的 slug 冲突发出警告。
类型安全的再导出
www/apps/api-reference/utils/api-ref-paths.ts 将生成的.mjs以正确的 TypeScript 类型再导出(底层.mjs是非可索引的字面量类型),并导出ApiRefIntroSection、ApiRefOperationEntry、ApiRefTagEntry、ApiRefAreaPaths等类型。约定:import 应来自@/utils/api-ref-paths,而不是直接引用.mjs。
渲染与数据流:混合模型(Hybrid Model)
一个 Tag = 一整个可滚动页面
一个 tag 渲染其全部 operations于同一个可滚动页面;operation URL 只是负责滚动定位到对应操作。完整管线如下(各阶段复用同一批组件):
- app/[area]/[section]/layout.tsx(服务端):调用
getBaseSpecs(area)(lib/index.ts → 请求/base-specs路由,只含 tags + metadata),用BaseSpecsProvider+AreaProvider+PageTitleProvider包裹内容,渲染<Tags tags={[tag]}/>(或 intro MDX)。该 Layout 在 tag↔operation 导航期间保持挂载。其中 app/base-specs/route.ts 支持area与可选的expand查询参数——expand时通过getPathsOfTag将某个 tag 的 paths 注入baseSpecs.expandedTags。 <Tags tags={[tag]}/>→ components/Tags/Section:渲染 tag 标题,并通过 SWR 请求GET /tag?tagName={slug}&area={area}(app/tag/route.ts → utils/get-paths-of-tag.ts)懒加载其 operations。懒加载的触发条件是pathname或activePath以该 tag 路径开头。- utils/get-paths-of-tag.ts:从
specs-tag-index.mjs取到该 tag 的paths/*.yaml文件列表(本地文件系统,或设置了SPECS_R2_BASE_URL时从 R2 读取),逐一解引用(dereference),并依据apiRefPaths给每个 operation注入x-path/x-slug,使前端链接/滚动目标与生成的侧边栏、sitemap、重定向映射保持一致。该函数还以unstable_cache包裹并设置revalidate: 3600(1 小时缓存)。 - components/Tags/Paths→
components/Tags/Operation:一次性渲染 tag 的所有operations(没有逐 operation 的懒渲染)。 - 深链滚动(集中在
Tags/Section):当导航到 tag 内的某个 section(标题、schema 或某个 operation,以usePathname为键)时,Tags/Section中的单一控制器滚动到目标元素。滚动位置的计算不是用offsetTop(它对深层嵌套的 operations 会少算),而是相对#main滚动容器用getBoundingClientRect求差(见 Tags/Section/index.tsx)。同时用ResizeObserver监听#content,在每次内容尺寸变化时重新锚定——因为 schema(独立的 SWR 请求)和 code samples(动态 import)加载完毕会推移布局,重锚定保证深链目标不被冲走,这对 adminorders这类大而慢的 tag 至关重要。控制器还监听wheel/touchmove/keydown用户滚动事件来中止自身,并设置 10 秒安全上限。 - 滚动监听 + 导航锁(utils/scroll-spy-lock.ts):滚动过程中,
Tags/Operation与Tags/Section/Schema使用InView(顶部附近的一条窄活动带 → 任一时刻只有一个活动 section)来更新侧边栏高亮(setActivePath)与 URL(history.replaceState)。Next 会把 history 同步到usePathname,因此当深链控制器正在滚动时会持有锁(lockScrollSpy),抑制所有 scroll-spy 的 URL 更新——否则加载时位于顶部的 schema 或第一个 operation 会抢先认领 URL、改变 pathname,从而中止深链滚动(症状就是"停留在 schema 上")。scroll-spy 的 URL 更新会被打标(markScrollSpyNavigation),让控制器能区分真实导航(isScrollSpyNavigation)而不重复触发。此外scheduleScrollSpyUpdate以 120ms 防抖合并快速滚动期间的多次 section 跨越,最终只应用一次 URL/高亮更新。活动状态/高亮是基于路径的(SidebarProvider使用shouldHandlePathChange,且shouldHandleHashChange=false)。
Intro section 的特殊处理
Intro sections(markdown/store.mdx / markdown/admin.mdx)是一个完整的编译 MDX 组件,带有自定义布局 JSX,因此不做切分——Layout 渲染完整的 intro MDX,由 components/ScrollToSection 滚动到标题id。
侧边栏:构建期全量预生成
侧边栏完全在构建期预生成:生成逻辑位于 www/packages/build-scripts/src/utils/get-api-ref-sidebar-children.ts,由generateSplitSidebars消费,输入是generated/api-ref-paths.mjs。生成内容包括:Introduction + 每个 intro section + 每个 tag(作为带path的 category)+ tag 的全部 operations(通过可序列化的badge呈现 HTTP 方法徽标)+ schema 链接。侧边栏始终完整可见,没有运行时懒注入,按区域在 providers/sidebar.tsx 中加载。
一个值得注意的演进细节:侧边栏链接直接使用真实的path;文档明确指出isPathHref标志已被删除(它是旧版 hash 侧边栏的遗留物,已从全仓库移除)。这印证了"纯路径化"的彻底性。
旧 hash 重定向与文档链接
HashRedirector
旧链接使用 hash 锚点(如/api/store#carts_getcartsid)。hash 永远不会到达服务端,因此 components/HashRedirector(挂载于 app/[area]/page.tsx)在 mount 时读取window.location.hash,并用router.replace跳转到apiRefRedirects映射出的路径。未映射的 hash(例如 section 内的 h3 子锚点)则保留,在 index 页面上按普通锚点行为处理。
文档内链接与全库迁移
- 内容中的文档链接(operation/param 的
externalDocs)在渲染时由 utils/resolve-doc-url.ts(resolveApiRefDocUrl)按区域解析,同样复用apiRefRedirects。 - 仓库其他位置手工编写的 TSDoc/MDX 链接,曾由 www/utils/scripts/migrate-api-ref-links.mjs 批量迁移;重新生成规范后若 hash 再次出现,可带
--write重新运行该脚本。
路由与工具辅助函数
- utils/area.ts ——
AREAS(["store", "admin"])、isArea类型守卫、getIntroSection、getTagBySlug、apiRefMetadataBase(元数据 base URL,取NEXT_PUBLIC_BASE_URL,默认http://localhost:3000)。所有路由页面共享。 - utils/get-url.ts —— 从页面路径生成绝对 URL(sitemap 使用)。
- utils/base-path-url.ts —— 为路径加上
basePath前缀。 - 数据路由:
app/tag、app/schema、app/base-specs、app/download/[area](均通过 utils/get-path-for-env.ts 从本地文件系统或 R2 读取specs/)。getPathForEnv的逻辑非常直白:设置了SPECS_R2_BASE_URL即视为 Cloudflare 环境,用/拼接 URL 路径;否则用 Node 的path.join拼本地路径。 - app/versions—— 列出最新版本(取自 docs 全局配置 docs-utils/global-config,每次发版都会更新;因为
version.number不含完整版本号,所以从releaseUrl的/tag/v?([^/]+)/?$中提取)以及它之前的四个归档版本,并提供各自完整 OAS 文档的 URL。归档版本的目录从 R2(bucket binding)或specs/versions列出(utils/get-spec-versions.ts)。版本 URL 指向 R2 中openapi.full.json文件——这些文件只存在于 R2(scripts/upload-specs-to-r2.mjs 在上传时从 YAML 推导生成),未设置SPECS_R2_BASE_URL时则回退到app/download/[area]。路由还实现了limit/offset分页(默认 15、上限 100)与Cache-Control: public, max-age=3600, must-revalidate。
重新生成(Regenerating)
文档给出了两个层级的重新生成命令,务必分清作用域:
# in this app: rebuild generated/ maps + sidebars from specs/ yarn prep # refresh the OAS specs themselves (from the API route request/response types in # packages/medusa/src/api) — heavy, usually CI: cd ../../utils && yarn generate:oasyarn prep实际执行 scripts/prepare.mjs,其内部依次调用generateSpecsPathsManifest()(scripts/generate-specs-manifest.mjs,产出api-ref-paths.mjs与specs-tag-index.mjs等)和generateSplitSidebars()(产出两张侧边栏.mjs)。manifest 生成器在读取specs/{area}/paths时会先排序文件列表,保证输出在任意 OS/文件系统顺序下都是确定性的。yarn generate:oas在www/utils中运行,从 packages/medusa/src/api 的路由类型刷新 OAS 规范本身,属于重量级操作,通常只在 CI(自动化的 "Updated API Reference" job)中执行。
依赖顺序注意:generated/依赖docs-utils与build-scripts两个包——如果修改了 slug 逻辑或侧边栏生成器,需先重建这两个包(yarn workspace docs-utils build、yarn workspace build-scripts build),再运行yarn prep。相关命令与依赖可对照 www/apps/api-reference/package.json(如prep、upload:r2、build:cloudflare等脚本)查看。
开发 / 测试 / 构建
yarn dev # needs NEXT_PUBLIC_BASE_URL + NEXT_PUBLIC_BASE_PATH (=/api) yarn test # vitest (components/providers/utils) yarn build # next build (also lints)yarn dev需要两个环境变量:NEXT_PUBLIC_BASE_URL与NEXT_PUBLIC_BASE_PATH(=/api)。为降低本地启动摩擦,lib/index.ts 镜像了 config/index.ts 与 next.config.mjs 的默认值(http://localhost:3000与/api),即使未设置环境变量也能在 dev 下工作。yarn test使用 vitest,覆盖 components/providers/utils 三部分,仓库中已有大量__tests__用例,例如 providers/tests、components/Tags/tests、utils/tests/get-spec-versions.test.ts 与 app/versions/tests。yarn build即next build,同时执行 lint。Cloudflare 部署场景另有build:cloudflare(node ./scripts/prepare.mjs && opennextjs-cloudflare build)。
工程约定(Conventions)
- 代码风格:无分号、双引号、2 空格缩进(Prettier)。
- 文件命名:kebab-case;组件位于
components/<Name>/index.tsx。 - 绝不手工编辑
generated/——一律重新生成。在specs/中,只有 descriptions 可手工编辑,其余全部由 API 路由的请求/响应类型生成。
结语:给读者的实践建议
如果你需要在该仓库中为该站点新增一个端点、调整一个 URL 或排查"深链停在 schema 上"之类的问题,按本文梳理的链路走即可:先改 packages/medusa/src/api 的路由类型(或仅编辑specs/中的描述)→ 在www/utils跑yarn generate:oas刷新规范 → 在本应用跑yarn prep重建 slug/侧边栏产物 →yarn dev本地验证。理解"slug 单一事实来源"与"Layout 保持挂载 + 滚动锁"两个设计支柱,就能把握住这个站点绝大部分的架构决策。
【免费下载链接】medusaThe world's most flexible commerce platform for agents and developers项目地址: https://gitcode.com/GitHub_Trending/me/medusa
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考