名称: nextjs 描述: 实现Next.js的指南 - 一个用于生产的React框架,具有服务器端渲染、静态生成和现代Web功能。在构建Next.js应用、实现App Router、处理服务器组件、数据获取、路由或优化性能时使用。 许可证: MIT 版本: 1.0.0
Next.js 技能
Next.js 是一个React框架,用于构建具有服务器端渲染、静态生成和内置强大优化功能的全栈Web应用。
参考
https://nextjs.org/docs/llms.txt
何时使用此技能
在以下情况使用此技能:
- 构建新的Next.js应用(v15+)
- 实现App Router架构
- 处理服务器组件和客户端组件
- 设置路由、布局和导航
- 实现数据获取模式
- 优化图像、字体和性能
- 配置元数据和SEO
- 设置API路由和路由处理程序
- 从Pages Router迁移到App Router
- 部署Next.js应用
核心概念
App Router vs Pages Router
App Router(推荐用于v13+):
- 现代架构,具有React服务器组件
- 基于文件系统的路由在
app/目录中 - 布局、加载状态和错误边界
- 流式和Suspense支持
- 具有布局的嵌套路由
Pages Router(传统):
- 传统页面路由在
pages/目录中 - 使用
getStaticProps、getServerSideProps、getInitialProps - 仍支持现有项目
关键架构原则
- 默认服务器组件:
app/中的组件是服务器组件,除非标记为'use client' - 基于文件的路由:文件系统定义应用路由
- 嵌套布局:通过布局跨路由共享UI
- 渐进增强:尽可能在没有JavaScript的情况下工作
- 自动优化:图像、字体、脚本自动优化
安装与设置
创建新项目
npx create-next-app@latest my-app
# 或
yarn create next-app my-app
# 或
pnpm create next-app my-app
# 或
bun create next-app my-app
交互式设置提示:
- TypeScript?(推荐是)
- ESLint?(推荐是)
- Tailwind CSS?(可选)
src/目录?(可选)- App Router?(新项目推荐是)
- 导入别名?(默认:@/*)
手动设置
npm install next@latest react@latest react-dom@latest
package.json脚本:
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
}
}
项目结构
my-app/
├── app/ # App Router (v13+)
│ ├── layout.tsx # 根布局
│ ├── page.tsx # 主页
│ ├── loading.tsx # 加载UI
│ ├── error.tsx # 错误UI
│ ├── not-found.tsx # 404页面
│ ├── global.css # 全局样式
│ └── [folder]/ # 路由段
├── public/ # 静态资产
├── components/ # React组件
├── lib/ # 实用函数
├── next.config.js # Next.js配置
├── package.json
└── tsconfig.json
路由
文件约定
page.tsx- 路由的页面UIlayout.tsx- 段和子节点的共享UIloading.tsx- 加载UI(在Suspense中包装页面)error.tsx- 错误UI(在错误边界中包装页面)not-found.tsx- 404 UIroute.ts- API端点(路由处理程序)template.tsx- 重新渲染的布局UIdefault.tsx- 并行路由后备
基本路由
静态路由:
app/
├── page.tsx → /
├── about/
│ └── page.tsx → /about
└── blog/
└── page.tsx → /blog
动态路由:
// app/blog/[slug]/page.tsx
export default function BlogPost({ params }: { params: { slug: string } }) {
return <h1>帖子: {params.slug}</h1>
}
捕获所有路由:
// app/shop/[...slug]/page.tsx
export default function Shop({ params }: { params: { slug: string[] } }) {
return <h1>类别: {params.slug.join('/')}</h1>
}
可选捕获所有:
// app/docs/[[...slug]]/page.tsx
// 匹配 /docs, /docs/a, /docs/a/b 等
路由组
组织路由而不影响URL:
app/
├── (marketing)/ # 组无URL段
│ ├── about/page.tsx → /about
│ └── blog/page.tsx → /blog
└── (shop)/
├── products/page.tsx → /products
└── cart/page.tsx → /cart
并行路由
在同一布局中渲染多个页面:
app/
├── @team/ # 槽
│ └── page.tsx
├── @analytics/ # 槽
│ └── page.tsx
└── layout.tsx # 使用两个槽
// app/layout.tsx
export default function Layout({
children,
team,
analytics,
}: {
children: React.ReactNode
team: React.ReactNode
analytics: React.ReactNode
}) {
return (
<>
{children}
{team}
{analytics}
</>
)
}
拦截路由
拦截路由以在模态中显示:
app/
├── feed/
│ └── page.tsx
├── photo/
│ └── [id]/
│ └── page.tsx
└── (..)photo/ # 拦截 /photo/[id]
└── [id]/
└── page.tsx
布局
根布局(必需)
// app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>{children}</body>
</html>
)
}
嵌套布局
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<section>
<nav>仪表板导航</nav>
{children}
</section>
)
}
布局特性:
- 跨多个页面共享
- 导航时保留状态
- 导航时不重新渲染
- 可以获取数据
服务器和客户端组件
服务器组件(默认)
app/中的组件默认为服务器组件:
// app/page.tsx(服务器组件)
async function getData() {
const res = await fetch('https://api.example.com/data')
return res.json()
}
export default async function Page() {
const data = await getData()
return <div>{data.title}</div>
}
优点:
- 在服务器上获取数据
- 直接访问后端资源
- 将敏感数据保留在服务器上
- 减少客户端JavaScript
- 改善初始页面加载
限制:
- 不能使用钩子(useState, useEffect)
- 不能使用浏览器API
- 不能添加事件监听器
客户端组件
用'use client'指令标记组件:
// components/counter.tsx
'use client'
import { useState } from 'react'
export function Counter() {
const [count, setCount] = useState(0)
return (
<button onClick={() => setCount(count + 1)}>
计数: {count}
</button>
)
}
使用客户端组件用于:
- 交互式UI(onClick, onChange)
- 状态管理(useState, useReducer)
- 效果(useEffect, useLayoutEffect)
- 浏览器API(localStorage, navigator)
- 自定义钩子
- React类组件
组合模式
// app/page.tsx(服务器组件)
import { ClientComponent } from './client-component'
export default function Page() {
return (
<div>
<h1>服务器渲染内容</h1>
<ClientComponent />
</div>
)
}
数据获取
服务器组件数据获取
// app/posts/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 } // 每小时重新验证
})
if (!res.ok) throw new Error('获取失败')
return res.json()
}
export default async function PostsPage() {
const posts = await getPosts()
return (
<ul>
{posts.map(post => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
缓存策略
强制缓存(默认):
fetch('https://api.example.com/data', { cache: 'force-cache' })
不存储(动态):
fetch('https://api.example.com/data', { cache: 'no-store' })
重新验证:
fetch('https://api.example.com/data', {
next: { revalidate: 3600 } // 秒
})
基于标签的重新验证:
fetch('https://api.example.com/data', {
next: { tags: ['posts'] }
})
// 在其他地方重新验证:
import { revalidateTag } from 'next/cache'
revalidateTag('posts')
并行数据获取
async function getData() {
const [posts, users] = await Promise.all([
fetch('https://api.example.com/posts').then(r => r.json()),
fetch('https://api.example.com/users').then(r => r.json()),
])
return { posts, users }
}
顺序数据获取
async function getData() {
const post = await fetch(`https://api.example.com/posts/${id}`).then(r => r.json())
const author = await fetch(`https://api.example.com/users/${post.authorId}`).then(r => r.json())
return { post, author }
}
路由处理程序(API路由)
基本路由处理程序
// app/api/hello/route.ts
export async function GET(request: Request) {
return Response.json({ message: 'Hello' })
}
export async function POST(request: Request) {
const body = await request.json()
return Response.json({ received: body })
}
动态路由处理程序
// app/api/posts/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const post = await getPost(params.id)
return Response.json(post)
}
export async function DELETE(
request: Request,
{ params }: { params: { id: string } }
) {
await deletePost(params.id)
return new Response(null, { status: 204 })
}
请求助手
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const id = searchParams.get('id')
const cookies = request.headers.get('cookie')
return Response.json({ id })
}
响应类型
// JSON
return Response.json({ data: 'value' })
// 文本
return new Response('Hello', { headers: { 'Content-Type': 'text/plain' } })
// 重定向
return Response.redirect('https://example.com')
// 状态码
return new Response('未找到', { status: 404 })
导航
Link组件
import Link from 'next/link'
export default function Page() {
return (
<>
<Link href="/about">关于</Link>
<Link href="/blog/post-1">帖子1</Link>
<Link href={{ pathname: '/blog/[slug]', query: { slug: 'post-1' } }}>
帖子1(替代方式)
</Link>
</>
)
}
useRouter钩子(客户端)
'use client'
import { useRouter } from 'next/navigation'
export function NavigateButton() {
const router = useRouter()
return (
<button onClick={() => router.push('/dashboard')}>
仪表板
</button>
)
}
路由器方法:
router.push(href)- 导航到路由router.replace(href)- 替换当前历史router.refresh()- 刷新当前路由router.back()- 向后导航router.forward()- 向前导航router.prefetch(href)- 预取路由
编程导航(服务器)
import { redirect } from 'next/navigation'
export default async function Page() {
const session = await getSession()
if (!session) {
redirect('/login')
}
return <div>受保护内容</div>
}
元数据与SEO
静态元数据
// app/page.tsx
import { Metadata } from 'next'
export const metadata: Metadata = {
title: '我的页面',
description: '页面描述',
keywords: ['nextjs', 'react'],
openGraph: {
title: '我的页面',
description: '页面描述',
images: ['/og-image.jpg'],
},
twitter: {
card: 'summary_large_image',
title: '我的页面',
description: '页面描述',
images: ['/twitter-image.jpg'],
},
}
export default function Page() {
return <div>内容</div>
}
动态元数据
// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
const post = await getPost(params.slug)
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
}
}
元数据文件
favicon.ico,icon.png,apple-icon.png- 网站图标opengraph-image.png,twitter-image.png- 社交图像robots.txt- 机器人文件sitemap.xml- 站点地图
图像优化
Image组件
import Image from 'next/image'
export default function Page() {
return (
<>
{/* 本地图像 */}
<Image
src="/profile.png"
alt="个人资料"
width={500}
height={500}
/>
{/* 远程图像 */}
<Image
src="https://example.com/image.jpg"
alt="远程"
width={500}
height={500}
/>
{/* 响应式填充 */}
<div style={{ position: 'relative', width: '100%', height: '400px' }}>
<Image
src="/hero.jpg"
alt="英雄"
fill
style={{ objectFit: 'cover' }}
/>
</div>
{/* 优先级加载 */}
<Image
src="/hero.jpg"
alt="英雄"
width={1200}
height={600}
priority
/>
</>
)
}
图像属性:
src- 图像路径(本地或URL)alt- 替代文本(必需)width,height- 尺寸(必需,除非填充)fill- 填充父容器sizes- 响应式尺寸quality- 1-100(默认75)priority- 预加载图像placeholder- ‘blur’ | ‘empty’blurDataURL- 模糊的数据URL
远程图像配置
// next.config.js
module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'example.com',
pathname: '/images/**',
},
],
},
}
字体优化
Google字体
// app/layout.tsx
import { Inter, Roboto_Mono } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
display: 'swap',
})
const robotoMono = Roboto_Mono({
subsets: ['latin'],
display: 'swap',
variable: '--font-roboto-mono',
})
export default function RootLayout({ children }) {
return (
<html lang="en" className={`${inter.className} ${robotoMono.variable}`}>
<body>{children}</body>
</html>
)
}
本地字体
import localFont from 'next/font/local'
const myFont = localFont({
src: './fonts/my-font.woff2',
display: 'swap',
variable: '--font-my-font',
})
加载状态
加载文件
// app/dashboard/loading.tsx
export default function Loading() {
return <div>加载仪表板中...</div>
}
流式与Suspense
// app/page.tsx
import { Suspense } from 'react'
async function Posts() {
const posts = await getPosts()
return <ul>{posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
}
export default function Page() {
return (
<div>
<h1>我的帖子</h1>
<Suspense fallback={<div>加载帖子中...</div>}>
<Posts />
</Suspense>
</div>
)
}
错误处理
错误文件
// app/error.tsx
'use client'
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div>
<h2>出错了!</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>重试</button>
</div>
)
}
全局错误
// app/global-error.tsx
'use client'
export default function GlobalError({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<html>
<body>
<h2>出错了!</h2>
<button onClick={() => reset()}>重试</button>
</body>
</html>
)
}
未找到
// app/not-found.tsx
export default function NotFound() {
return (
<div>
<h2>404 - 未找到</h2>
<p>无法找到请求的资源</p>
</div>
)
}
// 编程触发
import { notFound } from 'next/navigation'
export default async function Page({ params }) {
const post = await getPost(params.id)
if (!post) {
notFound()
}
return <div>{post.title}</div>
}
中间件
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// 身份验证检查
const token = request.cookies.get('token')
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
// 添加自定义标头
const response = NextResponse.next()
response.headers.set('x-custom-header', 'value')
return response
}
export const config = {
matcher: ['/dashboard/:path*', '/api/:path*'],
}
环境变量
# .env.local
DATABASE_URL=postgresql://...
NEXT_PUBLIC_API_URL=https://api.example.com
// 仅服务器端
const dbUrl = process.env.DATABASE_URL
// 客户端和服务器(NEXT_PUBLIC_前缀)
const apiUrl = process.env.NEXT_PUBLIC_API_URL
配置
next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// React严格模式
reactStrictMode: true,
// 图像域
images: {
remotePatterns: [
{ protocol: 'https', hostname: 'example.com' },
],
},
// 重定向
async redirects() {
return [
{
source: '/old-page',
destination: '/new-page',
permanent: true,
},
]
},
// 重写
async rewrites() {
return [
{
source: '/api/:path*',
destination: 'https://api.example.com/:path*',
},
]
},
// 标头
async headers() {
return [
{
source: '/(.*)',
headers: [
{ key: 'X-Frame-Options', value: 'DENY' },
],
},
]
},
// 环境变量
env: {
CUSTOM_KEY: 'value',
},
}
module.exports = nextConfig
最佳实践
- 使用服务器组件:默认为服务器组件,仅在需要时使用客户端组件
- 优化图像:始终使用
next/image进行自动优化 - 元数据:为SEO设置适当的元数据
- 加载状态:用Suspense提供加载UI
- 错误处理:实现错误边界
- 路由处理程序:用于API端点而不是单独的后端
- 缓存:利用内置缓存策略
- 布局:使用嵌套布局共享UI
- TypeScript:启用TypeScript以确保类型安全
- 性能:使用
priority加载首屏图像,懒加载非首屏图像
常见模式
受保护路由
// app/dashboard/layout.tsx
import { redirect } from 'next/navigation'
import { getSession } from '@/lib/auth'
export default async function DashboardLayout({ children }) {
const session = await getSession()
if (!session) {
redirect('/login')
}
return <>{children}</>
}
数据突变(服务器操作)
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
export async function createPost(formData: FormData) {
const title = formData.get('title')
await db.post.create({ data: { title } })
revalidatePath('/posts')
}
// app/posts/new/page.tsx
import { createPost } from '@/app/actions'
export default function NewPost() {
return (
<form action={createPost}>
<input name="title" type="text" required />
<button type="submit">创建</button>
</form>
)
}
静态生成
// 为动态路由生成静态参数
export async function generateStaticParams() {
const posts = await getPosts()
return posts.map(post => ({
slug: post.slug,
}))
}
export default async function Post({ params }) {
const post = await getPost(params.slug)
return <article>{post.content}</article>
}
部署
Vercel(推荐)
# 安装Vercel CLI
npm i -g vercel
# 部署
vercel
自托管
# 构建
npm run build
# 启动生产服务器
npm start
要求:
- Node.js 18.17或更高版本
output: 'standalone'在next.config.js中(可选,减少大小)
Docker
FROM node:18-alpine AS base
FROM base AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
EXPOSE 3000
CMD ["node", "server.js"]
故障排除
常见问题
-
水合错误
- 确保服务器和客户端渲染相同内容
- 检查服务器组件中是否有仅浏览器代码
- 验证没有基于浏览器API的条件渲染
-
图像未加载
- 在
next.config.js中添加远程域 - 检查图像路径(使用前导
/表示公共路径) - 验证提供宽度和高度
- 在
-
API路由404
- 检查文件名为
route.ts/js而非index.ts - 验证导出名为GET/POST而非默认导出
- 确保在
app/api/目录中
- 检查文件名为
-
"use client"错误
- 为使用钩子的组件添加
'use client' - 在服务器组件中导入客户端组件,反之不行
- 检查事件处理程序有
'use client'
- 为使用钩子的组件添加
-
元数据未更新
- 清除浏览器缓存
- 检查元数据导出命名正确
- 验证异步generateMetadata返回Promise<Metadata>
资源
- 文档: https://nextjs.org/docs
- 学习课程: https://nextjs.org/learn
- 示例: https://github.com/vercel/next.js/tree/canary/examples
- 博客: https://nextjs.org/blog
- GitHub: https://github.com/vercel/next.js
实施清单
构建Next.js应用时:
- [ ] 用
create-next-app创建项目 - [ ] 配置TypeScript和ESLint
- [ ] 设置带元数据的根布局
- [ ] 实现路由结构
- [ ] 添加加载和错误状态
- [ ] 配置图像优化
- [ ] 设置字体优化
- [ ] 实现数据获取模式
- [ ] 根据需要添加API路由
- [ ] 配置环境变量
- [ ] 根据需要设置中间件
- [ ] 优化生产构建
- [ ] 在生产模式下测试
- [ ] 配置部署平台
- [ ] 设置监控和分析