Skip to content

Next.js 静态生成 (SSG) 📄

🎯 概述

静态生成(Static Site Generation,SSG)是 Next.js 在构建时预渲染页面的方式,生成静态 HTML 文件,提供最佳性能和 SEO 效果。

📦 什么是 SSG?

SSG 工作原理

  1. 构建时获取数据
  2. 生成静态 HTML 文件
  3. 部署到 CDN
  4. 用户访问时直接返回 HTML
  5. 客户端激活

SSG 优势

  • ⚡ 极快的加载速度
  • 🔍 完美的 SEO
  • 💰 低服务器成本
  • 🌐 易于 CDN 分发
  • 🔒 更高的安全性

🚀 App Router 中的 SSG

默认静态生成

tsx
// app/page.tsx
// 默认情况下,没有动态函数的页面会被静态生成
export default function Page() {
  return <h1>静态页面</h1>
}

带数据的静态生成

tsx
// app/posts/page.tsx
async function getPosts() {
  const res = await fetch('https://api.example.com/posts')
  return res.json()
}

export default async function Posts() {
  const posts = await getPosts()
  
  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
        </article>
      ))}
    </div>
  )
}

动态路由静态生成

tsx
// app/posts/[id]/page.tsx
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(res => res.json())
  
  return posts.map((post) => ({
    id: post.id,
  }))
}

async function getPost(id: string) {
  const res = await fetch(`https://api.example.com/posts/${id}`)
  return res.json()
}

export default async function Post({ params }: { params: { id: string } }) {
  const post = await getPost(params.id)
  
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  )
}

📝 Pages Router 中的 SSG

getStaticProps

tsx
// pages/posts/index.tsx
import { GetStaticProps } from 'next'

interface Post {
  id: string
  title: string
}

interface Props {
  posts: Post[]
}

export const getStaticProps: GetStaticProps<Props> = async () => {
  const res = await fetch('https://api.example.com/posts')
  const posts = await res.json()
  
  return {
    props: {
      posts,
    },
    revalidate: 60, // ISR: 每60秒重新生成
  }
}

export default function Posts({ posts }: Props) {
  return (
    <div>
      {posts.map(post => (
        <article key={post.id}>{post.title}</article>
      ))}
    </div>
  )
}

getStaticPaths

tsx
// pages/posts/[id].tsx
import { GetStaticPaths, GetStaticProps } from 'next'

export const getStaticPaths: GetStaticPaths = async () => {
  const res = await fetch('https://api.example.com/posts')
  const posts = await res.json()
  
  const paths = posts.map((post) => ({
    params: { id: post.id },
  }))
  
  return {
    paths,
    fallback: false, // 404 for other routes
  }
}

export const getStaticProps: GetStaticProps = async ({ params }) => {
  const res = await fetch(`https://api.example.com/posts/${params!.id}`)
  const post = await res.json()
  
  return {
    props: {
      post,
    },
  }
}

export default function Post({ post }) {
  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  )
}

🔄 Fallback 模式

fallback: false

tsx
export const getStaticPaths = async () => {
  return {
    paths: [
      { params: { id: '1' } },
      { params: { id: '2' } },
    ],
    fallback: false, // 其他路径返回 404
  }
}

fallback: true

tsx
// pages/posts/[id].tsx
import { useRouter } from 'next/router'

export const getStaticPaths = async () => {
  return {
    paths: [{ params: { id: '1' } }],
    fallback: true, // 其他路径会在首次访问时生成
  }
}

export default function Post({ post }) {
  const router = useRouter()
  
  if (router.isFallback) {
    return <div>加载中...</div>
  }
  
  return <article>{post.title}</article>
}

fallback: 'blocking'

tsx
export const getStaticPaths = async () => {
  return {
    paths: [],
    fallback: 'blocking', // 服务端生成,不显示加载状态
  }
}

🎨 增量静态再生 (ISR)

App Router ISR

tsx
// app/posts/page.tsx
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 60 } // 每60秒重新验证
  })
  return res.json()
}

export default async function Posts() {
  const posts = await getPosts()
  return <div>{/* ... */}</div>
}

Pages Router ISR

tsx
// pages/posts/index.tsx
export const getStaticProps = async () => {
  const posts = await fetchPosts()
  
  return {
    props: { posts },
    revalidate: 60, // 每60秒重新生成
  }
}

