react-spring 文档站迁移实战:从 Remix 2 到 React Router 7 框架模式
【免费下载链接】react-spring✌️ A spring physics based React animation library项目地址: https://gitcode.com/gh_mirrors/re/react-spring
React Router 7 的框架模式(framework mode)是 Remix 2 的官方继任者:Remix 团队并入 React Router 之后,同样的 loader/action 路由模型、服务端渲染(SSR)、MDX 路由模块与 Vercel 部署链路被统一收纳进react-router/@react-router/*包体系。本指南以 react-spring 官方文档站(docs/workspace)从 Remix 2.15.2 迁移到 React Router 7 的真实规格为骨架,完整讲解依赖映射、路由约定保留、Vite 插件替换、Vercel 预设切换、Feedback 模块随迁删除,以及一套可复制的端到端验证流程。读完你可以将此方案直接套用到任何 Remix 2 站点迁移中,并知道每一步该改哪些文件、用什么命令验证、守住哪些不变量。
本仓库的specs/003-remix-to-react-router-7/目录保存了这次迁移的完整决策档案(spec.md、research.md、data-model.md、plan.md、quickstart.md、contracts/routes.md),而迁移本身已在仓库中落地:docs/目前运行的正是 React Router 7.15.1。
迁移背景与目标框架选择
为什么是 React Router 7 框架模式
用户的原始诉求很直接:"docs 用的是 Remix 2,应该用 React Router 7,请迁移。"(见 spec.md 的 Input 字段)。技术决策记录在 research.md 的 R1 节:
- 框架模式而非纯客户端模式:React Router 7 存在两种形态——客户端路由库与带 SSR 的框架模式。框架模式才是 Remix v2 的"直系继任者",保留 loader/action、路由模块、服务端入口约定。若退化为纯客户端库会丢失 SSR,直接违反规格中的 FR-005("每个公开文档路由的首屏必须服务端渲染")与 SC-006(Lighthouse SEO 分数)。
- 被否决的备选:留在 Remix 2(用户明确要求迁移)、切换 Next.js / Astro / TanStack Start(内容管线需重写,超出框架替换的范围)、客户端-only React Router 7(违反 SSR 需求)。
迁移的边界:只动 docs,不动动画库
react-spring 是一个 pnpm + Turborepo 单体仓库,库层依赖链是rafz → shared → animated → core → targets → umbrella。本次迁移严格限定在docs/workspace 内,plan.md 的 Constitution Check 明确:库包边界(packages/*、targets/*、demo)零改动,@react-spring/web、@react-spring/rafz仍以普通 workspace 依赖被 docs 消费。最终 diff 自检(T058)要求git diff --stat只出现docs/、根pnpm-lock.yaml、根.gitignore、根CLAUDE.md与规格目录。
依赖映射:@remix-run/*全面退场
这是整个迁移的"总开关"。映射表来自># INV-P1:解析树中零 @remix-run/* 包 grep -E '"@remix-run/' pnpm-lock.yaml || echo "OK: no @remix-run packages remain" # INV-P2:整个 workspace 恰好解析一个 major(应为 7) pnpm ls react-router -r --depth -1 --json \ | jq -r '..|.version? // empty' \ | grep -E '^[0-9]+' \ | cut -d. -f1 \ | sort -u # 期望输出:7
落到当前仓库,docs/package.json 已经处于迁移后的目标态:react-router、@react-router/node、@react-router/serve均为7.15.1,devDependencies 中@react-router/dev、@react-router/fs-routes同为7.15.1,@remix-run/*与@supabase/supabase-js已彻底消失。注意zod与cookie被保留——前者被 docs/scripts/docs/frontmatter.ts 使用,后者被theme.server.ts使用,它们并非 Feedback 模块的专属依赖。
路由约定:用@react-router/fs-routes保住 Remix v2 扁平命名
React Router 7 框架模式要求通过app/routes.ts显式声明路由树,但官方提供了@react-router/fs-routes作为 Remix v2 扁平文件命名(flat-route)的迁移离岸垫。这是本次迁移最精妙的一笔(research.md R3):38 个 MDX 路由文件一个都不改名。
仓库中 docs/app/routes.ts 的实际内容:
import { flatRoutes } from '@react-router/fs-routes' import type { RouteConfig } from '@react-router/dev/routes' export default flatRoutes({ ignoredRouteFiles: ['**/.*', '**/*.css'], }) satisfies RouteConfigignoredRouteFiles原样继承旧 Remix 配置,保证与路由文件同目录的 dotfile 和 CSS Module 不会被误识别为路由。凭借这条规则,docs.components.use-spring.mdx继续解析为/docs/components/use-spring,URL 与线上逐字节一致。被否决的方案包括:改为 RR7 原生嵌套目录约定(要动 39 个文件、破坏已收录的搜索索引 URL、徒增风险)以及手工逐条声明路由(冗长且易漂移)。
这条决策直接服务于规格的核心铁律——URL 契约(contracts/routes.md C1):每个公开路由的 GET 必须返回HTTP 200、Content-Type: text/html; charset=utf-8、非空 HTML 正文,且<title>包含预期片段(如/docs/components/use-spring的 title 含useSpring)。完整映射见>import { vercelPreset } from '@vercel/react-router/vite' import type { Config } from '@react-router/dev/config' export default { presets: [vercelPreset()], } satisfies Config
@vercel/react-router是 Vercel 官方为 RR7 框架模式提供的预设,构建时产出 Vercel 运行时期望的部署产物形态(.vercel/output/**),取代旧@vercel/remix的角色。Vercel 项目本身无需手工改配置,靠框架自动识别即可。
类型引用与 TS 配置
research.md R7 给出机械化的类型导入替换表,react-router统一了这些类型:
| Remix 旧来源 | RR7 新来源 |
|---|---|
import { LoaderFunctionArgs } from '@remix-run/node' | import type { LoaderFunctionArgs } from 'react-router' |
import { ActionFunctionArgs } from '@remix-run/node' | import type { ActionFunctionArgs } from 'react-router' |
import { MetaFunction } from '@vercel/remix' | import type { MetaFunction } from 'react-router' |
import { LinksFunction } from '@vercel/remix' | import type { LinksFunction } from 'react-router' |
import { json } from '@vercel/remix' | 删除,改用原生Response.json(...)或 RR7 的data() |
仓库中 docs/app/root.tsx 是这一替换的活样板:Links、Meta、Outlet、Scripts、ScrollRestoration、useLoaderData以及MetaFunction、LinksFunction、LoaderFunctionArgs、ActionFunctionArgs全部从react-router导入,服务端主题助手getTheme/setTheme仍来自./helpers/theme.server。docs/tsconfig.json的include加入./.react-router/types/**/*(react-router typegen的产物目录),docs/env.d.ts的三斜线引用换成@react-router/node。
构建产物路径与 Turborepo 缓存配置
RR7 的默认构建输出与 Remix 2 不同:build/client/(带 hash 的静态资源)+build/server/(服务端 bundle),不再产生 Remix 时代的public/build/。因此 docs/turbo.json 必须同步更新,否则 Turbo 会用过期的 glob 静默误缓存(research.md R9)。仓库当前配置:
{ "extends": ["//"], "tasks": { "build": { "inputs": [ "app/**", "public/**", "react-router.config.ts", "vite.config.mts" ], "outputs": ["build/**", ".react-router/**"] } } }两点正确性收益:outputs的build/**一个 glob 同时覆盖 client 与 server;inputs相比旧的["app/**"]补上了public/**(静态资源)与新配置文件,避免缓存失效判断失真。.react-router/**是 typegen 产物,放进outputs让 Turbo 可缓存生成类型。根.gitignore增加.react-router/一行,防止类型生成产物入库。
脚本与开发者工作流(US3)
脚本映射见 research.md R11,仓库 docs/package.json 已处于目标态:
| 迁移前 | 迁移后 |
|---|---|
"build": "remix vite:build" | "build": "react-router build" |
"dev": "concurrently \"pnpm dev:remix\" \"pnpm scripts:watch\"" | "dev": "concurrently \"pnpm dev:rr\" \"pnpm scripts:watch\"" |
"dev:remix": "vite dev" | "dev:rr": "react-router dev" |
"start": "remix-serve ./build/server/index.js" | "start": "react-router-serve ./build/server/index.js" |
"test:ts": "tsc --noEmit" | "test:ts": "react-router typegen && tsc --noEmit" |
要点:必须用react-router dev(框架模式的官方开发命令,自带 HMR 与路由模块热重载)而不是裸vite dev;react-router typegen作为test:ts前置步骤,先按路由生成类型再跑tsc --noEmit,避免冷检出时出现虚假类型错误。根目录pnpm docs:dev/pnpm docs:build通过 Turbo 委托到 workspace,无需改动。唯一改名的是dev:remix→dev:rr,docs/README.md与根CLAUDE.md同步更新。
随迁删除:Feedback 模块(FR-019)
规格把"删除页面内 Feedback 模块"与框架迁移捆绑在同一变更中(spec.md FR-019,research.md R13)。理由很务实:不为即将删除的代码支付迁移成本——不必把api.feedback.ts的 Remix 风格 action 签名改写成 RR7 形式,不必验证 Supabase-on-RR7-on-Vercel。删除清单:
docs/app/components/Feedback/Feedback.tsx、Feedback.css.ts及空目录docs/app/routes/api.feedback.ts- docs/app/routes/docs.tsx 中移除 Feedback 的 import 与
<Feedback />JSX docs._index.mdx中宣传"每页反馈按钮"的文案改写,保留 GitHub Discussions 链接作为唯一反馈渠道docs/package.json移除@supabase/supabase-js
验证不变量(INV-F1/F2):源码中grep -rE 'Feedback|/api/feedback|@supabase' docs/app docs/scripts应为空;GET /api/feedback在预览环境必须返回 404(由 catch-all splat 路由接管,而非 500)。Supabase 项目与feedback表在数据层原样保留,SUPABASE_URL/SUPABASE_ANON_KEY环境变量的清理是合入后的手动后续项。
完整验证配方:从本地到 Vercel 预览
quickstart.md 提供了一条由贡献者全流程可跑的验证配方,规格里的四条用户故事(US1 框架替换、US2 URL 保持、US3 本地开发体验、US4 Vercel 部署)都落在其中:
1. 干净安装
git checkout 003-remix-to-react-router-7 pnpm install --frozen-lockfile期望:无 peer-dep 大版本告警;pnpm ls --filter @react-spring/docs | grep remix-run为空。
2. 类型检查:pnpm --filter @react-spring/docs test:ts退出码 0(内部即react-router typegen && tsc --noEmit,对应 FR-017 / SC-008)。
3. 开发服务器:pnpm docs:dev起在http://localhost:3000,输出应出现react-router dev。逐项检查:首页 SSR 渲染且主题首绘无闪烁(FR-013)、/docs/components/use-spring的 MDX 带代码高亮与 callout、控制台无 hydration 警告、编辑 MDX 约 2 秒内热更新、主题切换硬刷新后保持、DocSearch 搜索可用、内嵌 Sandpack 示例可运行。
4. 生产构建与本地预览:pnpm --filter @react-spring/docs build成功后docs/build/client/与docs/build/server/存在,且不产生旧的docs/public/build/;随后pnpm --filter @react-spring/docs start用react-router-serve本地提供产物。
5. Vercel 预览部署:推送分支后 Vercel 自动构建出预览 URL,随后对 contracts/routes.md 的整张路由表做 HTTP 状态码巡检:
PREVIEW_URL=https://<your-preview>.vercel.app while read -r path; do code=$(curl -s -o /dev/null -w "%{http_code}" "$PREVIEW_URL$path") echo "$code $path" done < <(awk -F'|' '/^\| `\// {gsub(/^[ `]+|[ `]+$/, "", $2); print $2}' specs/003-remix-to-react-router-7/contracts/routes.md)所有非 splat 行应打印200;再故意访问一个不存在路径(如/nope)确认 404 页渲染。
6. Lighthouse 抽查:对预览 URL 的/与/docs/components/use-spring跑 Lighthouse,Performance/SEO/Accessibility 相对线上波动不超过 5 分(SC-006),LCP 在同等网络节流下偏差 10% 以内(SC-007)。
7. 依赖树断言:即上文 INV-P1/INV-P2 两条命令,lockfile 无@remix-run/*、react-router仅解析一个 major(7)。
8. 回归与回滚:无数据迁移,回滚即git revert <commit-sha>后重装 lockfile,纯代码与锁文件操作。完成判定还包括:grep -rE '@(remix-run|vercel/remix)' docs/app docs/vite.config.mts docs/env.d.ts为空、docs/app/components/Feedback/目录不复存在(SC-010)。
迁移后的现状核对
对照当前仓库可以确认这次迁移已经完整落地,且与规格高度一致:
- docs/package.json 依赖与脚本均为 RR7 形态(
react-router build、react-router dev、react-router-serve、react-router typegen && tsc --noEmit),版本统一为7.15.1; - docs/vite.config.mts 使用
reactRouter(),无installGlobals(),MDX / Vanilla Extract / tsconfigPaths 插件原样保留; - docs/app/routes.ts 以
flatRoutes()维持 Remix v2 扁平命名,38 个 MDX 路由文件(docs.advanced.*.mdx、docs.components.*.mdx等,见 docs/app/routes 目录)未改名,URL 面与线上逐字节一致; - docs/turbo.json 的 inputs/outputs 已按
build/**+.react-router/**更新; - docs/app/root.tsx 所有运行时与类型导入统一来自
react-router,theme.server.ts保持服务端专用; - 规格 requirements.md 中 Content Quality、Requirement Completeness、Feature Readiness 三组条目全部勾选——没有遗留
[NEEDS CLARIFICATION]标记,成功标准可测量且技术无关,范围显式界定在docs/workspace,Vercel 保持为主机。
从用户故事的验收视角看:US1(现代框架上渲染)由构建、类型检查与本地导航检查覆盖;US2(URL 全量解析)由路由契约巡检覆盖;US3(本地工作流)由dev:rr与 HMR 验证覆盖;US4(Vercel 部署)由预览部署 + 预览巡检覆盖。整个迁移是"小到单分支顺序执行即可、紧耦合到每条故事共享docs/package.json与docs/app/*"的机械性框架替换——真正决定成败的,是那份将 URL 契约、依赖树不变量与首屏 SSR 行为固化为可执行验证的规格文档。
延伸阅读:想深入这次迁移的每一项决策依据与备选方案分析,可继续阅读 research.md(R1–R13 技术决策)、data-model.md(包映射与路由契约的完整数据模型)、plan.md(59 项任务分 7 个阶段的实施计划)与 tasks.md(含并行任务编排与执行顺序依赖)。
【免费下载链接】react-spring✌️ A spring physics based React animation library项目地址: https://gitcode.com/gh_mirrors/re/react-spring
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考