发布于

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

作者

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>&copy; 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
}