Next.js 是一个基于 React 的全栈应用程序框架,用于构建现代化的 Web 应用程序。
1.什么是 Next.js
Next.js 是一个基于 React 的生产级框架,它提供了构建现代 Web 应用所需的所有工具和最佳实践。
1-1.核心特性
- 服务端渲染 (SSR):提供更好的 SEO 和首屏加载性能
- 静态站点生成 (SSG):构建时预渲染页面,部署到 CDN
- 增量静态再生 (ISR):静态页面的按需更新
- 自动路由:基于文件系统的路由,无需手动配置
- API 路由:内置 API 端点支持
- 代码分割:自动按页面进行代码分割
- TypeScript 支持:开箱即用的 TypeScript 支持
- 图像优化:内置的图像优化和懒加载
1-2.渲染模式
Next.js 支持多种渲染模式:
渲染模式 | 说明 | 适用场景 |
---|---|---|
SSR | 服务端渲染,每次请求都在服务器生成 HTML | 动态内容、需要 SEO 的应用 |
SSG | 静态站点生成,构建时预渲染所有页面 | 内容相对静态的网站、博客 |
ISR | 增量静态再生,静态页面按需更新 | 大型网站、内容更新频繁但不实时 |
CSR | 客户端渲染,传统 SPA 模式 | 后台管理系统、不需要 SEO 的应用 |
2.技术栈
Next.js 的技术栈包括:
2-1.核心依赖
- React 18+:UI 框架
- Webpack/Turbopack:构建工具
- SWC:JavaScript/TypeScript 编译器
- Node.js:服务器运行时
2-2.生态系统
- Vercel:官方部署平台
- Next.js Commerce:电商解决方案
- NextAuth.js:身份验证
- SWR/TanStack Query:数据获取
3.项目结构
3-1.App Router (推荐,Next.js 13+)
my-next-app/
├── .next/ # 构建输出目录
├── .env.local # 环境变量
├── app/ # App Router 目录
│ ├── globals.css # 全局样式
│ ├── layout.tsx # 根布局
│ ├── page.tsx # 首页
│ ├── about/
│ │ └── page.tsx # /about 页面
│ ├── blog/
│ │ ├── page.tsx # /blog 页面
│ │ └── [slug]/
│ │ └── page.tsx # /blog/[slug] 动态页面
│ └── api/ # API 路由
│ └── users/
│ └── route.ts # /api/users 端点
├── components/ # React 组件
├── lib/ # 工具函数
├── public/ # 静态资源
├── next.config.js # Next.js 配置
└── package.json # 项目依赖
3-2.Pages Router (传统方式)
my-next-app/
├── pages/ # 页面目录
│ ├── _app.tsx # 应用入口
│ ├── _document.tsx # HTML 文档
│ ├── index.tsx # 首页
│ ├── about.tsx # /about 页面
│ ├── blog/
│ │ ├── index.tsx # /blog 页面
│ │ └── [slug].tsx # /blog/[slug] 动态页面
│ └── api/ # API 路由
│ └── users.ts # /api/users 端点
├── components/ # React 组件
├── styles/ # 样式文件
├── public/ # 静态资源
└── next.config.js # Next.js 配置
4.快速开始
4-1.创建项目
bash
# 使用 create-next-app
npx create-next-app@latest my-next-app
# 使用 TypeScript
npx create-next-app@latest my-next-app --typescript
# 使用 Tailwind CSS
npx create-next-app@latest my-next-app --tailwind
# 完整配置
npx create-next-app@latest my-next-app --typescript --tailwind --eslint --app
4-2.安装依赖
bash
cd my-next-app
npm install
4-3.启动开发服务器
bash
npm run dev
4-4.构建生产版本
bash
# 构建应用
npm run build
# 启动生产服务器
npm run start
# 导出静态文件(SSG)
npm run build && npm run export
5.配置文件
5-1.基础配置
javascript
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
// 实验性功能
experimental: {
appDir: true, // 启用 App Router
serverActions: true // 启用 Server Actions
},
// 图像配置
images: {
domains: ['example.com'],
formats: ['image/webp', 'image/avif']
},
// 环境变量
env: {
CUSTOM_KEY: process.env.CUSTOM_KEY
},
// 重定向
async redirects() {
return [
{
source: '/old-path',
destination: '/new-path',
permanent: true
}
]
},
// 重写
async rewrites() {
return [
{
source: '/api/:path*',
destination: 'https://api.example.com/:path*'
}
]
}
}
module.exports = nextConfig
5-2.TypeScript 配置
json
// tsconfig.json
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "es6"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
6.页面和路由
6-1.App Router (Next.js 13+)
App Router 是 Next.js 13+ 推荐的路由方式:
app/
├── page.tsx # / (首页)
├── about/
│ └── page.tsx # /about
├── blog/
│ ├── page.tsx # /blog
│ ├── [slug]/
│ │ └── page.tsx # /blog/[slug]
│ └── [...slug]/
│ └── page.tsx # /blog/[...slug] (捕获所有路由)
└── dashboard/
├── layout.tsx # 嵌套布局
├── page.tsx # /dashboard
└── settings/
└── page.tsx # /dashboard/settings
6-2.页面组件
tsx
// app/blog/[slug]/page.tsx
interface PageProps {
params: { slug: string }
searchParams: { [key: string]: string | string[] | undefined }
}
export default function BlogPost({ params, searchParams }: PageProps) {
return (
<div>
<h1>博客文章: {params.slug}</h1>
<p>搜索参数: {JSON.stringify(searchParams)}</p>
</div>
)
}
// 生成静态参数 (SSG)
export async function generateStaticParams() {
const posts = await fetch('https://api.example.com/posts').then(res => res.json())
return posts.map((post: any) => ({
slug: post.slug
}))
}
// 生成元数据
export async function generateMetadata({ params }: PageProps) {
const post = await fetch(`https://api.example.com/posts/${params.slug}`)
.then(res => res.json())
return {
title: post.title,
description: post.excerpt
}
}
6-3.布局组件
tsx
// app/layout.tsx (根布局)
import './globals.css'
export const metadata = {
title: 'My Next.js App',
description: 'Generated by create next app'
}
export default function RootLayout({
children
}: {
children: React.ReactNode
}) {
return (
<html lang="zh">
<body>
<header>
<nav>导航栏</nav>
</header>
<main>{children}</main>
<footer>页脚</footer>
</body>
</html>
)
}
tsx
// app/dashboard/layout.tsx (嵌套布局)
export default function DashboardLayout({
children
}: {
children: React.ReactNode
}) {
return (
<div className="dashboard">
<aside>侧边栏</aside>
<div className="content">{children}</div>
</div>
)
}
7.数据获取
7-1.服务端组件 (默认)
tsx
// app/posts/page.tsx
async function getPosts() {
const res = await fetch('https://api.example.com/posts', {
// 缓存配置
next: { revalidate: 3600 } // 1小时后重新验证
})
if (!res.ok) {
throw new Error('Failed to fetch posts')
}
return res.json()
}
export default async function PostsPage() {
const posts = await getPosts()
return (
<div>
<h1>文章列表</h1>
{posts.map((post: any) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
)
}
7-2.客户端组件
tsx
'use client'
import { useState, useEffect } from 'react'
export default function ClientPostsPage() {
const [posts, setPosts] = useState([])
const [loading, setLoading] = useState(true)
useEffect(() => {
fetch('/api/posts')
.then(res => res.json())
.then(data => {
setPosts(data)
setLoading(false)
})
}, [])
if (loading) return <div>加载中...</div>
return (
<div>
<h1>文章列表</h1>
{posts.map((post: any) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
)
}
7-3.SWR 数据获取
bash
npm install swr
tsx
'use client'
import useSWR from 'swr'
const fetcher = (url: string) => fetch(url).then(res => res.json())
export default function PostsWithSWR() {
const { data: posts, error, isLoading } = useSWR('/api/posts', fetcher)
if (error) return <div>加载失败</div>
if (isLoading) return <div>加载中...</div>
return (
<div>
<h1>文章列表</h1>
{posts?.map((post: any) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
</article>
))}
</div>
)
}
8.API 路由
8-1.App Router API
typescript
// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server'
// GET /api/posts
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const page = searchParams.get('page') || '1'
try {
const posts = await fetchPostsFromDB(parseInt(page))
return NextResponse.json({ posts })
} catch (error) {
return NextResponse.json(
{ error: 'Failed to fetch posts' },
{ status: 500 }
)
}
}
// POST /api/posts
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const post = await createPost(body)
return NextResponse.json({ post }, { status: 201 })
} catch (error) {
return NextResponse.json(
{ error: 'Failed to create post' },
{ status: 500 }
)
}
}
8-2.动态 API 路由
typescript
// app/api/posts/[id]/route.ts
interface RouteParams {
params: { id: string }
}
export async function GET(
request: NextRequest,
{ params }: RouteParams
) {
try {
const post = await getPostById(params.id)
if (!post) {
return NextResponse.json(
{ error: 'Post not found' },
{ status: 404 }
)
}
return NextResponse.json({ post })
} catch (error) {
return NextResponse.json(
{ error: 'Failed to fetch post' },
{ status: 500 }
)
}
}
export async function PUT(
request: NextRequest,
{ params }: RouteParams
) {
try {
const body = await request.json()
const post = await updatePost(params.id, body)
return NextResponse.json({ post })
} catch (error) {
return NextResponse.json(
{ error: 'Failed to update post' },
{ status: 500 }
)
}
}
export async function DELETE(
request: NextRequest,
{ params }: RouteParams
) {
try {
await deletePost(params.id)
return NextResponse.json({ message: 'Post deleted' })
} catch (error) {
return NextResponse.json(
{ error: 'Failed to delete post' },
{ status: 500 }
)
}
}
8-3.Server Actions
tsx
// app/posts/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
try {
const post = await savePostToDB({ title, content })
revalidatePath('/posts')
redirect(`/posts/${post.id}`)
} catch (error) {
throw new Error('Failed to create post')
}
}
export async function deletePost(id: string) {
try {
await deletePostFromDB(id)
revalidatePath('/posts')
} catch (error) {
throw new Error('Failed to delete post')
}
}
tsx
// app/posts/create/page.tsx
import { createPost } from '../actions'
export default function CreatePost() {
return (
<form action={createPost}>
<input name="title" placeholder="标题" required />
<textarea name="content" placeholder="内容" required />
<button type="submit">创建文章</button>
</form>
)
}
9.状态管理
9-1.React Context
tsx
// lib/context/AuthContext.tsx
'use client'
import { createContext, useContext, useState, ReactNode } from 'react'
interface User {
id: string
name: string
email: string
}
interface AuthContextType {
user: User | null
login: (email: string, password: string) => Promise<void>
logout: () => void
}
const AuthContext = createContext<AuthContextType | undefined>(undefined)
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null)
const login = async (email: string, password: string) => {
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
})
if (response.ok) {
const userData = await response.json()
setUser(userData.user)
}
}
const logout = () => {
setUser(null)
}
return (
<AuthContext.Provider value={{ user, login, logout }}>
{children}
</AuthContext.Provider>
)
}
export function useAuth() {
const context = useContext(AuthContext)
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider')
}
return context
}
9-2.Zustand 状态管理
bash
npm install zustand
typescript
// lib/store/useStore.ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface User {
id: string
name: string
email: string
}
interface AuthState {
user: User | null
isAuthenticated: boolean
login: (user: User) => void
logout: () => void
}
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
user: null,
isAuthenticated: false,
login: (user) => set({ user, isAuthenticated: true }),
logout: () => set({ user: null, isAuthenticated: false })
}),
{
name: 'auth-storage'
}
)
)
tsx
// components/LoginForm.tsx
'use client'
import { useAuthStore } from '@/lib/store/useStore'
export default function LoginForm() {
const { login, isAuthenticated } = useAuthStore()
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
// 登录逻辑
const user = { id: '1', name: 'John', email: 'john@example.com' }
login(user)
}
if (isAuthenticated) {
return <div>已登录</div>
}
return (
<form onSubmit={handleSubmit}>
<input type="email" placeholder="邮箱" required />
<input type="password" placeholder="密码" required />
<button type="submit">登录</button>
</form>
)
}
10.中间件
10-1.基础中间件
typescript
// 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 (request.nextUrl.pathname.startsWith('/dashboard')) {
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*'
]
}
10-2.认证中间件
typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { jwtVerify } from 'jose'
export async function middleware(request: NextRequest) {
const token = request.cookies.get('token')?.value
// 保护的路由
const protectedPaths = ['/dashboard', '/profile', '/admin']
const isProtectedPath = protectedPaths.some(path =>
request.nextUrl.pathname.startsWith(path)
)
if (isProtectedPath) {
if (!token) {
return NextResponse.redirect(new URL('/login', request.url))
}
try {
// 验证 JWT token
await jwtVerify(
token,
new TextEncoder().encode(process.env.JWT_SECRET!)
)
} catch (error) {
return NextResponse.redirect(new URL('/login', request.url))
}
}
return NextResponse.next()
}
11.部署
11-1.Vercel 部署 (推荐)
bash
# 安装 Vercel CLI
npm install -g vercel
# 部署
vercel
# 生产部署
vercel --prod
json
// vercel.json
{
"functions": {
"app/api/**/*.ts": {
"maxDuration": 30
}
},
"env": {
"DATABASE_URL": "@database-url"
},
"build": {
"env": {
"NEXT_PUBLIC_API_URL": "https://api.example.com"
}
}
}
11-2.Docker 部署
dockerfile
# Dockerfile
FROM node:18-alpine AS base
# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then yarn global add pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN yarn build
# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app
ENV NODE_ENV production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT 3000
CMD ["node", "server.js"]
11-3.静态导出
javascript
// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'export',
trailingSlash: true,
images: {
unoptimized: true
}
}
module.exports = nextConfig
bash
npm run build
12.最佳实践
12-1.性能优化
tsx
// 图像优化
import Image from 'next/image'
export default function OptimizedImage() {
return (
<Image
src="/hero.jpg"
alt="Hero image"
width={800}
height={600}
priority // 首屏图片
placeholder="blur" // 模糊占位符
blurDataURL="data:image/jpeg;base64,..." // 占位符数据
/>
)
}
tsx
// 动态导入
import dynamic from 'next/dynamic'
const DynamicComponent = dynamic(() => import('../components/Heavy'), {
loading: () => <p>Loading...</p>,
ssr: false // 禁用服务端渲染
})
export default function Page() {
return (
<div>
<DynamicComponent />
</div>
)
}
12-2.SEO 优化
tsx
// app/blog/[slug]/page.tsx
import { Metadata } from 'next'
interface PageProps {
params: { slug: string }
}
export async function generateMetadata({ params }: PageProps): Promise<Metadata> {
const post = await getPost(params.slug)
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.image],
type: 'article'
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: [post.image]
}
}
}
12-3.错误处理
tsx
// 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>
)
}
tsx
// app/not-found.tsx
export default function NotFound() {
return (
<div>
<h2>页面未找到</h2>
<p>抱歉,您访问的页面不存在。</p>
<a href="/">返回首页</a>
</div>
)
}
12-4.环境变量
bash
# .env.local
DATABASE_URL=postgresql://...
NEXTAUTH_SECRET=your-secret-here
NEXTAUTH_URL=http://localhost:3000
# 公开变量(以 NEXT_PUBLIC_ 开头)
NEXT_PUBLIC_API_URL=https://api.example.com
typescript
// 使用环境变量
const apiUrl = process.env.NEXT_PUBLIC_API_URL
const dbUrl = process.env.DATABASE_URL // 仅服务端可用
Next.js 提供了强大而灵活的开发体验,通过合理使用其特性可以构建高性能的现代 Web 应用程序。相比 Nuxt.js,Next.js 在 React 生态系统中有着更成熟的工具链和更大的社区支持。