TanStack Start 中文文档
快速开始

从 Next.js 迁移

本指南提供将项目从 Next.js App Router 迁移到 TanStack Start 的逐步流程。我们尊重 Next.js 的强大特性,并力求让这次过渡尽可能顺畅。

分步指南(基础篇)

这份分步指南概述了如何将你的 Next.js App Router 项目迁移到 TanStack Start。目标是帮助你理解迁移过程中的基本步骤,以便你能根据自己项目的具体需求进行调整。

前置条件

开始之前,本指南假设你的项目结构是这样的:

├── next.config.ts
├── package.json
├── postcss.config.mjs
├── public
│   ├── file.svg
│   ├── globe.svg
│   ├── next.svg
│   ├── vercel.svg
│   └── window.svg
├── README.md
├── src
│   └── app
│       ├── favicon.ico
│       ├── globals.css
│       ├── layout.tsx
│       └── page.tsx
└── tsconfig.json

或者,你可以克隆下面这个起始模板跟着一起做:

npx gitpick nrjdalal/awesome-templates/tree/main/next.js-apps/next.js-start next.js-start-er

这是一个使用 App Router 的基础 Next.js 应用,我们将把它迁移到 TanStack Start。

1. 移除 Next.js

首先,卸载 Next.js 并删除相关的配置文件:

npm uninstall @tailwindcss/postcss next
rm postcss.config.* next.config.*

2. 安装所需依赖

TanStack Start 基于 TanStack Router,构建工具支持 ViteRsbuild。下面的 Vite 方案把 Nitro 作为部署插件一起安装了。

npm i @tanstack/react-router @tanstack/react-start nitro vite @vitejs/plugin-react

接着安装你想用的 Tailwind CSS 构建工具集成:

npm i -D @tailwindcss/vite tailwindcss

3. 更新项目配置

依赖装好之后,更新你的项目配置文件以适配 TanStack Start。

{
  "type": "module",
  "scripts": {
    "dev": "vite dev",
    "build": "vite build",
    "start": "node .output/server/index.mjs"
  }
}
vite.config.ts
import { defineConfig } from 'vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import viteReact from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import { nitro } from 'nitro/vite'

export default defineConfig({
  server: {
    port: 3000,
  },
  resolve: {
    // Enables Vite to resolve imports using path aliases.
    tsconfigPaths: true,
  },
  plugins: [
    tailwindcss(),
    tanstackStart({
      srcDirectory: 'src', // This is the default
      router: {
        // Specifies the directory TanStack Router uses for your routes.
        routesDirectory: 'app', // Defaults to "routes", relative to srcDirectory
      },
    }),
    viteReact(),
    nitro(),
  ],
})
postcss.config.mjs
export default {
  plugins: {
    '@tailwindcss/postcss': {},
  },
}

默认情况下,routesDirectory 的取值是 routes。为了与 Next.js App Router 的约定保持一致,你也可以把它设为 app

4. 改造根布局

注意

TanStack Start 采用与 Remix 类似的路由方式,并做了一些调整以通过令牌(tokens)支持嵌套结构和特殊特性。更多内容请看路由概念指南。

不要用 layout.tsx,改为在 src/app 目录下创建一个名为 __root.tsx 的文件。这个文件将作为应用的根布局。

  • src/app/layout.tsx 改成 src/app/__root.tsx
- import type { Metadata } from "next"
import {
  Outlet,
  createRootRoute,
  HeadContent,
  Scripts,
} from "@tanstack/react-router"
import appCss from "./globals.css?url"

- export const metadata: Metadata = { 
-   title: "Create Next App", 
-   description: "Generated by create next app", 
- } 
export const Route = createRootRoute({
  head: () => ({
    meta: [
      { charSet: "utf-8" },
      {
        name: "viewport",
        content: "width=device-width, initial-scale=1",
      },
      { title: "TanStack Start Starter" }
    ],
    links: [
      {
        rel: 'stylesheet',
        href: appCss,
      },
    ],
  }),
  component: RootLayout,
})

- export default function RootLayout({ 
-   children, 
- }: Readonly<{ 
-   children: React.ReactNode
- }>) { 
-   return ( 
-     <html lang="en">
-       <body>{children}</body>
-     </html>
-   ) 
- } 
function RootLayout() {
  return (
    <html lang="en">
      <head>
        <HeadContent />
      </head>
      <body>
        <Outlet />
        <Scripts />
      </body>
    </html>
  )
}

5. 改造首页

不要用 page.tsx,为 / 路由创建一个 index.tsx 文件。

  • src/app/page.tsx 改成 src/app/index.tsx
+ import { createFileRoute } from '@tanstack/react-router'

