从 Next.js App Router 迁移到 TanStack Start:React 全栈框架逐步迁移指南

从 Next.js App Router 迁移到 TanStack Start:React 全栈框架逐步迁移指南 从 Next.js App Router 迁移到 TanStack StartReact 全栈框架逐步迁移指南【免费下载链接】router A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router导读本文是一份基于 TanStack Start当前仓库 packages/react-start的逐步迁移清单面向希望从 Next.js App Router 迁移到 TanStack Start 的 React 开发者。你将掌握两者在路由、布局、数据获取、Server Actions、API 路由、中间件与 SEO 等维度的概念映射关系并学会用createServerFn、createMiddleware、createStart等核心 API 替换 Next.js 的use server、use client与middleware.ts。读完本文后你可以按顺序完成一次完整的、类型安全的框架迁移。迁移前的三个关键认知TanStack Start 与 Next.js App Router 在架构哲学上有本质差异动手迁移前必须建立三个心智模型否则后续每一步都可能踩坑。1. 默认同构Isomorphic而不是默认服务端关键TanStack Start 默认是同构的——所有代码默认在客户端和服务端两个环境中运行除非你用createServerFn显式隔离服务端逻辑。这与 Next.js Server Components 默认只在服务端运行的模型恰好相反。这意味着从 Next.js 迁移时你不能再依赖组件默认只在服务端跑这个假设。凡是需要服务端专用逻辑如访问数据库、读取私有密钥的代码都必须主动放进createServerFn或createMiddleware().server()中。2. 使用createServerFn而不是use server指令关键TanStack Start 使用createServerFn定义服务端函数不要沿用任何use server或use client指令。从源码看createServerFn.ts 是构建服务端函数的工厂函数未显式指定method时默认GET返回一个支持.middleware()、.validator()、.handler()链式调用的构建器对象。而 react-start 的入口文件 显式重导出createServerFn、createMiddleware、createStart等 API保证这些名称在 Vite SSR 冷启动周期中稳定注册。所以在tanstack/react-start中直接import { createServerFn } from tanstack/react-start即可。3. 类型完全推断绝不手动断言关键TanStack Router/Start 的类型是完全推断的。永远不要 cast不要手动标注已被推断的值。这一点的直接体现是Route.useParams()、Route.useLoaderData()等 Hook参数与加载数据的类型由路由声明自动推导无需像 Next.js 那样手动声明params: { id: string }。迁移前准备按顺序完成以下前置步骤创建迁移分支git checkout -b migrate-to-tanstack-start安装 TanStack Startnpm i tanstack/react-start tanstack/react-router npm i -D vite vitejs/plugin-react移除 Next.jsnpm uninstall next next/font next/image概念映射总表下表是 Next.js App Router 与 TanStack Start 的核心概念一一对应关系是整个迁移过程的地图Next.js App RouterTanStack Startapp/page.tsxsrc/routes/index.tsxapp/layout.tsxsrc/routes/__root.tsxapp/posts/[id]/page.tsxsrc/routes/posts/$postId.tsxapp/api/users/route.tssrc/routes/api/users.ts(server property)use server Server ActionscreateServerFn()use client不需要一切默认同构Server Components默认所有组件同构仅服务端逻辑使用createServerFnnext/navigationuseRouteruseRouter()fromtanstack/react-routernext/linkLinkLinkfromtanstack/react-routernext/head或metadata导出路由上的head属性middleware.tsedgecreateMiddleware()insrc/start.tsnext.config.jsvite.config.tswithtanstackStart()generateStaticParamsprerenderconfig invite.config.tsStep 1Vite 配置用vite.config.ts替换next.config.js// vite.config.ts import { defineConfig } from vite import { tanstackStart } from tanstack/react-start/plugin/vite import viteReact from vitejs/plugin-react export default defineConfig({ plugins: [ tanstackStart(), // MUST come before react() viteReact(), ], })两点注意插件顺序tanstackStart()必须放在react()之前这是构建正确性的硬性要求脚本配置更新package.json中的脚本与模块类型{ type: module, scripts: { dev: vite dev, build: vite build, start: node .output/server/index.mjs } }从源码结构看tanstackStart()插件负责把src/routes/下的文件路由生成到routeTree.gen并把服务端入口打包进.output/server因此build产物由vite build产出生产启动则指向.output/server/index.mjs。Step 2创建 Router 工厂src/router.tsx负责创建路由实例供入口文件与 SSR 端复用// src/router.tsx import { createRouter } from tanstack/react-router import { routeTree } from ./routeTree.gen export function getRouter() { const router createRouter({ routeTree, scrollRestoration: true, }) return router }routeTree.gen由 Vite 插件在开发时自动生成因此不要手写路由树。scrollRestoration: true让路由在导航时自动恢复滚动位置替代 Next.js 的滚动恢复行为。Step 3布局 → 根路由Next.js 的根布局// app/layout.tsx export const metadata { title: My App } export default function RootLayout({ children }) { return ( html body{children}/body /html ) }对应的 TanStack Start 根路由// src/routes/__root.tsx import type { ReactNode } from react import { Outlet, createRootRoute, HeadContent, Scripts, } from tanstack/react-router export const Route createRootRoute({ head: () ({ meta: [ { charSet: utf-8 }, { name: viewport, content: widthdevice-width, initial-scale1 }, { title: My App }, ], }), component: RootComponent, }) function RootComponent() { return ( html head HeadContent / /head body Outlet / Scripts / /body /html ) }迁移要点metadata导出 →head函数Next.js 的export const metadata变为根路由上的head: () ({ meta: [...] })children→Outlet /子路由内容通过Outlet /渲染必须手动放置HeadContent /与Scripts /分别负责注入路由声明的 meta 标签与服务端渲染脚本这在迁移后的检查清单中也会再次验证。Step 4页面 → 文件路由Next.js 动态路由页面// app/posts/[id]/page.tsx export default function PostPage({ params }: { params: { id: string } }) { // ... }TanStack Start 的文件路由// src/routes/posts/$postId.tsx import { createFileRoute } from tanstack/react-router export const Route createFileRoute(/posts/$postId)({ component: PostPage, }) function PostPage() { const { postId } Route.useParams() // ... }关键差异动态段语法使用$param而非[param]参数获取方式通过Route.useParams()获取而不是组件 props且类型自动推断文件路径分隔路由路径在文件名中使用.或/分隔。Step 5Server Actions → Server FunctionsNext.js 的 Server Action// app/actions.ts use server export async function createPost(formData: FormData) { const title formData.get(title) as string await db.posts.create({ title }) }TanStack Start 的服务端函数// src/utils/posts.functions.ts import { createServerFn } from tanstack/react-start export const createPost createServerFn({ method: POST }) .validator((data) { if (!(data instanceof FormData)) throw new Error(Expected FormData) return { title: data.get(title)?.toString() || } }) .handler(async ({ data }) { await db.posts.create({ title: data.title }) return { success: true } })从 createServerFn.ts 的实现看.validator()会以函数方式把校验逻辑挂到构建器的validator/inputValidator上并返回新的构建器校验器只在服务端执行见 createServerFn.ts 中if (validator env server)分支且在请求阶段运行于中间件链中。服务端函数内部通过executeMiddleware按client→server顺序执行中间件链createServerFn.ts最终把服务端handler的返回结果与sendContext回传客户端。另外.handler()接收的data是已通过校验器处理后的数据类型也被完整推断——不需要像 Next.js 那样手动as string断言。Step 6数据获取模式改造Next.js 的 Server Component 在服务端直接读取数据库// app/posts/page.tsx (Server Component — server-only by default) export default async function PostsPage() { const posts await db.posts.findMany() return PostList posts{posts} / }TanStack Start 的等价写法是服务端函数 loader// src/routes/posts.tsx import { createFileRoute } from tanstack/react-router import { createServerFn } from tanstack/react-start const getPosts createServerFn({ method: GET }).handler(async () { return db.posts.findMany() }) export const Route createFileRoute(/posts)({ loader: () getPosts(), // loader is isomorphic, getPosts runs on server component: PostsPage, }) function PostsPage() { const posts Route.useLoaderData() return PostList posts{posts} / }这里的设计是loader本身同构可运行在两端但getPosts是服务端函数因此数据获取一定发生在服务端SSR 时服务端函数被直接调用见 createStart.ts 中对serverFns.fetch的注释SSR 期间服务端函数直接调用而非走网络请求客户端导航时则通过内部 fetch 机制调用。Route.useLoaderData()拿到类型完全推断的加载数据。Step 7API 路由 → Server RoutesNext.js 的 Route Handlers// app/api/users/route.ts export async function GET() { const users await db.users.findMany() return Response.json(users) }TanStack Start 用带server属性的文件路由实现同样能力// src/routes/api/users.ts import { createFileRoute } from tanstack/react-router export const Route createFileRoute(/api/users)({ server: { handlers: { GET: async () { const users await db.users.findMany() return Response.json(users) }, }, }, })API 端点文件仍然放在src/routes/下与页面路由共用同一套文件路由约定只是通过server.handlers挂载 HTTP 方法处理器。Step 8导航改造Next.js 的Linkimport Link from next/link ;Link href{/posts/${post.id}}View Post/LinkTanStack Router 的Linkimport { Link } from tanstack/react-router ;Link to/posts/$postId params{{ postId: post.id }} View Post /Link不要把参数插值进to字符串——to只写带$占位的路由路径参数一律通过paramsprop 传入。这样to是类型安全的路由路径写错、参数缺失或类型不匹配都会在编译期报错。Step 9中间件改造Next.js 的 edge 中间件// middleware.ts export function middleware(request: NextRequest) { const token request.cookies.get(session) if (!token) return NextResponse.redirect(new URL(/login, request.url)) } export const config { matcher: [/dashboard/:path*] }TanStack Start 需要手动创建src/start.ts用createMiddlewarecreateStart实现// src/start.ts — must be manually created import { createStart, createMiddleware } from tanstack/react-start import { redirect } from tanstack/react-router const authMiddleware createMiddleware().server(async ({ next, request }) { const cookie request.headers.get(cookie) if (!cookie?.includes(session)) { throw redirect({ to: /login }) } return next() }) export const startInstance createStart(() ({ requestMiddleware: [authMiddleware], }))从 createMiddleware.ts 的源码看createMiddleware默认type: request类型为request | function并支持链式追加.middleware()、.validator()、.client()、.server()。.server()注册的函数只在服务端执行正是鉴权类逻辑的归属地。createStart接收一个返回配置对象的函数其配置项在 createStart.ts 中定义包括requestMiddleware请求级中间件数组即上文挂载的位置functionMiddleware作用于所有服务端函数的函数级中间件serializationAdapters序列化适配器defaultSsr默认 SSR 选项serverFns.fetch服务端函数的自定义 fetch 实现按调用点 后挂载中间件 先挂载中间件 createStart 全局 fetch的优先级生效且仅客户端生效SSR 时直接调用。中间件执行的核心逻辑在executeMiddlewarecreateServerFn.ts它把全局函数中间件与当前链路的中间件扁平化后依次执行客户端环境只运行各中间件的client分支服务端环境只运行server分支服务端还会过滤掉请求阶段已执行过的中间件以避免重复执行。中间件返回Response或redirect时链路会提前终止并回传结果。Step 10Metadata/SEO 改造Next.js 的metadata导出export const metadata { title: Post Title, description: Post description, }TanStack Start 在路由的head函数中声明且能读取 loader 数据实现动态 SEOexport const Route createFileRoute(/posts/$postId)({ loader: async ({ params }) fetchPost(params.postId), head: ({ loaderData }) ({ meta: [ { title: loaderData.title }, { name: description, content: loaderData.excerpt }, { property: og:title, content: loaderData.title }, ], }), })head接收({ loaderData })意味着 SEO 标签可以由加载数据动态生成且这些 meta 会被根路由中的HeadContent /注入到head。迁移完成后的检查清单按顺序逐项验证移除所有use server和use client指令移除next.config.js/next.config.ts移除app/目录由src/routes/替代移除middleware.ts由src/start.ts替代确认代码中不再残留任何next/*导入运行npm run dev并逐一检查所有路由确认服务端专用代码都在createServerFn内部而不是裸写在组件或 loader 中检查Scripts /是否位于根路由的body内常见错误与规避1. 关键沿用 Server Component 心智模型// 错误 — 把组件当成服务端专用Next.js 习惯 function PostsPage() { const posts await db.posts.findMany() // fails on client return div{posts.map(...)}/div } // 正确 — 使用服务端函数 loader const getPosts createServerFn({ method: GET }).handler(async () { return db.posts.findMany() }) export const Route createFileRoute(/posts)({ loader: () getPosts(), component: PostsPage, })因为组件默认同构直接在组件内await db...会在客户端运行时报错必须把数据库访问封装进createServerFn再由 loader 调用。2. 关键继续使用use server指令// 错误 — use server 是 Next.js/React 的模式 use server export async function myAction() { ... } // 正确 — 使用 createServerFn export const myAction createServerFn({ method: POST }) .handler(async () { ... })use server指令在 TanStack Start 中没有意义迁移时必须全部删除并改写成createServerFn。3. 高优先级把参数插值进 Link 的 href// 错误 — Next.js 模式 Link to{/posts/${post.id}}View/Link // 正确 — TanStack Router 模式 Link to/posts/$postId params{{ postId: post.id }}View/Link插值写法会破坏类型安全也无法享受路由路径的编译期校验。延伸阅读react-start SKILL — React Start 的完整搭建指南start-core 服务端函数 — 服务端函数模式详解start-core 执行模型 — 同构执行模型深入解析【免费下载链接】router A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考