从 Next.js 迁移

本指南提供了将项目从 Next.js App Router 迁移到 TanStack Start 的分步过程。我们尊重 Next.js 强大的功能,并旨在使此次过渡尽可能顺畅。

分步指南 (基础)

本分步指南概述了如何将 Next.js App Router 项目迁移到 TanStack Start。目标是帮助您了解迁移过程中涉及的基本步骤,以便您可以根据您的特定项目需求进行调整。

先决条件

在开始之前,本指南假设您的项目结构如下

txt
├── 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
├── 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

或者,您可以通过克隆以下入门模板来跟进。

sh
npx gitpick nrjdalal/awesome-templates/tree/main/next.js-apps/next.js-start next.js-start-er
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 并删除相关配置文件

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

2. 安装所需依赖

TanStack Start 利用 Vite 和 TanStack Router

sh
npm i @tanstack/react-router @tanstack/react-start vite
npm i @tanstack/react-router @tanstack/react-start vite

用于 Tailwind CSS 和使用路径别名解析导入

sh
npm i -D @tailwindcss/vite tailwindcss vite-tsconfig-paths
npm i -D @tailwindcss/vite tailwindcss vite-tsconfig-paths

3. 更新项目配置

现在您已经安装了必要的依赖项,更新您的项目配置文件以与 TanStack Start 配合使用。

  • package.json
json
{
  "type": "module",
  "scripts": {
    "dev": "vite dev",
    "build": "vite build",
    "start": "node .output/server/index.mjs"
  }
}
{
  "type": "module",
  "scripts": {
    "dev": "vite dev",
    "build": "vite build",
    "start": "node .output/server/index.mjs"
  }
}
  • vite.config.ts
ts
// vite.config.ts
import tailwindcss from '@tailwindcss/vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import { defineConfig } from 'vite'
import tsconfigPaths from 'vite-tsconfig-paths'

export default defineConfig({
  server: {
    port: 3000,
  },
  plugins: [
    tailwindcss(),
    // Enables Vite to resolve imports using path aliases.
    tsconfigPaths(),
    tanstackStart({
      tsr: {
        // Specifies the directory TanStack Router uses for your routes.
        routesDirectory: 'src/app', // Defaults to "src/routes"
      },
    }),
  ],
})
// vite.config.ts
import tailwindcss from '@tailwindcss/vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import { defineConfig } from 'vite'
import tsconfigPaths from 'vite-tsconfig-paths'

export default defineConfig({
  server: {
    port: 3000,
  },
  plugins: [
    tailwindcss(),
    // Enables Vite to resolve imports using path aliases.
    tsconfigPaths(),
    tanstackStart({
      tsr: {
        // Specifies the directory TanStack Router uses for your routes.
        routesDirectory: 'src/app', // Defaults to "src/routes"
      },
    }),
  ],
})

默认情况下,routesDirectory 设置为 src/routes。为了与 Next.js App Router 约定保持一致,您可以将其设置为 src/app

4. 调整根布局

TanStack Start 采用类似于 Remix 的路由方法,并进行了一些更改,以支持使用令牌的嵌套结构和特殊功能。在路由概念指南中了解更多信息。

layout.tsx 替换为在 src/app 目录中创建一个名为 __root.tsx 的文件。此文件将作为应用程序的根布局。

  • src/app/layout.tsxsrc/app/__root.tsx
tsx
- import type { Metadata } from "next" // [!code --]
import {
  Outlet,
  createRootRoute,
  HeadContent,
  Scripts,
} from "@tanstack/react-router"
import "./globals.css"

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

- export default function RootLayout({ // [!code --]
-   children, // [!code --]
- }: Readonly<{ // [!code --]
-   children: React.ReactNode // [!code --]
- }>) { // [!code --]
-   return ( // [!code --]
-     <html lang="en"> // [!code --]
-       <body>{children}</body> // [!code --]
-     </html> // [!code --]
-   ) // [!code --]
- } // [!code --]
function RootLayout() {
  return (
    <html lang="en">
      <head>
        <HeadContent />
      </head>
      <body>
        <Outlet />
        <Scripts />
      </body>
    </html>
  )
}
- import type { Metadata } from "next" // [!code --]
import {
  Outlet,
  createRootRoute,
  HeadContent,
  Scripts,
} from "@tanstack/react-router"
import "./globals.css"

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

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

5. 调整主页

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

  • src/app/page.tsxsrc/app/index.tsx
tsx
- export default function Home() { // [!code --]
+ export const Route = createFileRoute('/')({ // [!code ++]
+   component: Home, // [!code ++]
+ }) // [!code ++]

+ function Home() { // [!code ++]
  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/src/images/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.cn/start/latest"
        target="_blank"
      >
        Docs
      </a>
    </main>
  )
}
- export default function Home() { // [!code --]
+ export const Route = createFileRoute('/')({ // [!code ++]
+   component: Home, // [!code ++]
+ }) // [!code ++]

+ function Home() { // [!code ++]
  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/src/images/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.cn/start/latest"
        target="_blank"
      >
        Docs
      </a>
    </main>
  )
}

6. 我们已经迁移了吗?

在运行开发服务器之前,您需要创建一个路由文件,该文件将定义 TanStack Router 在 TanStack Start 中的行为。

  • src/router.tsx
