Next.js 入门
本文总结了常见的 Next.js 概念与实践,包括项目创建、路由、layout、服务端/客户端组件、数据获取、错误/加载处理、元数据、Server Action 与中间件等。
创建项目
使用官方脚手架创建一个 Next.js 项目:
npx create-next-app@latest注意:Node 环境要求 v18.18 及以上。
路由(基于文件系统)
Next.js 的路由基于文件系统,一个文件夹代表一个路由,因此通常不需要使用 react-router。
创建页面
在 app 目录下创建对应的文件夹,然后在该文件夹中创建 page.tsx(或 page.jsx),并导出一个 React 组件。该页面组件会被插入到对应层级的 layout 组件中。
动态路由
通过在文件夹名中使用中括号创建动态路由,例如:
-[id] |_ work |_ page.js将会对应路由 /:id/work。
路由跳转
- Link 组件:用于声明式导航。
- useRouter:用于编程式导航,提供
push、replace、back、refresh等方法。
useRouter 示例
'use client'import { useRouter } from 'next/navigation';
export default function MyComponent() { const router = useRouter(); return ( <button onClick={() => router.push('/about')}>Go to About</button> );}路由组
使用小括号包裹文件夹名,创建一个路由组用于分组但不作为路由层级,例如:
app/ dashboard/ (layout)/ personal/ page.jsx analyse/ page.jsx路由组用于模块划分和共享布局。
layout(布局)
在任意 app 目录下定义 layout.tsx,导出一个组件作为该层级及以下共享的框架。layout 组件需要接收 children 参数,默认为服务端组件:
import SideNav from './nav-link'
export default function Layout({ children }: { children: React.ReactNode }) { return ( <div className="flex h-screen"> <div className="w-64"> <SideNav /> </div> <div className="flex-1">{children}</div> </div> )}切换页面时,layout 不会重新加载(保留状态)。
服务端组件 与 客户端组件
Next.js 将默认组件视为服务端组件。若要将组件设为客户端组件,需要在文件顶部添加 "use client"。
服务端组件(Server Components)
- 在服务器上渲染(返回 HTML)。
- 不能使用浏览器 API 或 React client hooks。
- 可以直接访问数据库或其他服务器资源。
客户端组件(Client Components)
- 在浏览器端运行。
- 支持交互与状态(可以使用 React hooks)。
- 不能直接访问服务器资源(需通过 fetch/API 获取)。
推荐:服务端负责数据,客户端负责交互
父组件可以是服务端组件,内部嵌套客户端组件进行交互。
数据获取方式
建议在服务端组件中进行数据请求,这样对 SEO 更友好,并能使用 Next.js 的缓存与 revalidate。常见方式:
1) 在服务端组件中直接使用 await + fetch
export default async function Page() { const res = await fetch('https://api.example.com/users') const users = await res.json()
return ( <ul> {users.map((u: any) => ( <li key={u.id}>{u.name}</li> ))} </ul> )}2) 直接使用服务器资源
服务端组件可直接与数据库或本地服务代码交互(在安全的环境下)。
3) Next.js 缓存机制 与 revalidate
(此处可补充:fetch 的 cache、next revalidate 选项,以及 ISR 的使用示例)
4) generateStaticParams / generateMetadata
用于静态生成时提供参数或动态构造页面元数据(后面示例中会展示)。
客户端组件的数据获取
- 使用
useEffect+ fetch - 使用 SWR 等客户端数据库
流式渲染 与 loading
loading.tsx 是约定文件,用于在路由数据尚未准备好时显示占位 UI。Next.js 会在异步组件外层自动使用 Suspense。
<Suspense fallback={<Loading />}> <Page /></Suspense>错误处理
error.tsx 用于捕获渲染错误并显示回退 UI(此文件通常为客户端组件):
'use client'
export default function Error({ error, reset }: { error: Error; reset: () => void }) { return ( <div className="flex flex-col items-center justify-center h-screen"> <h2 className="text-red-600 text-2xl font-bold">出错了 😢</h2> <p className="mt-2 text-gray-600">{error.message}</p> <button onClick={() => reset()} className="mt-4 px-4 py-2 bg-blue-600 text-white rounded">重试</button> </div> )}404 页面处理(not-found.tsx)
not-found.tsx 用于处理 404 状态。示例:
export default function NotFound() { return ( <div className="flex flex-col items-center justify-center h-screen"> <h2 className="text-3xl font-bold text-red-600">404 - 页面未找到 😢</h2> <p className="mt-2 text-gray-600">请检查您访问的地址是否正确。</p> </div> )}服务端手动触发 404:
import { notFound } from 'next/navigation'
export default async function Page() { const res = await fetch('https://api.example.com/user/123')
if (!res.ok) { notFound() // 手动触发 404 页面 }
const data = await res.json() return <div>{data.name}</div>}元数据(Metadata)
通过在 layout.tsx 或 page.tsx 导出元数据对象或 generateMetadata 函数来设置页面的元信息,利于 SEO。
静态元数据
export const metadata = { title: "", description: "", keywords: [""],}动态 generateMetadata
import { Metadata } from 'next'
export async function generateMetadata({ params }: { params: { id: string } }): Promise<Metadata> { const post = await fetch(`https://api.example.com/posts/${params.id}`).then(res => res.json())
return { title: post.title, description: post.summary, openGraph: { title: post.title, description: post.summary, images: [post.coverImage], }, }}服务端行为(Server Actions)
Server Action 允许在服务器上运行函数并由客户端直接调用(不经过 HTTP API)。
'use server'
import { db } from '@/lib/db'
export async function createUser(data: { name: string; email: string }) { const user = await db.user.create({ data }) return user}Server Action 可直接绑定表单并传递复杂数据结构,Next.js 会自动序列化。
中间件(middleware)
中间件在请求到达页面或 API 之前执行,可用于重定向、认证、日志等。
import { NextResponse } from 'next/server'import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) { const { pathname } = request.nextUrl const isLoggedIn = request.cookies.get('token')?.value
if (pathname.startsWith('/dashboard') && !isLoggedIn) { return NextResponse.redirect(new URL('/login', request.url)) }
return NextResponse.next()}
export const config = { matcher: ['/dashboard/:path*', '/profile/:path*']}部分信息可能已经过时