- 发布于
Next.js 全栈开发指南:构建现代 React 应用的完整解决方案
- 作者

- 姓名
- 全能波
- GitHub
- @weicracker
Next.js 全栈开发指南:构建现代 React 应用的完整解决方案
Next.js 是一个强大的 React 框架,提供了构建现代 Web 应用所需的所有功能。本文将深入探讨 Next.js 13+ 的最新特性和最佳实践。
Next.js 13+ 新特性概览
App Router 架构
// app/layout.js - 根布局
import './globals.css'
export const metadata = {
title: 'My Next.js App',
description: 'A modern web application built with Next.js',
}
export default function RootLayout({ children }) {
return (
<html lang="zh-CN">
<body>
<header>
<nav>
<Link href="/">首页</Link>
<Link href="/about">关于</Link>
<Link href="/blog">博客</Link>
</nav>
</header>
<main>{children}</main>
<footer>
<p>© 2024 My App</p>
</footer>
</body>
</html>
)
}
// app/page.js - 首页
export default function HomePage() {
return (
<div>
<h1>欢迎来到我的网站</h1>
<p>这是使用 Next.js 13+ App Router 构建的现代 React 应用。</p>
</div>
)
}
// app/blog/page.js - 博客页面
import { getBlogPosts } from '@/lib/blog'
export default async function BlogPage() {
const posts = await getBlogPosts()
return (
<div>
<h1>博客文章</h1>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{posts.map((post) => (
<article key={post.id} className="border rounded-lg p-6">
<h2 className="text-xl font-bold mb-2">{post.title}</h2>
<p className="text-gray-600 mb-4">{post.excerpt}</p>
<Link
href={`/blog/${post.slug}`}
className="text-blue-600 hover:underline"
>
阅读更多
</Link>
</article>
))}
</div>
</div>
)
}
Server Components 和 Client Components
// app/components/ServerComponent.js - 服务器组件
import { db } from '@/lib/database'
// 默认情况下,App Router 中的组件都是 Server Components
export default async function ServerComponent() {
// 可以直接在组件中进行数据库查询
const users = await db.user.findMany({
select: { id: true, name: true, email: true }
})
return (
<div>
<h2>用户列表(服务器渲染)</h2>
<ul>
{users.map(user => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
</div>
)
}
// app/components/ClientComponent.js - 客户端组件
'use client' // 标记为客户端组件
import { useState, useEffect } from 'react'
export default function ClientComponent() {
const [count, setCount] = useState(0)
const [mounted, setMounted] = useState(false)
useEffect(() => {
setMounted(true)
}, [])
if (!mounted) {
return <div>加载中...</div>
}
return (
<div>
<h2>交互式计数器(客户端渲染)</h2>
<p>当前计数: {count}</p>
<button
onClick={() => setCount(count + 1)}
className="bg-blue-500 text-white px-4 py-2 rounded"
>
增加
</button>
</div>
)
}
// app/components/HybridComponent.js - 混合组件
import ServerComponent from './ServerComponent'
import ClientComponent from './ClientComponent'
export default function HybridComponent() {
return (
<div>
<ServerComponent />
<ClientComponent />
</div>
)
}
数据获取策略
1. 服务器端数据获取
// app/products/page.js
import { notFound } from 'next/navigation'
async function getProducts() {
const res = await fetch('https://api.example.com/products', {
// 缓存策略
next: { revalidate: 3600 }, // 1小时后重新验证
})
if (!res.ok) {
throw new Error('获取产品数据失败')
}
return res.json()
}
export default async function ProductsPage() {
try {
const products = await getProducts()
return (
<div>
<h1>产品列表</h1>
<div className="grid grid-cols-1 gap-6 md:grid-cols-3">
{products.map((product) => (
<ProductCard key={product.id} product={product} />
))}
</div>
</div>
)
} catch (error) {
return (
<div>
<h1>加载失败</h1>
<p>无法加载产品数据,请稍后重试。</p>
</div>
)
}
}
// 生成静态参数
export async function generateStaticParams() {
const products = await getProducts()
return products.map((product) => ({
id: product.id.toString(),
}))
}
// 元数据生成
export async function generateMetadata({ params }) {
const product = await getProduct(params.id)
if (!product) {
return {
title: '产品未找到',
}
}
return {
title: product.name,
description: product.description,
openGraph: {
title: product.name,
description: product.description,
images: [product.image],
},
}
}
2. 客户端数据获取
// app/hooks/useProducts.js
'use client'
import { useState, useEffect } from 'react'
export function useProducts() {
const [products, setProducts] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
useEffect(() => {
async function fetchProducts() {
try {
setLoading(true)
const response = await fetch('/api/products')
if (!response.ok) {
throw new Error('获取产品失败')
}
const data = await response.json()
setProducts(data)
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}
fetchProducts()
}, [])
return { products, loading, error }
}
// app/components/ProductList.js
;('use client')
import { useProducts } from '@/hooks/useProducts'
export default function ProductList() {
const { products, loading, error } = useProducts()
if (loading) return <div>加载中...</div>
if (error) return <div>错误: {error}</div>
return (
<div>
{products.map((product) => (
<div key={product.id} className="rounded border p-4">
<h3>{product.name}</h3>
<p>{product.description}</p>
<span className="font-bold">${product.price}</span>
</div>
))}
</div>
)
}
API Routes 开发
1. REST API 实现
// app/api/users/route.js
import { NextResponse } from 'next/server'
import { db } from '@/lib/database'
import { auth } from '@/lib/auth'
// GET /api/users
export async function GET(request) {
try {
const { searchParams } = new URL(request.url)
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '10')
const search = searchParams.get('search') || ''
const users = await db.user.findMany({
where: search
? {
OR: [
{ name: { contains: search, mode: 'insensitive' } },
{ email: { contains: search, mode: 'insensitive' } },
],
}
: {},
skip: (page - 1) * limit,
take: limit,
select: {
id: true,
name: true,
email: true,
createdAt: true,
},
})
const total = await db.user.count({
where: search
? {
OR: [
{ name: { contains: search, mode: 'insensitive' } },
{ email: { contains: search, mode: 'insensitive' } },
],
}
: {},
})
return NextResponse.json({
users,
pagination: {
page,
limit,
total,
pages: Math.ceil(total / limit),
},
})
} catch (error) {
console.error('获取用户列表失败:', error)
return NextResponse.json({ error: '服务器内部错误' }, { status: 500 })
}
}
// POST /api/users
export async function POST(request) {
try {
const session = await auth(request)
if (!session) {
return NextResponse.json({ error: '未授权' }, { status: 401 })
}
const body = await request.json()
const { name, email, password } = body
// 验证输入
if (!name || !email || !password) {
return NextResponse.json({ error: '缺少必填字段' }, { status: 400 })
}
// 检查邮箱是否已存在
const existingUser = await db.user.findUnique({
where: { email },
})
if (existingUser) {
return NextResponse.json({ error: '邮箱已被使用' }, { status: 409 })
}
// 创建用户
const hashedPassword = await bcrypt.hash(password, 12)
const user = await db.user.create({
data: {
name,
email,
password: hashedPassword,
},
select: {
id: true,
name: true,
email: true,
createdAt: true,
},
})
return NextResponse.json(user, { status: 201 })
} catch (error) {
console.error('创建用户失败:', error)
return NextResponse.json({ error: '服务器内部错误' }, { status: 500 })
}
}
2. 动态路由 API
// app/api/users/[id]/route.js
import { NextResponse } from 'next/server'
import { db } from '@/lib/database'
// GET /api/users/[id]
export async function GET(request, { params }) {
try {
const { id } = params
const user = await db.user.findUnique({
where: { id: parseInt(id) },
select: {
id: true,
name: true,
email: true,
createdAt: true,
posts: {
select: {
id: true,
title: true,
createdAt: true,
},
},
},
})
if (!user) {
return NextResponse.json({ error: '用户不存在' }, { status: 404 })
}
return NextResponse.json(user)
} catch (error) {
console.error('获取用户失败:', error)
return NextResponse.json({ error: '服务器内部错误' }, { status: 500 })
}
}
// PUT /api/users/[id]
export async function PUT(request, { params }) {
try {
const { id } = params
const body = await request.json()
const { name, email } = body
const user = await db.user.update({
where: { id: parseInt(id) },
data: { name, email },
select: {
id: true,
name: true,
email: true,
updatedAt: true,
},
})
return NextResponse.json(user)
} catch (error) {
if (error.code === 'P2025') {
return NextResponse.json({ error: '用户不存在' }, { status: 404 })
}
console.error('更新用户失败:', error)
return NextResponse.json({ error: '服务器内部错误' }, { status: 500 })
}
}
// DELETE /api/users/[id]
export async function DELETE(request, { params }) {
try {
const { id } = params
await db.user.delete({
where: { id: parseInt(id) },
})
return NextResponse.json({ message: '用户已删除' })
} catch (error) {
if (error.code === 'P2025') {
return NextResponse.json({ error: '用户不存在' }, { status: 404 })
}
console.error('删除用户失败:', error)
return NextResponse.json({ error: '服务器内部错误' }, { status: 500 })
}
}
认证和授权
1. NextAuth.js 集成
// app/api/auth/[...nextauth]/route.js
import NextAuth from 'next-auth'
import CredentialsProvider from 'next-auth/providers/credentials'
import GoogleProvider from 'next-auth/providers/google'
import { PrismaAdapter } from '@next-auth/prisma-adapter'
import { db } from '@/lib/database'
import bcrypt from 'bcryptjs'
const handler = NextAuth({
adapter: PrismaAdapter(db),
providers: [
CredentialsProvider({
name: 'credentials',
credentials: {
email: { label: '邮箱', type: 'email' },
password: { label: '密码', type: 'password' },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
return null
}
const user = await db.user.findUnique({
where: { email: credentials.email },
})
if (!user) {
return null
}
const isPasswordValid = await bcrypt.compare(credentials.password, user.password)
if (!isPasswordValid) {
return null
}
return {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
}
},
}),
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
session: {
strategy: 'jwt',
},
callbacks: {
async jwt({ token, user }) {
if (user) {
token.role = user.role
}
return token
},
async session({ session, token }) {
session.user.id = token.sub
session.user.role = token.role
return session
},
},
pages: {
signIn: '/auth/signin',
signUp: '/auth/signup',
error: '/auth/error',
},
})
export { handler as GET, handler as POST }
// lib/auth.js
import { getServerSession } from 'next-auth/next'
import { authOptions } from '@/app/api/auth/[...nextauth]/route'
export async function getSession() {
return await getServerSession(authOptions)
}
export async function getCurrentUser() {
const session = await getSession()
return session?.user
}
// 权限检查中间件
export function withAuth(handler, requiredRole = null) {
return async (request, context) => {
const session = await getSession()
if (!session) {
return NextResponse.json({ error: '未授权' }, { status: 401 })
}
if (requiredRole && session.user.role !== requiredRole) {
return NextResponse.json({ error: '权限不足' }, { status: 403 })
}
return handler(request, context)
}
}
2. 客户端认证
// app/components/AuthProvider.js
'use client'
import { SessionProvider } from 'next-auth/react'
export default function AuthProvider({ children, session }) {
return (
<SessionProvider session={session}>
{children}
</SessionProvider>
)
}
// app/components/LoginForm.js
'use client'
import { useState } from 'react'
import { signIn, getSession } from 'next-auth/react'
import { useRouter } from 'next/navigation'
export default function LoginForm() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const router = useRouter()
const handleSubmit = async (e) => {
e.preventDefault()
setLoading(true)
setError('')
try {
const result = await signIn('credentials', {
email,
password,
redirect: false
})
if (result?.error) {
setError('邮箱或密码错误')
} else {
router.push('/dashboard')
router.refresh()
}
} catch (error) {
setError('登录失败,请重试')
} finally {
setLoading(false)
}
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium">
邮箱
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium">
密码
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2"
/>
</div>
{error && (
<div className="text-red-600 text-sm">{error}</div>
)}
<button
type="submit"
disabled={loading}
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50"
>
{loading ? '登录中...' : '登录'}
</button>
<button
type="button"
onClick={() => signIn('google')}
className="w-full bg-red-600 text-white py-2 px-4 rounded-md hover:bg-red-700"
>
使用 Google 登录
</button>
</form>
)
}
// app/components/ProtectedRoute.js
'use client'
import { useSession } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'
export default function ProtectedRoute({ children, requiredRole = null }) {
const { data: session, status } = useSession()
const router = useRouter()
useEffect(() => {
if (status === 'loading') return // 还在加载中
if (!session) {
router.push('/auth/signin')
return
}
if (requiredRole && session.user.role !== requiredRole) {
router.push('/unauthorized')
return
}
}, [session, status, router, requiredRole])
if (status === 'loading') {
return <div>加载中...</div>
}
if (!session) {
return null
}
if (requiredRole && session.user.role !== requiredRole) {
return null
}
return children
}