tsx
import { createRouter as createTanStackRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'

export function createRouter() {
  const router = createTanStackRouter({
    routeTree,
    scrollRestoration: true,
  })

  return router
}

declare module '@tanstack/react-router' {
  interface Register {
    router: ReturnType<typeof createRouter>
  }
}
import { createRouter as createTanStackRouter } from '@tanstack/react-router'
import { routeTree } from './routeTree.gen'

export function createRouter() {
  const router = createTanStackRouter({
    routeTree,
    scrollRestoration: true,
  })

  return router
}

declare module '@tanstack/react-router' {
  interface Register {
    router: ReturnType<typeof createRouter>
  }
}

🧠 在这里,您可以配置从默认的预加载功能缓存陈旧性的一切。

如果此时您看到一些 TypeScript 错误,请不要担心;下一步将解决它们。

7. 验证迁移

运行开发服务器

sh
npm run dev
npm run dev

然后,访问 https://:3000。您应该会看到 TanStack Start 欢迎页面及其标志和文档链接。

如果您遇到问题,请回顾以上步骤并确保文件名和路径完全匹配。有关参考实现,请参阅迁移后存储库

后续步骤 (高级)

现在您已将 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 中检索动态路由参数非常简单。

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

+ function Page() { // [!code ++]
-   const { slug } = await params // [!code --]
+   const { slug } = Route.useParams() // [!code ++]
  return <div>My Post: {slug}</div>
}
- export default async function Page({ // [!code --]
-   params, // [!code --]
- }: { // [!code --]
-   params: Promise<{ slug: string }> // [!code --]
- }) { // [!code --]
+ export const Route = createFileRoute('/app/posts/$slug')({ // [!code ++]
+   component: Page, // [!code ++]
+ }) // [!code ++]

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

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

同样,您可以使用 const { page, filter, sort } = Route.useSearch() 访问 searchParams

了解有关动态和全匹配路由的更多信息。

tsx
- import Link from "next/link" // [!code --]
+ import { Link } from "@tanstack/react-router" // [!code ++]

function Component() {
-   return <Link href="/dashboard">Dashboard</Link> // [!code --]
+   return <Link to="/dashboard">Dashboard</Link> // [!code ++]
}
- import Link from "next/link" // [!code --]
+ import { Link } from "@tanstack/react-router" // [!code ++]

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

了解有关链接的更多信息。

服务端 操作 函数

tsx
- 'use server' // [!code --]
+ import { createServerFn } from "@tanstack/react-start" // [!code ++]

- export const create = async () => { // [!code --]
+ export const create = createServerFn().handler(async () => { // [!code ++]
  return true
- } // [!code --]
+ }) // [!code ++]
- 'use server' // [!code --]
+ import { createServerFn } from "@tanstack/react-start" // [!code ++]

- export const create = async () => { // [!code --]
+ export const create = createServerFn().handler(async () => { // [!code ++]
  return true
- } // [!code --]
+ }) // [!code ++]

了解有关服务端函数的更多信息。

服务端路由 处理程序

ts
- export async function GET() { // [!code --]
+ export const ServerRoute = createServerFileRoute().methods({ // [!code ++]
+   GET: async () => { // [!code ++]
    return Response.json("Hello, World!")
  }
+ }) // [!code ++]
- export async function GET() { // [!code --]
+ export const ServerRoute = createServerFileRoute().methods({ // [!code ++]
+   GET: async () => { // [!code ++]
    return Response.json("Hello, World!")
  }
+ }) // [!code ++]

了解有关服务端路由的更多信息。

字体

tsx
- import { Inter } from "next/font/google" // [!code --]

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

- export default function Page() { // [!code --]
-   return <p className={inter.className}>Font Sans</p> // [!code --]
- } // [!code --]
- import { Inter } from "next/font/google" // [!code --]

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

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

使用 Tailwind CSS 的 CSS 优先方法,而不是 next/font。安装字体(例如,来自 Fontsource

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

将以下内容添加到 src/app/globals.css

css
@import 'tailwindcss';

@import '@fontsource-variable/dm-sans'; /* [!code ++] */
@import '@fontsource-variable/jetbrains-mono'; /* [!code ++] */

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

/* ... */
@import 'tailwindcss';

@import '@fontsource-variable/dm-sans'; /* [!code ++] */
@import '@fontsource-variable/jetbrains-mono'; /* [!code ++] */

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

/* ... */

获取数据

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

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

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}
- export default async function Page() { // [!code --]
+ export const Route = createFileRoute('/')({ // [!code ++]
+   component: Page, // [!code ++]
+   loader: async () => { // [!code ++]
+     const res = await fetch('https://api.vercel.app/blog') // [!code ++]
+     return res.json() // [!code ++]
+   }, // [!code ++]
+ }) // [!code ++]

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

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}
我们的合作伙伴
Code Rabbit
Netlify
Neon
Clerk
Convex
Sentry
Prisma
订阅 Bytes

您的每周 JavaScript 资讯。每周一免费发送给超过 10 万开发者。

Bytes

无垃圾邮件。您可以随时取消订阅。

订阅 Bytes

您的每周 JavaScript 资讯。每周一免费发送给超过 10 万开发者。

Bytes

无垃圾邮件。您可以随时取消订阅。