按需重新验证

tsx
// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache'
import { NextRequest } from 'next/server'

export async function POST(request: NextRequest) {
  const path = request.nextUrl.searchParams.get('path')
  
  if (path) {
    revalidatePath(path)
    return Response.json({ revalidated: true, now: Date.now() })
  }
  
  return Response.json({ revalidated: false, now: Date.now() })
}

使用:

bash
curl -X POST http://localhost:3000/api/revalidate?path=/posts

📊 性能优化

1. 部分预渲染

tsx
// app/posts/[id]/page.tsx
export async function generateStaticParams() {
  // 只预渲染热门文章
  const popularPosts = await getPopularPosts()
  
  return popularPosts.slice(0, 100).map((post) => ({
    id: post.id,
  }))
}

export const dynamicParams = true // 允许其他路径动态生成

2. 并行数据获取

tsx
async function getPageData() {
  const [posts, categories, tags] = await Promise.all([
    fetch('https://api.example.com/posts').then(r => r.json()),
    fetch('https://api.example.com/categories').then(r => r.json()),
    fetch('https://api.example.com/tags').then(r => r.json()),
  ])
  
  return { posts, categories, tags }
}

3. 图片优化

tsx
import Image from 'next/image'

export default function Post({ post }) {
  return (
    <article>
      <Image
        src={post.coverImage}
        alt={post.title}
        width={1200}
        height={630}
        priority // 优先加载
      />
      <h1>{post.title}</h1>
    </article>
  )
}

🎯 实战示例

博客网站

tsx
// app/blog/page.tsx
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { revalidate: 3600 } // 1小时
  })
  return res.json()
}

export default async function Blog() {
  const posts = await getPosts()
  
  return (
    <div className="blog-grid">
      {posts.map(post => (
        <article key={post.id}>
          <h2>{post.title}</h2>
          <p>{post.excerpt}</p>
          <Link href={`/blog/${post.slug}`}>阅读更多</Link>
        </article>
      ))}
    </div>
  )
}

// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts')
    .then(res => res.json())
  
  return posts.map((post) => ({
    slug: post.slug,
  }))
}

async function getPost(slug: string) {
  const res = await fetch(`https://api.example.com/posts/${slug}`, {
    next: { revalidate: 3600 }
  })
  return res.json()
}

export default async function BlogPost({ params }) {
  const post = await getPost(params.slug)
  
  return (
    <article>
      <h1>{post.title}</h1>
      <time>{post.publishedAt}</time>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

产品目录

tsx
// app/products/page.tsx
async function getProducts() {
  const res = await fetch('https://api.example.com/products', {
    next: { revalidate: 300 } // 5分钟
  })
  return res.json()
}

export default async function Products() {
  const products = await getProducts()
  
  return (
    <div className="product-grid">
      {products.map(product => (
        <div key={product.id}>
          <Image
            src={product.image}
            alt={product.name}
            width={300}
            height={300}
          />
          <h3>{product.name}</h3>
          <p>{product.price}元</p>
        </div>
      ))}
    </div>
  )
}

📚 最佳实践

1. 选择合适的渲染策略

tsx
// 完全静态 - 内容很少变化
export default async function AboutPage() {
  return <div>关于我们</div>
}

// ISR - 内容定期更新
async function getData() {
  const res = await fetch('url', {
    next: { revalidate: 3600 }
  })
  return res.json()
}

// 动态 - 内容频繁变化
async function getData() {
  const res = await fetch('url', {
    cache: 'no-store'
  })
  return res.json()
}

2. 优化构建时间

tsx
// 限制预渲染数量
export async function generateStaticParams() {
  const posts = await getPosts()
  
  // 只预渲染最新的100篇文章
  return posts.slice(0, 100).map(post => ({
    id: post.id,
  }))
}

// 允许其他路径按需生成
export const dynamicParams = true

3. 错误处理

tsx
export async function generateStaticParams() {
  try {
    const posts = await getPosts()
    return posts.map(post => ({ id: post.id }))
  } catch (error) {
    console.error('获取文章列表失败:', error)
    return [] // 返回空数组,避免构建失败
  }
}

🔗 相关资源


下一步:学习 Next.js 增量静态再生 (ISR)