Next.js全栈框架实战:SSR渲染与API Routes数据交互

Next.js是基于React的全栈框架,内置服务端渲染(SSR)、静态生成(SSG)、API路由等能力,免去了单独搭建后端服务的成本。从Next.js 13开始引入的App Router架构,通过React Server Components实现了更细粒度的渲染控制。以下是Next.js全栈开发的实战配置,覆盖SSR渲染、API Routes、数据获取与部署的完整流程。

App Router架构与渲染模式

Next.js 13+的App Router使用app/目录组织路由,每个目录代表一个路由段,page.tsx为路由组件。渲染模式由文件中导出的组件类型和函数决定:

app/
  layout.tsx        # 根布局(Server Component)
  page.tsx          # 首页
  blog/
    page.tsx        # /blog 页面
    [slug]/
      page.tsx      # /blog/:slug 动态路由
  api/
    users/
      route.ts      # API路由 /api/users

四种渲染模式的区别:

SSG (Static Site Generation)  - 构建时生成HTML,适合内容固定的页面
SSR (Server-Side Rendering)    - 每次请求时生成HTML,适合个性化内容
ISR (Incremental Static Regeneration) - 静态生成+定时更新,兼顾性能与时效
RSC (React Server Components)  - 服务端组件,零客户端JS开销

Server Components与Client Components

App Router中默认所有组件都是Server Component,在服务端渲染且不打包到客户端JS中。需要交互的组件通过’use client’声明为Client Component:

// app/blog/page.tsx (Server Component - 默认)
import { db } from '@/lib/db'
import { PostCard } from '@/components/PostCard'

// 服务端直接访问数据库,不暴露连接信息到客户端
async function getPosts() {
  const posts = await db.post.findMany({
    orderBy: { createdAt: 'desc' },
    take: 20
  })
  return posts
}

export default async function BlogPage() {
  const posts = await getPosts()
  
  return (
    <main>
      <h1>博客文章</h1>
      {posts.map(post => (
        <PostCard key={post.id} post={post} />
      ))}
    </main>
  )
}

// components/PostCard.tsx
// Server Component,无'use client'声明
export function PostCard({ post }: { post: Post }) {
  return (
    <article>
      <h2>{post.title}</h2>
      <p>{post.excerpt}</p>
      <time>{post.createdAt.toLocaleDateString()}</time>
    </article>
  )
}

// components/LikeButton.tsx
'use client'  // 声明为Client Component
import { useState } from 'react'

export function LikeButton({ postId }: { postId: string }) {
  const [liked, setLiked] = useState(false)
  const [count, setCount] = useState(0)
  
  const handleLike = async () => {
    const res = await fetch(`/api/posts/${postId}/like`, {
      method: 'POST'
    })
    if (res.ok) {
      setLiked(!liked)
      setCount(prev => liked ? prev - 1 : prev + 1)
    }
  }
  
  return (
    <button onClick={handleLike}>
      {liked ? '取消' : '点赞'} ({count})
    </button>
  )
}

数据获取与缓存策略

Next.js扩展了fetch API,内置缓存和重验证支持:

// app/blog/[slug]/page.tsx
// SSR:每次请求都重新获取数据
export default async function PostPage({ params }: { params: { slug: string } }) {
  const res = await fetch(`https://api.example.com/posts/${params.slug}`, {
    cache: 'no-store'  // 禁用缓存,每次请求都获取最新数据
  })
  const post = await res.json()
  
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

// ISR:静态生成,每60秒重新验证
export default async function BlogList() {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 60 }  // 60秒后重新生成
  })
  const posts = await res.json()
  // ...
}

// SSG:构建时获取数据
export default async function AboutPage() {
  const res = await fetch('https://api.example.com/about', {
    cache: 'force-cache'  // 构建时获取并永久缓存
  })
  // ...
}

动态路由段的参数验证和静态参数生成:

// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation'

// 生成静态路径(SSG模式)
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(r => r.json())
  return posts.map((post: { slug: string }) => ({
    slug: post.slug
  }))
}

// 动态metadata
export async function generateMetadata({ params }: { params: { slug: string } }) {
  const post = await fetch(`https://api.example.com/posts/${params.slug}`).then(r => r.json())
  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      images: [post.coverImage]
    }
  }
}

export default async function PostPage({ params }: { params: { slug: string } }) {
  const res = await fetch(`https://api.example.com/posts/${params.slug}`)
  
  if (!res.ok) {
    notFound()  // 返回404页面
  }
  
  const post = await res.json()
  // ...
}

API Routes开发

Next.js的API路由在app/api/目录下定义,使用Route Handler语法:

// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
import { validateToken } from '@/lib/auth'

