ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

vinext 兼容性压力测试实战:用 pages-router-complex 演练大型 Pages Router 企业应用迁移

vinext 兼容性压力测试实战:用 pages-router-complex 演练大型 Pages Router 企业应用迁移 后端Web框架SSR【免费下载链接】vinextVite plugin that reimplements the Next.js API surface — deploy anywhere项目地址https://gitcode.com/gh_mirrors/vi/vinext点击查看免费下载导读pages-router-complex 是 vinext基于 Vite 重新实现 Next.js 接口面、可部署到任意平台的插件专门构造的“故意绕弯子”的 Pages Router 兼容性靶场用于检验真实大型、老旧企业级 Pages Router 代码库中才会出现的模式能否在 vinext 下正常运行。本文将逐条拆解该示例应用所覆盖的路由、数据层、中间件、国际化、缓存、客户端导航等复杂模式并结合仓库源码与 e2e 测试给出可复用的实战参考。一、示例定位既是兼容性目标也是可运行的真应用examples/pages-router-complex/README.md 开篇就明确了它的双重身份它不是一个玩具 demo而是 vinext 的兼容性目标compatibility target。其中刻意采用的模式“只在大型、老旧、企业级 pages-router 代码库中才会浮现”。它的工程配置方式与vinext init脚手架完全一致vinext cloudflare/vite-plugin两个 Vite 插件KV 数据缓存VINEXT_KV_CACHEWorkers CDN 缓存Images 优化器以vinext/server/fetch-handler作为 worker 入口的wrangler.jsonc。其可信度锚点在于“必须能在真实 Next.js 下正确运行pnpm dev:next”。运行于 vinextpnpm dev是目标而非尚未实现的承诺——tests/e2e/pages-router-complex/ 下的 e2e 套件记录了目标行为今天在 vinext 下可能仍有失败用例README 明确说明这一事实见下文的 Known findings 一节。这与 vinext 本身的定位一致根目录 packages/vinext/package.json 描述为 “Run Next.js apps on Vite. Drop-in replacement for the next CLI.”并且通过 exports 映射暴露了./server/fetch-handler等 worker 入口packages/vinext/src/index.ts 也登记了该入口正是本示例wrangler.jsonc中main: vinext/server/fetch-handler所引用的模块。二、部署形态按vinext init脚手架搭建的 Cloudflare 工程examples/pages-router-complex/wrangler.jsonc 展示了完整的 Cloudflare 部署配置是理解本示例运行时环境的关键{ $schema: node_modules/wrangler/config-schema.json, name: pages-router-complex, compatibility_date: 2026-04-01, compatibility_flags: [nodejs_compat], main: vinext/server/fetch-handler, // Load this one runtime secret from .dev.vars/.env/process.env during local // development without exposing the rest of the host process environment. secrets: { required: [ATLAS_PURGE_BEARER] }, assets: { directory: dist/client, not_found_handling: none, binding: ASSETS }, cache: { enabled: true }, images: { binding: IMAGES }, kv_namespaces: [ { binding: VINEXT_KV_CACHE, id: your-kv-namespace-id } ] }几个值得注意的细节main指向vinext/server/fetch-handler即 vinext 的 fetch 风格请求处理器作为 Worker 的入口模块secrets.required声明了运行必需的密钥ATLAS_PURGE_BEARERexamples/pages-router-complex/pages/api/purge.page.ts 中用作 memo 缓存清理端点的 bearer 校验compatibility_date固定在 2026-04-01 而非脚手架的“今天”这是受 workerd 二进制版本上限约束的见 Known findingsKV 命名空间VINEXT_KV_CACHE是 vinext 数据缓存的底层存储id需替换为你自己的命名空间 ID。Vite 侧配置在 examples/pages-router-complex/vite.config.mtsimport { fileURLToPath } from node:url; import { defineConfig } from vite; import vinext from vinext; import { cloudflare } from cloudflare/vite-plugin; import { kvDataAdapter } from vinext/cloudflare/cache/kv-data-adapter; import { cdnAdapter } from vinext/cloudflare/cache/cdn-adapter; import { imagesOptimizer } from vinext/cloudflare/images/images-optimizer; export default defineConfig({ plugins: [ vinext({ cache: { data: kvDataAdapter(), cdn: cdnAdapter() }, images: { optimizer: imagesOptimizer() }, }), cloudflare(), ], resolve: { alias: { atlas: fileURLToPath(new URL(./lib, import.meta.url)), }, }, });这里atlas/*别名属于工具链层webpack 侧Next.js ground-truth 运行由 next.config.js 的 webpack hook 注入Vite 侧由resolve.alias注入而 tsconfig 的pathsexamples/pages-router-complex/tsconfig.json仅对类型检查器保持权威。这是应对 TypeScript 7 原生预览版下 Next 自带 tsconfig-paths 支持退化问题的双保险见 Known findings。三、被刻意压测的模式清单本示例的核心骨架README 逐条列举了本示例“刻意绕弯”的模式下面按主题组织并结合源码逐一展开。3.1 非标准pageExtensions全后缀路由examples/pages-router-complex/next.config.js 配置pageExtensions: [page.tsx, page.ts],其后果是每个可路由文件都带后缀pages/_app.page.tsx、pages/_document.page.tsx、pages/404.page.tsx、pages/api/status.page.ts等非页面辅助文件可以与页面文件同处一目录如pages/shell-initial-props.ts就是_app.page.tsx的辅助模块但不会被当成路由特殊文件也带后缀middleware.page.ts、instrumentation.page.ts同样遵循该命名约定。值得注意的是这一约定本身就会触发 Next.js 16.2.7 Turbopack 的编译失败全局 CSS-in-_app 校验在_app被重命名为_app.page.tsx时误报因此该应用的dev:next/build:next脚本都显式追加了--webpackexamples/pages-router-complex/package.json。3.2 App 壳层getInitialProps共享服务端数据路径examples/pages-router-complex/pages/_app.page.tsx 在 App 组件上挂载了App.getInitialProps buildShellInitialProps。关键点在于自定义 App 携带getInitialProps会关闭所有页面的自动静态优化automatic static optimisation让每个页面都强制走同一条服务端数据路径并且在每次客户端导航时都会重新执行——这正是 README 强调“在 App 壳上存在 getInitialProps 本身就是要点”的原因。examples/pages-router-complex/pages/shell-initial-props.ts 实现了这一数据函数其行为组合非常典型通过内存 TTL memoiser 拉取 masthead/baseboard 壳层数据核心实现在 examples/pages-router-complex/lib/chrome/fetch-chrome-bundle.ts它用 examples/pages-router-complex/lib/memo/memo-cache.ts 的buildResultCache包装了一次“nav-tree alpha-index”的并发加载deriveKey按ctx.zone.id缓存maxAgeSeconds: 60 * 55 分钟shouldStore仅在 navTree 存在时才写入embedded-shellcookie 标记请求完全跳过该 fetchinsideAppShell在 examples/pages-router-complex/lib/client-state/cookies.ts 中实现——服务端读请求 Cookieatlas-shell1客户端读document.cookie两者一致地返回布尔值fetchChromeBundle收到insideShell后直接returndraft 模式绕过 memofetchChromeBundle中ctx.draft ? loadNavPayloadFresh(ctx) : loadNavPayload(ctx)即预览请求永远走未缓存的新鲜加载examples/pages-router-complex/lib/chrome/fetch-chrome-bundle.ts读取 env 派生设置readPublicSettingsForData()来自 examples/pages-router-complex/lib/runtime-settings/settings.ts把ATLAS_*前缀的环境变量按“公开子集”收敛为PublicSettings密钥ATLAS_DATA_EDGE_TOKEN、ATLAS_PURGE_BEARER等通过scopedSecret(credentialScope, name)按产品作用域解析客户端导航重跑时发出 beaconrunningInBrowser() emitBeacon(shell-props-client-refetch, { priorUrl })使“壳层 props 在每次客户端导航时重跑”这一事实可被观测。3.3 特殊文件的额外具名导出本示例在“特殊文件”上塞入了与职责无关的额外导出用于压测框架对这类文件的宽容度_app 的条件顶层 await 导出examples/pages-router-complex/pages/_app.page.tsx 末尾export { outboundStub } from atlas/outbound-stub/outbound-stub。该值来自 examples/pages-router-complex/lib/outbound-stub/outbound-stub.ts仅在typeof window undefined process.env.STUB_OUTBOUND 1时才是一个顶层 await 的 Promise动态 import 安装 fetch 打桩其余环境为undefined——测试专用模块因此永远不进入浏览器 bundle。middleware 的无关异步 loaderexamples/pages-router-complex/middleware.page.ts 在middlewareconfig之外还导出了loadTrialsManifesttrials 代理的 manifest 加载器让 edge 与 server 代码共享同一模块。配套地next.config.js的 webpack hook 用NormalModuleReplacementPlugin将/outbound-stub\/install-outbound-stub/替换为./void-module.js注释明确说明“测试专用模块绝不能进入浏览器 bundle在模块解析层面替换比 alias 更可靠”examples/pages-router-complex/next.config.js。3.4 基于类的_document请求态驱动 HTML 属性examples/pages-router-complex/pages/_document.page.tsx 采用类组件 自定义getInitialProps的形式调色板派生自ctx.req.urlpaletteForPath(ctx.req?.url || /)html lang派生自 zonehtmlLangFor(zone.language)zone 来自zoneFromRouteQuery(ctx.query)examples/pages-router-complex/lib/zones/zone.tsen-US→en、en-CA→enbeforeInteractive脚本trials-agent外部 URL、rum-probecrossOriginanonymous、trials-manifest-bootstrapdangerouslySetInnerHTML内联 bootstrap 片段来自 examples/pages-router-complex/lib/trials/trials.tsx 的trialsBootstrapSnippet原始内联scriptbody 内直接手写两个dangerouslySetInnerHTML脚本设置document.body.dataset.scripted、为旧浏览器 polyfillArray.prototype.atbody上的 data 属性data-stackatlas与data-palette{palette}。3.5 中间件流水线CDN 前缀、图片 403、硬导航响应、draft 转发、zone 路由examples/pages-router-complex/middleware.page.ts 用正则 matcher 形式收敛匹配范围export const config { matcher: /((?!_next/static|favicon.ico).*), };流水线顺序注释明确“Infra short-circuits come first”CDN 前缀重写/atlas/cdn/_next/*→/_next/*NextResponse.rewrite对应next.config.js中 prod-only 的assetPrefix: /atlas/cdn//_next/image硬 403NextResponse.json({ error: ... }, { status: 403 })——因为本应用所有next/image都使用自定义 loader 直连媒体代理见 3.10框架优化端点永不被使用403 才是安全的/_next/data原始请求返回 JSONhardNavTo把/next/data/buildId/page.json剥成页面 URL 并作为{ hardNavTo }返回配合skipMiddlewareUrlNormalize: true框架不得先把数据 URL 归一化成页面路径由_app.page.tsx中的hardNavTo分支window.location.assign完成硬导航editor-draft 请求头重定向x-editor-draft: true且尚无__prerender_bypasscookie 时302 到/api/draft?draftonlandingpathnameAPI 路由的 draft-cookie 擦洗scrubDraftCookiesForServiceRoutes(req)examples/pages-router-complex/lib/draft/scrub-draft-cookies.ts在pathname.startsWith(/api/)且非/api/draft且携带__prerender_bypass时从 cookie 头中过滤掉__prerender_bypass与__next_preview_data然后NextResponse.next({ request })继续——因为框架的 API 路由管道会“急切地”校验这两个 cookie 并在不匹配时清除它们多 zone draft 场景下会误伤在中间件层过滤可绕开该重置单段 zone 重写/重定向ushome zone永远不出现在公开 URL——显式写出/us/...会被 307 重定向到无前缀规范形ca直接透传并打x-zone: ca响应头无 zone 前缀的路径被 rewrite 进 home zone 并打x-zone: us。这套 zone 行为由 tests/e2e/pages-router-complex/zone-routing.spec.ts 完整覆盖/返回 200 且x-zone: us/us/journal重定向到/journal307/308/ca/journal直接 200 且x-zone: ca/api/status不带x-zone头。3.6[zone]路由维度 真正的 i18next/react-i18next 运行时zone 模型examples/pages-router-complex/lib/zones/zone.ts 定义了AudienceZone.NAus/en-US/America/New_York与AudienceZone.CAca/en-CA/America/TorontozoneFromRouteQuery把[zone]路由参数映射为 zone未知值回落 home zonei18next 运行时examples/pages-router-complex/lib/zones/i18n.ts 通过createInstance()为每个请求/标签页构建隔离实例initImmediate: false使初始化同步——SSR 首遍即可渲染已翻译文案en-CAbundle 刻意不完整只有lookupLabel: Look something up, eh缺失 key 在运行时回落到en-US基础语言zone 感知的 next/link 包装examples/pages-router-complex/lib/zones/zoned-link.tsx 的ZonedLink用useAudienceZone()examples/pages-router-complex/lib/zones/use-audience-zone.ts从 router.query 读 zone计算zonedHrefhome zone 输出无前缀 URL非 home zone 输出/slugbare而/api/、/legacy/服务路径永不 zone 前缀化。3.7 Catch-all 静态兄弟路由的优先级博弈gallery/[...facets]examples/pages-router-complex/pages/[zone]/gallery/[...facets].page.tsx是 catch-all但必须让位于静态兄弟路由gallery/curated/first.page.tsxexamples/pages-router-complex/pages/[zone]/gallery/curated/first.page.tsx与gallery/directory/a-z.page.tsx——注释明确“Static sibling that must beat thegallery/[...facets]catch-all”facet 去重重定向isMalformedTrail检测相邻重复段如/gallery/skies/skiesredirectForMalformedTrail302 到去重后的墙examples/pages-router-complex/helpers/facet-guards.ts字符级清理重定向scrubWallPath小写化、空白转短横、合并连续短横且重定向目标是公开 URLhome zone 剥掉内部前缀非 home zone 保留不可接受的查询串仍返回可缓存 404isFacetQueryAcceptable拒绝重复参数除facet/facets与非法page过深 trail 永久坍缩超过两段时permanent: true重定向到前两段per-wall surrogate TTL 覆盖WALL_TTL_SECONDS[/gallery/skies/clips] 3600默认DEFAULT_WALL_TTL_SECONDS 10800通过applyCachePolicy写入响应头。3.8 动态/静态/动态“三明治”路由[collection]/item/[assetId]examples/pages-router-complex/pages/[zone]/[collection]/item/[assetId].page.tsx是“dynamic/static/dynamic”三明治路由。其 page-data 函数按目录记录的内容决定三种模板分支非纯数字assetId先 404/^\d$/.test(assetId)目录未命中或记录已下线record.retiredOn返回{ notFound: true }TEMPLATE_BY_KINDclip/pack选模板默认standardclip额外带streamManifestUrlm3u8pack额外带packContents记录内的 bundle 数组。3.9 gSSP 的 metering HOF、CDN 缓存头、条件 CSPmetering HOFexamples/pages-router-complex/lib/beacon/meter-server-props.ts 的meterServerProps包装每个页面的getServerSideProps按结果形态记录 tallynotFound→ 404、redirect→ 307/308、异常 → 500并原样重抛错误。所有页面导出meterServerProps(pageData)而非裸函数tally 实现examples/pages-router-complex/lib/beacon/tally.ts 用bot|crawl|spider|slurp|headless正则判定爬虫 UA键为METHOD path statusCode crawlerboolCDN 缓存头examples/pages-router-complex/lib/edge-policy/policy.ts 封装Surrogate-Control含deltanoop注释说明 edge 层无法做 delta 编码再校验、Surrogate-Key与浏览器Cache-Control以及markUncacheableprivate, no-store条件 CSP 响应头副作用gallery 页通过tightAssetPolicyArmexamples/pages-router-complex/lib/trials/trials.tsx判断tight_asset_policy试验臂启用时调用sendTightAssetPolicyHeader(context, policyArm.reportOnly)examples/pages-router-complex/helpers/asset-policy.ts无数据获取函数的页面examples/pages-router-complex/pages/[zone]/detail-tools/client-flags.page.tsx 没有任何 gSSP/gIP但仍穿过 App 壳层 gIP因为自定义 App 关闭了自动静态优化。它是纯客户端行为切换路由/调试 cookie 的 radio/checkbox且刻意把 cookie 读取放进useEffect——因为“服务端渲染没有 cookie渲染期读 cookie 会撕裂 hydration”。3.10 服务端快照数据层Server-snapshot data layer这是本示例最具“企业架构”味道的部分链路完整gSSP 通过服务端 handle 执行 opsexamples/pages-router-complex/lib/graph-handle/server.ts 的openServerGraphHandle返回run(op, variables)每次执行结果写入传入的prefetchStore快照随 page props 传递prefetch.snapshot()序列化进 propsexamples/pages-router-complex/lib/graph-handle/prefetch-state.tsopCacheKey形如op({...})浏览器 handle 以快照播种hydration 从内存读examples/pages-router-complex/lib/graph-handle/react.tsx 的useBrowserGraphHandle用useState(() prefetchStore({ browser: true, seedState: graphSnapshot }))创建一次handle按 handle 名后缀选重试阶梯GALLERY/LOOKUP 3 次、DETAIL 2 次、默认 1 次useGraphOp内存优先命中快照则同步返回{ data, inFlight: false, fromSnapshot: true }未命中才 POST 到>pnpm dev # vinext dev server当前需要 RELEASE_TAG见下 pnpm dev:next # 真实 Next.js dev serverground truth--webpack见下 pnpm build # vinext buildRELEASE_TAG 内联设置 pnpm build:next # next build # 行为套件服务端在 vinext 下自动启动 PLAYWRIGHT_PROJECTpages-router-complex pnpm run test:e2e两条注意点dev/build需要RELEASE_TAGbuild脚本内联RELEASE_TAGlocaldev脚本本身没有设置README 明确“needs RELEASE_TAG for now”。原因见 Known findings——vinext dev 会在启动时调用generateBuildId而它要求RELEASE_TAGdev:next/build:next带--webpack规避 Next.js 16.2.7 Turbopack 对_app.page.tsx的全局 CSS 校验误报。e2e 套件本身按主题拆成了 15 个 spec 文件tests/e2e/pages-router-complex/api-routes、asset-view、cache-policy、client-routing、dataless-page、document、draft-mode、gallery-guards、graph-snapshot、hydration-extras、image-and-links、middleware-extras、navigation-hooks、shell-initial-props、zone-routing——它们正是上文各模式的可执行行为契约。五、已知发现Known findings诚实记录的兼容性边界README 的 Known findings 一节记录了四条事实是评估 vinext 成熟度的第一手材料Next.js 16.2.7 Turbopack 编译失败当pageExtensions把_app改名为_app.page.tsx时全局 CSS-in-_app 校验会误报。因此dev:next/build:next强制--webpackTypeScript 7native previewNext 自身 tsconfig-paths 支持在其下退化所以atlas/*别名被显式接进两个打包器webpack hook Viteresolve.aliastsconfigpaths不含已被移除的baseUrl保持对类型检查器权威vinext devCloudflare 插件当前 65/73 spec 通过8 个已知缺口全部标记为test.fixmetests/e2e/pages-router-complex/ 下各 spec 文件中的test.fixme使通过面可以跑进 CI。缺口包括generateBuildId在 dev 启动时被调用Next.js 只在构建期调用——e2e server 通过导出RELEASE_TAG来补偿shallow routing router.events含一个破坏页面交互性的 hydration 连锁反应next/image自定义 loader/fill路径history.replaceState-beside-the-router 的混合模式purge 端点的未授权 POST 响应。workerd 二进制版本上限固定的 workerd 把compatibility_date封顶在 2026-04-08因此wrangler.jsonc钉在 2026-04-01 而非脚手架的“今天”。这些test.fixme的分布可以在 spec 文件中直接检索到例如api-routes.spec.ts的test.fixme(purge requires the bearer token, ...)、client-routing.spec.ts的test.fixme(changing sort shallow-navigates without re-running gSSP, ...)、navigation-hooks.spec.ts的test.fixme(filter changes update the URL via history.replaceState without navigating, ...)等——对想要参与 vinext 兼容性建设的读者这些标记就是现成的“待办清单”。六、目录结构解读README 的 Layout 一节给出了三层划分与仓库实际文件一一对应lib/应用内部平台库通过 tsconfig 路径别名atlas/*按 monorepo 共享包的方式消费包括 zoneszone 路由 i18next 运行时、edge-policyCDN 头、memoTTL 结果缓存、beaconmetrics/RUM、graph-handle数据层、chromemasthead/baseboard、wiring blocksprovider 金字塔与框架、trials、draft 等。其中wiring/stack.tsxexamples/pages-router-complex/lib/wiring/stack.tsx的注释点明了 provider 金字塔的嵌套顺序是承重结构sign-in copy formatting viewport runtime-settings critique graph-handle edge-probe trials side-panelhelpers/、surfaces/应用级 page-data 辅助函数asset-policy、facet-guards、front-door-ttl与页面模板asset-view-templatepages/前述完整的路由树。七、把这套示例用起来迁移对照与自检清单如果你正面临“把大型、老旧的 Pages Router 应用迁到 vinext/Cloudflare”的任务可以把这个示例当作一份可执行的验收清单自定义pageExtensions是否被你的打包器正确解析特殊文件_app/_document/middleware/instrumentation是否带后缀App 壳层getInitialProps是否如期关闭自动静态优化、是否在客户端导航重跑中间件流水线中的 rewrite/redirect/JSON 响应、skipMiddlewareUrlNormalize语义是否一致catch-all 与静态兄弟的优先级、重定向与 404 的可缓存性是否一致gSSP 包装 HOF、Surrogate-Control/Surrogate-Key等自定义响应头是否原样落地服务端快照 客户端内存播种的数据层是否让 hydration 免于重复请求next/image自定义 loader 下中间件 403 是否安全API 路由的 draft 网关、bearer 门控、上游代理等边界行为是否一致。需要特别强调的是本示例的运行前提是 Cloudflare Workers 环境wrangler.jsonc、cloudflare/vite-plugin、KV/CDN/Images 绑定并且dev/build需要RELEASE_TAG、wrangler.jsonc中 KV 命名空间id需要替换——这些都在 examples/pages-router-complex/ 内可查证是动手跑通之前必读的环境约束。赞分享后端Web框架SSR【免费下载链接】vinextVite plugin that reimplements the Next.js API surface — deploy anywhere项目地址https://gitcode.com/gh_mirrors/vi/vinext点击查看免费下载相关推荐Next.js 从 Pages Router 完整迁移到 App Router以博客应用的迁移任务为实战范本Next.js 从 Pages Router 完整迁移到 App Router以博客应用的迁移任务为实战范本 本文以 Next.js 仓库中 evals/ev前端后端Web框架SSR前端构建5分钟终极指南让你的Windows任务栏变透明桌面美化从此简单5分钟终极指南让你的Windows任务栏变透明桌面美化从此简单 想让你的Windows桌面焕然一新吗你是否厌倦了那个一成不变的黑色或白色任务栏希望它能够后端Web框架SSR在 Next.js Pages Router 项目中集成 Convex 与 Auth0nextjs-pages-router 示例应用全解析在 Next.js Pages Router 项目中集成 Convex 与 Auth0nextjs pages router 示例应用全解析 本文以仓库中的数据库后端上一篇探索高效API管理工具Manage-FastAPI下一篇Grafana Tempo 1.4 版本发布解读服务端指标、trace 查询性能优化与升级指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表