从 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,构建工具支持 Vite 或 Rsbuild。下面的 Vite 方案把 Nitro 作为部署插件一起安装了。
npm i @tanstack/react-router @tanstack/react-start nitro vite @vitejs/plugin-react接着安装你想用的 Tailwind CSS 构建工具集成:
npm i -D @tailwindcss/vite tailwindcss3. 更新项目配置
依赖装好之后,更新你的项目配置文件以适配 TanStack Start。
{
"type": "module",
"scripts": {
"dev": "vite dev",
"build": "vite build",
"start": "node .output/server/index.mjs"
}
}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(),
],
})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.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({
- 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>
)
}