// GET /api/users - 获取用户列表
export async function GET(request: NextRequest) {
  const auth = validateToken(request)
  if (!auth) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }
  
  const { searchParams } = new URL(request.url)
  const page = parseInt(searchParams.get('page') || '1')
  const limit = parseInt(searchParams.get('limit') || '20')
  
  const users = await db.user.findMany({
    skip: (page - 1) * limit,
    take: limit,
    select: {
      id: true,
      name: true,
      email: true,
      createdAt: true
    }
  })
  
  const total = await db.user.count()
  
  return NextResponse.json({
    data: users,
    pagination: {
      page,
      limit,
      total,
      totalPages: Math.ceil(total / limit)
    }
  })
}

// POST /api/users - 创建用户
export async function POST(request: NextRequest) {
  const body = await request.json()
  
  // 输入验证
  if (!body.email || !body.name) {
    return NextResponse.json(
      { error: 'email and name are required' },
      { status: 400 }
    )
  }
  
  // 检查邮箱是否已存在
  const existing = await db.user.findUnique({
    where: { email: body.email }
  })
  if (existing) {
    return NextResponse.json(
      { error: 'Email already exists' },
      { status: 409 }
    )
  }
  
  const user = await db.user.create({
    data: {
      name: body.name,
      email: body.email,
      role: body.role || 'user'
    }
  })
  
  return NextResponse.json(user, { status: 201 })
}

// app/api/users/[id]/route.ts
// GET /api/users/:id
export async function GET(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  const user = await db.user.findUnique({
    where: { id: params.id }
  })
  
  if (!user) {
    return NextResponse.json({ error: 'User not found' }, { status: 404 })
  }
  
  return NextResponse.json(user)
}

// DELETE /api/users/:id
export async function DELETE(
  request: NextRequest,
  { params }: { params: { id: string } }
) {
  await db.user.delete({
    where: { id: params.id }
  })
  
  return new NextResponse(null, { status: 204 })
}

Middleware中间件鉴权

Next.js Middleware在请求进入页面或API前执行,适合做认证、重定向等逻辑:

// middleware.ts (项目根目录)
import { NextRequest, NextResponse } from 'next/server'

export function middleware(request: NextRequest) {
  const token = request.cookies.get('auth-token')?.value
  
  // 保护/dashboard路由
  if (request.nextUrl.pathname.startsWith('/dashboard')) {
    if (!token) {
      const loginUrl = new URL('/login', request.url)
      loginUrl.searchParams.set('redirect', request.nextUrl.pathname)
      return NextResponse.redirect(loginUrl)
    }
  }
  
  // 保护API路由(除了登录注册)
  if (request.nextUrl.pathname.startsWith('/api')) {
    const publicPaths = ['/api/auth/login', '/api/auth/register']
    if (!publicPaths.includes(request.nextUrl.pathname) && !token) {
      return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
    }
  }
  
  return NextResponse.next()
}

export const config = {
  matcher: [
    '/dashboard/:path*',
    '/api/:path*'
  ]
}

部署配置与性能优化

next.config.js配置:

/** @type {import('next').NextConfig} */
const nextConfig = {
  // 图片优化
  images: {
    formats: ['image/avif', 'image/webp'],
    remotePatterns: [
      { protocol: 'https', hostname: 'cdn.example.com' }
    ]
  },
  
  // 自定义header
  async headers() {
    return [
      {
        source: '/(.*)',
        headers: [
          { key: 'X-Content-Type-Options', value: 'nosniff' },
          { key: 'X-Frame-Options', value: 'DENY' },
          { key: 'X-XSS-Protection', value: '1; mode=block' }
        ]
      }
    ]
  },
  
  // 重写规则(API代理)
  async rewrites() {
    return [
      {
        source: '/api/v2/:path*',
        destination: 'https://api-v2.example.com/:path*'
      }
    ]
  }
}

module.exports = nextConfig

使用Server Actions替代传统API调用(Next.js 14+):

// app/posts/create/page.tsx
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'

async function createPost(formData: FormData) {
  'use server'
  
  const title = formData.get('title') as string
  const content = formData.get('content') as string
  
  await db.post.create({
    data: { title, content, authorId: currentUser.id }
  })
  
  // 重新生成博客列表页
  revalidatePath('/blog')
  redirect('/blog')
}

export default function CreatePostPage() {
  return (
    <form action={createPost}>
      <input name="title" type="text" required />
      <textarea name="content" required />
      <button type="submit">发布</button>
    </form>
  )
}

Server Actions省去了手写API路由和fetch调用的步骤,表单提交直接在服务端处理,且自动处理CSRF防护。revalidatePath在数据变更后触发对应页面的重新生成,保持SSR内容与数据的实时同步。

Docker容器化部署

# Dockerfile
FROM node:20-alpine AS base

FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci

FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build

FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
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"]

next.config.js中需开启standalone输出:

const nextConfig = {
  output: 'standalone'
}

构建产出的.next/standalone目录包含独立运行所需的最小依赖,无需node_modules,镜像体积可控制在200MB以内。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/nextjs-quan-zhan-kuang-jia-shi-zhan-ssr-xuan-ran-yu/

(0)
小编小编
上一篇 1天前
下一篇 1天前

相关推荐