- export default function Home() { 
+ export const Route = createFileRoute('/')({ 
+   component: Home, 
+ }) 

+ function Home() { 
  return (
    <main className="min-h-dvh w-screen flex items-center justify-center flex-col gap-y-4 p-4">
      <img
        className="max-w-sm w-full"
        src="https://raw.githubusercontent.com/TanStack/tanstack.com/main/public/images/logos/splash-dark.png"
        alt="TanStack Logo"
      />
      <h1>
        <span className="line-through">Next.js</span> TanStack Start
      </h1>
      <a
        className="bg-foreground text-background rounded-full px-4 py-1 hover:opacity-90"
        href="https://tanstack.com/start/latest"
        target="_blank"
      >
        Docs
      </a>
    </main>
  )
}

6. 迁移完成了吗?

在你能运行开发服务器之前,还需要创建一个文件,用于定义 TanStack Start 中 TanStack Router 的行为。

  • 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
}

注意

在这里你可以配置一切,从默认的预加载功能缓存过期策略

如果此时看到一些 TypeScript 错误,别担心;下一步会解决它们。

7. 验证迁移

运行开发服务器:

npm run dev

然后访问 http://localhost:3000。你应该会看到 TanStack Start 的欢迎页,带有它的 logo 和一个文档链接。

如果遇到问题,请复查上面的步骤,确保文件名和路径完全一致。参考实现可以看迁移后的仓库

下一步(进阶)

现在你已经把 Next.js 应用的基础结构迁移到 TanStack Start 了,可以继续探索更高级的特性和概念。

路由概念

路由示例Next.jsTanStack Start
根布局src/app/layout.tsxsrc/app/__root.tsx
/(首页)src/app/page.tsxsrc/app/index.tsx
/posts(静态路由)src/app/posts/page.tsxsrc/app/posts.tsx
/posts/[slug](动态路由)src/app/posts/[slug]/page.tsxsrc/app/posts/$slug.tsx
/posts/[...slug](全匹配路由)src/app/posts/[...slug]/page.tsxsrc/app/posts/$.tsx
/api/endpoint(API 路由)src/app/api/endpoint/route.tssrc/app/api/endpoint.ts

更多内容请看路由概念

动态路由与全匹配路由

在 TanStack Start 中获取动态路由参数非常直接。

- export default async function Page({ 
-   params, 
- }: { 
-   params: Promise<{ slug: string }> 
- }) { 
+ export const Route = createFileRoute('/app/posts/$slug')({ 
+   component: Page, 
+ }) 

+ function Page() { 
-   const { slug } = await params 
+   const { slug } = Route.useParams() 
  return <div>My Post: {slug}</div>
}

注意:如果你写了全匹配路由(比如 src/app/posts/$.tsx),可以通过 const { _splat } = Route.useParams() 访问参数。

类似地,你可以用 const { page, filter, sort } = Route.useSearch() 访问 searchParams

更多内容请看动态路由与全匹配路由

链接

- import Link from "next/link"
+ import { Link } from "@tanstack/react-router"

function Component() {
-   return <Link href="/dashboard">Dashboard</Link> 
+   return <Link to="/dashboard">Dashboard</Link> 
}

更多内容请看链接组件

图片

Next.js 使用 next/image 组件做图片优化。在 TanStack Start 中,你可以使用 Unpic 这个包来实现类似功能,它几乎是开箱即用的替代品。

import Image from 'next/image'
import { Image } from '@unpic/react'
function Component() {
  return (
    <Image
      src="/path/to/image.jpg"
      alt="Description"
      width="600"
      height="400"
      width={600} 
      height={400} 
    />
  )
}

Server Actions 函数

- 'use server'
+ import { createServerFn } from "@tanstack/react-start"

- export const create = async () => { 
+ export const create = createServerFn().handler(async () => { 
  return true
- } 
+ }) 

更多内容请看服务器函数

服务器路由 Handlers

- export async function GET() { 
+ export const Route = createFileRoute('/api/hello')({ 
+  server: { 
+     handlers: { 
+       GET: async () => { 
+         return Response.json("Hello, World!")
+       } 
+    } 
+  } 
+ }) 

更多内容请看服务器路由

字体

- import { Inter } from "next/font/google"

- const inter = Inter({ 
-   subsets: ["latin"], 
-   display: "swap", 
- }) 

- export default function Page() { 
-   return <p className={inter.className}>Font Sans</p> 
- } 

不再用 next/font,改用 Tailwind CSS 的 CSS 优先方案。先安装字体(比如从 Fontsource):

npm i -D @fontsource-variable/dm-sans @fontsource-variable/jetbrains-mono

然后在 src/app/globals.css 中添加:

@import 'tailwindcss' source('../');

@import '@fontsource-variable/dm-sans'; 
@import '@fontsource-variable/jetbrains-mono'; 

@theme inline {
  --font-sans: 'DM Sans Variable', sans-serif; 
  --font-mono: 'JetBrains Mono Variable', monospace; 
  /* ... */
}

/* ... */

获取数据

- export default async function Page() { 
+ export const Route = createFileRoute('/')({ 
+   component: Page, 
+   loader: async () => { 
+     const res = await fetch('https://api.vercel.app/blog') 
+     return res.json() 
+   }, 
+ }) 

+ function Page() { 
-   const data = await fetch('https://api.vercel.app/blog') 
-   const posts = await data.json() 
+   const posts = Route.useLoaderData() 

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}

On this page