本指南提供了将项目从 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
├── 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
npx gitpick nrjdalal/awesome-templates/tree/main/next.js-apps/next.js-start next.js-start-er
此结构是一个使用 App Router 的基本 Next.js 应用程序,我们将将其迁移到 TanStack Start。
首先,卸载 Next.js 并删除相关配置文件
npm uninstall @tailwindcss/postcss next
rm postcss.config.* next.config.*
npm uninstall @tailwindcss/postcss next
rm postcss.config.* next.config.*
TanStack Start 利用 Vite 和 TanStack Router
npm i @tanstack/react-router @tanstack/react-start vite
npm i @tanstack/react-router @tanstack/react-start vite
用于 Tailwind CSS 和使用路径别名解析导入
npm i -D @tailwindcss/vite tailwindcss vite-tsconfig-paths
npm i -D @tailwindcss/vite tailwindcss vite-tsconfig-paths
现在您已经安装了必要的依赖项,更新您的项目配置文件以与 TanStack Start 配合使用。
{
"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
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。
TanStack Start 采用类似于 Remix 的路由方法,并进行了一些更改,以支持使用令牌的嵌套结构和特殊功能。在路由概念指南中了解更多信息。
将 layout.tsx 替换为在 src/app 目录中创建一个名为 __root.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>
)
}
不是 page.tsx,而是为 / 路由创建一个 index.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>
)
}
在运行开发服务器之前,您需要创建一个路由文件,该文件将定义 TanStack Router 在 TanStack Start 中的行为。
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 错误,请不要担心;下一步将解决它们。
运行开发服务器
npm run dev
npm run dev
然后,访问 https://:3000。您应该会看到 TanStack Start 欢迎页面及其标志和文档链接。
如果您遇到问题,请回顾以上步骤并确保文件名和路径完全匹配。有关参考实现,请参阅迁移后存储库。
现在您已将 Next.js 应用程序的基本结构迁移到 TanStack Start,您可以探索更高级的功能和概念。
路由示例 | Next.js | TanStack Start |
---|---|---|
根布局 | src/app/layout.tsx | src/app/__root.tsx |
/ (主页) | src/app/page.tsx | src/app/index.tsx |
/posts (静态路由) | src/app/posts/page.tsx | src/app/posts.tsx |
/posts/[slug] (动态) | src/app/posts/[slug]/page.tsx | src/app/posts/$slug.tsx |
/posts/[...slug] (全匹配) | src/app/posts/[...slug]/page.tsx | src/app/posts/$.tsx |
/api/endpoint (API 路由) | src/app/api/endpoint/route.ts | src/app/api/endpoint.ts |
了解有关路由概念的更多信息。
在 TanStack Start 中检索动态路由参数非常简单。
- 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。
了解有关动态和全匹配路由的更多信息。
- 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 ++]
}
了解有关链接的更多信息。
- '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 ++]
了解有关服务端函数的更多信息。
- 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 ++]
了解有关服务端路由的更多信息。
- 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)
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
@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 ++] */
/* ... */
}
/* ... */
- 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>
)
}
您的每周 JavaScript 资讯。每周一免费发送给超过 10 万开发者。