发布于

Next.js全栈开发实战:App Router与Server Components深度应用

作者

Next.js全栈开发实战:App Router与Server Components深度应用

Next.js 13+引入的App Router和Server Components为全栈开发带来了革命性的变化。本文将深入探讨这些新特性的实战应用。

App Router基础

文件系统路由

// app/layout.tsx - 根布局
import { Inter } from 'next/font/google'
import { Metadata } from 'next'
import './globals.css'
import { Providers } from './providers'
import { Header } from '@/components/Header'
import { Footer } from '@/components/Footer'

const inter = Inter({ subsets: ['latin'] })

export const metadata: Metadata = {
  title: {
    default: 'My Next.js App',
    template: '%s | My Next.js App'
  },
  description: 'A modern full-stack application built with Next.js',
  keywords: ['Next.js', 'React', 'TypeScript', 'Full-stack'],
  authors: [{ name: 'Your Name' }],
  creator: 'Your Name',
  openGraph: {
    type: 'website',
    locale: 'en_US',
    url: 'https://myapp.com',
    title: 'My Next.js App',
    description: 'A modern full-stack application',
    siteName: 'My Next.js App',
    images: [
      {
        url: '/og-image.jpg',
        width: 1200,
        height: 630,
        alt: 'My Next.js App'
      }
    ]
  },
  twitter: {
    card: 'summary_large_image',
    title: 'My Next.js App',
    description: 'A modern full-stack application',
    creator: '@yourusername',
    images: ['/og-image.jpg']
  },
  robots: {
    index: true,
    follow: true,
    googleBot: {
      index: true,
      follow: true,
      'max-video-preview': -1,
      'max-image-preview': 'large',
      'max-snippet': -1
    }
  }
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en" suppressHydrationWarning>
      <body className={inter.className}>
        <Providers>
          <div className="min-h-screen flex flex-col">
            <Header />
            <main className="flex-1">
              {children}
            </main>
            <Footer />
          </div>
        </Providers>
      </body>
    </html>
  )
}

// app/page.tsx - 首页
import { Suspense } from 'react'
import { Hero } from '@/components/Hero'
import { FeaturedPosts } from '@/components/FeaturedPosts'
import { Newsletter } from '@/components/Newsletter'
import { LoadingSpinner } from '@/components/LoadingSpinner'

export default function HomePage() {
  return (
    <div className="space-y-12">
      <Hero />
      
      <section className="container mx-auto px-4">
        <h2 className="text-3xl font-bold text-center mb-8">
          Featured Posts
        </h2>
        <Suspense fallback={<LoadingSpinner />}>
          <FeaturedPosts />
        </Suspense>
      </section>
      
      <Newsletter />
    </div>
  )
}

// app/blog/page.tsx - 博客列表页
import { Suspense } from 'react'
import { Metadata } from 'next'
import { BlogList } from '@/components/BlogList'
import { BlogFilters } from '@/components/BlogFilters'
import { Pagination } from '@/components/Pagination'
import { LoadingSpinner } from '@/components/LoadingSpinner'

export const metadata: Metadata = {
  title: 'Blog',
  description: 'Read our latest blog posts about web development and technology'
}

interface BlogPageProps {
  searchParams: {
    page?: string
    category?: string
    search?: string
    sort?: string
  }
}

export default function BlogPage({ searchParams }: BlogPageProps) {
  const page = Number(searchParams.page) || 1
  const category = searchParams.category || ''
  const search = searchParams.search || ''
  const sort = searchParams.sort || 'newest'

  return (
    <div className="container mx-auto px-4 py-8">
      <div className="mb-8">
        <h1 className="text-4xl font-bold mb-4">Blog</h1>
        <p className="text-gray-600 text-lg">
          Discover insights, tutorials, and thoughts on web development
        </p>
      </div>

      <div className="grid grid-cols-1 lg:grid-cols-4 gap-8">
        <aside className="lg:col-span-1">
          <BlogFilters />
        </aside>
        
        <main className="lg:col-span-3">
          <Suspense 
            key={`${page}-${category}-${search}-${sort}`}
            fallback={<LoadingSpinner />}
          >
            <BlogList 
              page={page}
              category={category}
              search={search}
              sort={sort}
            />
          </Suspense>
          
          <div className="mt-8">
            <Suspense fallback={<div>Loading pagination...</div>}>
              <Pagination 
                currentPage={page}
                category={category}
                search={search}
                sort={sort}
              />
            </Suspense>
          </div>
        </main>
      </div>
    </div>
  )
}

// app/blog/[slug]/page.tsx - 博客详情页
import { Suspense } from 'react'
import { Metadata } from 'next'
import { notFound } from 'next/navigation'
import { BlogPost } from '@/components/BlogPost'
import { RelatedPosts } from '@/components/RelatedPosts'
import { Comments } from '@/components/Comments'
import { ShareButtons } from '@/components/ShareButtons'
import { getBlogPost, getBlogPosts } from '@/lib/blog'

interface BlogPostPageProps {
  params: {
    slug: string
  }
}

// 生成静态参数
export async function generateStaticParams() {
  const posts = await getBlogPosts()
  
  return posts.map((post) => ({
    slug: post.slug,
  }))
}

// 生成元数据
export async function generateMetadata({ params }: BlogPostPageProps): Promise<Metadata> {
  const post = await getBlogPost(params.slug)
  
  if (!post) {
    return {
      title: 'Post Not Found'
    }
  }

  return {
    title: post.title,
    description: post.excerpt,
    authors: [{ name: post.author.name }],
    publishedTime: post.publishedAt,
    modifiedTime: post.updatedAt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      type: 'article',
      publishedTime: post.publishedAt,
      modifiedTime: post.updatedAt,
      authors: [post.author.name],
      images: [
        {
          url: post.coverImage,
          width: 1200,
          height: 630,
          alt: post.title
        }
      ]
    },
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage]
    }
  }
}

export default async function BlogPostPage({ params }: BlogPostPageProps) {
  const post = await getBlogPost(params.slug)
  
  if (!post) {
    notFound()
  }

  return (
    <article className="container mx-auto px-4 py-8">
      <div className="max-w-4xl mx-auto">
        <BlogPost post={post} />
        
        <div className="mt-8 pt-8 border-t">
          <ShareButtons 
            url={`/blog/${post.slug}`}
            title={post.title}
          />
        </div>
        
        <div className="mt-12">
          <Suspense fallback={<div>Loading related posts...</div>}>
            <RelatedPosts 
              currentPostId={post.id}
              category={post.category}
            />
          </Suspense>
        </div>
        
        <div className="mt-12">
          <Suspense fallback={<div>Loading comments...</div>}>
            <Comments postId={post.id} />
          </Suspense>
        </div>
      </div>
    </article>
  )
}

// app/blog/[slug]/not-found.tsx - 404页面
import Link from 'next/link'

export default function NotFound() {
  return (
    <div className="container mx-auto px-4 py-16 text-center">
      <h1 className="text-4xl font-bold mb-4">Post Not Found</h1>
      <p className="text-gray-600 mb-8">
        The blog post you're looking for doesn't exist.
      </p>
      <Link 
        href="/blog"
        className="inline-block bg-blue-600 text-white px-6 py-3 rounded-lg hover:bg-blue-700 transition-colors"
      >
        Back to Blog
      </Link>
    </div>
  )
}

路由组和布局

// app/(dashboard)/layout.tsx - 仪表板布局
import { Sidebar } from '@/components/dashboard/Sidebar'
import { DashboardHeader } from '@/components/dashboard/Header'
import { AuthGuard } from '@/components/AuthGuard'

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <AuthGuard>
      <div className="flex h-screen bg-gray-100">
        <Sidebar />
        <div className="flex-1 flex flex-col overflow-hidden">
          <DashboardHeader />
          <main className="flex-1 overflow-x-hidden overflow-y-auto bg-gray-100 p-6">
            {children}
          </main>
        </div>
      </div>
    </AuthGuard>
  )
}

// app/(dashboard)/dashboard/page.tsx - 仪表板首页
import { Suspense } from 'react'
import { StatsCards } from '@/components/dashboard/StatsCards'
import { RecentActivity } from '@/components/dashboard/RecentActivity'
import { Charts } from '@/components/dashboard/Charts'
import { LoadingCard } from '@/components/dashboard/LoadingCard'

export default function DashboardPage() {
  return (
    <div className="space-y-6">
      <div>
        <h1 className="text-2xl font-semibold text-gray-900">Dashboard</h1>
        <p className="mt-1 text-sm text-gray-600">
          Welcome back! Here's what's happening with your account.
        </p>
      </div>

      <Suspense fallback={<LoadingCard />}>
        <StatsCards />
      </Suspense>

      <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
        <Suspense fallback={<LoadingCard />}>
          <Charts />
        </Suspense>
        
        <Suspense fallback={<LoadingCard />}>
          <RecentActivity />
        </Suspense>
      </div>
    </div>
  )
}

// app/(dashboard)/dashboard/posts/page.tsx - 文章管理
import { Suspense } from 'react'
import Link from 'next/link'
import { PostsTable } from '@/components/dashboard/PostsTable'
import { PostsFilters } from '@/components/dashboard/PostsFilters'
import { Button } from '@/components/ui/Button'
import { PlusIcon } from '@heroicons/react/24/outline'

interface PostsPageProps {
  searchParams: {
    page?: string
    status?: string
    search?: string
  }
}

export default function PostsPage({ searchParams }: PostsPageProps) {
  return (
    <div className="space-y-6">
      <div className="flex justify-between items-center">
        <div>
          <h1 className="text-2xl font-semibold text-gray-900">Posts</h1>
          <p className="mt-1 text-sm text-gray-600">
            Manage your blog posts and articles
          </p>
        </div>
        
        <Link href="/dashboard/posts/new">
          <Button className="flex items-center gap-2">
            <PlusIcon className="h-4 w-4" />
            New Post
          </Button>
        </Link>
      </div>

      <PostsFilters />

      <Suspense fallback={<div>Loading posts...</div>}>
        <PostsTable searchParams={searchParams} />
      </Suspense>
    </div>
  )
}

// app/(dashboard)/dashboard/posts/[id]/edit/page.tsx - 编辑文章
import { Suspense } from 'react'
import { notFound } from 'next/navigation'
import { PostEditor } from '@/components/dashboard/PostEditor'
import { getPost } from '@/lib/posts'

interface EditPostPageProps {
  params: {
    id: string
  }
}

export default async function EditPostPage({ params }: EditPostPageProps) {
  const post = await getPost(params.id)
  
  if (!post) {
    notFound()
  }

  return (
    <div className="space-y-6">
      <div>
        <h1 className="text-2xl font-semibold text-gray-900">Edit Post</h1>
        <p className="mt-1 text-sm text-gray-600">
          Make changes to your blog post
        </p>
      </div>

      <Suspense fallback={<div>Loading editor...</div>}>
        <PostEditor post={post} />
      </Suspense>
    </div>
  )
}

Server Components与数据获取

Server Components实现

// components/FeaturedPosts.tsx - Server Component
import { getBlogPosts } from '@/lib/blog'
import { PostCard } from './PostCard'

export async function FeaturedPosts() {
  // 在服务器端获取数据
  const posts = await getBlogPosts({ 
    featured: true, 
    limit: 6 
  })

  if (posts.length === 0) {
    return (
      <div className="text-center py-12">
        <p className="text-gray-600">No featured posts available.</p>
      </div>
    )
  }

  return (
    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
      {posts.map((post) => (
        <PostCard key={post.id} post={post} />
      ))}
    </div>
  )
}

// components/BlogList.tsx - Server Component with search params
import { getBlogPosts, getBlogPostsCount } from '@/lib/blog'
import { PostCard } from './PostCard'
import { EmptyState } from './EmptyState'

interface BlogListProps {
  page: number
  category: string
  search: string
  sort: string
}

export async function BlogList({ page, category, search, sort }: BlogListProps) {
  const limit = 12
  const offset = (page - 1) * limit

  // 并行获取数据
  const [posts, totalCount] = await Promise.all([
    getBlogPosts({
      limit,
      offset,
      category,
      search,
      sort,
      published: true
    }),
    getBlogPostsCount({
      category,
      search,
      published: true
    })
  ])

  if (posts.length === 0) {
    return (
      <EmptyState 
        title="No posts found"
        description="Try adjusting your search criteria or browse all posts."
      />
    )
  }

  return (
    <div className="space-y-6">
      <div className="flex justify-between items-center">
        <p className="text-sm text-gray-600">
          Showing {offset + 1}-{Math.min(offset + limit, totalCount)} of {totalCount} posts
        </p>
      </div>
      
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        {posts.map((post) => (
          <PostCard key={post.id} post={post} />
        ))}
      </div>
    </div>
  )
}

// components/dashboard/StatsCards.tsx - Server Component with data fetching
import { getStats } from '@/lib/dashboard'
import { StatsCard } from './StatsCard'
import { 
  UserGroupIcon, 
  DocumentTextIcon, 
  EyeIcon, 
  HeartIcon 
} from '@heroicons/react/24/outline'

export async function StatsCards() {
  const stats = await getStats()

  const cards = [
    {
      title: 'Total Users',
      value: stats.totalUsers,
      change: stats.userGrowth,
      icon: UserGroupIcon,
      color: 'blue'
    },
    {
      title: 'Total Posts',
      value: stats.totalPosts,
      change: stats.postGrowth,
      icon: DocumentTextIcon,
      color: 'green'
    },
    {
      title: 'Page Views',
      value: stats.totalViews,
      change: stats.viewGrowth,
      icon: EyeIcon,
      color: 'purple'
    },
    {
      title: 'Total Likes',
      value: stats.totalLikes,
      change: stats.likeGrowth,
      icon: HeartIcon,
      color: 'pink'
    }
  ]

  return (
    <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
      {cards.map((card) => (
        <StatsCard key={card.title} {...card} />
      ))}
    </div>
  )
}

// lib/blog.ts - 数据获取函数
import { cache } from 'react'
import { prisma } from './prisma'

// 使用React cache缓存函数结果
export const getBlogPosts = cache(async (options: {
  limit?: number
  offset?: number
  category?: string
  search?: string
  sort?: string
  featured?: boolean
  published?: boolean
} = {}) => {
  const {
    limit = 10,
    offset = 0,
    category,
    search,
    sort = 'newest',
    featured,
    published
  } = options

  const where: any = {}

  if (published !== undefined) {
    where.published = published
  }

  if (featured !== undefined) {
    where.featured = featured
  }

  if (category) {
    where.category = {
      slug: category
    }
  }

  if (search) {
    where.OR = [
      { title: { contains: search, mode: 'insensitive' } },
      { excerpt: { contains: search, mode: 'insensitive' } },
      { content: { contains: search, mode: 'insensitive' } }
    ]
  }

  const orderBy: any = {}
  switch (sort) {
    case 'oldest':
      orderBy.publishedAt = 'asc'
      break
    case 'popular':
      orderBy.views = 'desc'
      break
    case 'title':
      orderBy.title = 'asc'
      break
    default:
      orderBy.publishedAt = 'desc'
  }

  const posts = await prisma.post.findMany({
    where,
    orderBy,
    take: limit,
    skip: offset,
    include: {
      author: {
        select: {
          id: true,
          name: true,
          email: true,
          avatar: true
        }
      },
      category: {
        select: {
          id: true,
          name: true,
          slug: true
        }
      },
      tags: {
        select: {
          id: true,
          name: true,
          slug: true
        }
      },
      _count: {
        select: {
          comments: true,
          likes: true
        }
      }
    }
  })

  return posts
})

export const getBlogPost = cache(async (slug: string) => {
  const post = await prisma.post.findUnique({
    where: { slug },
    include: {
      author: {
        select: {
          id: true,
          name: true,
          email: true,
          avatar: true,
          bio: true
        }
      },
      category: {
        select: {
          id: true,
          name: true,
          slug: true
        }
      },
      tags: {
        select: {
          id: true,
          name: true,
          slug: true
        }
      },
      comments: {
        where: { approved: true },
        include: {
          author: {
            select: {
              id: true,
              name: true,
              avatar: true
            }
          }
        },
        orderBy: { createdAt: 'desc' }
      },
      _count: {
        select: {
          comments: true,
          likes: true
        }
      }
    }
  })

  if (post) {
    // 增加浏览量
    await prisma.post.update({
      where: { id: post.id },
      data: { views: { increment: 1 } }
    })
  }

  return post
})

export const getBlogPostsCount = cache(async (options: {
  category?: string
  search?: string
  published?: boolean
} = {}) => {
  const { category, search, published } = options

  const where: any = {}

  if (published !== undefined) {
    where.published = published
  }

  if (category) {
    where.category = {
      slug: category
    }
  }

  if (search) {
    where.OR = [
      { title: { contains: search, mode: 'insensitive' } },
      { excerpt: { contains: search, mode: 'insensitive' } },
      { content: { contains: search, mode: 'insensitive' } }
    ]
  }

  return prisma.post.count({ where })
})

总结

Next.js全栈开发的核心要点:

  1. App Router:文件系统路由、布局嵌套、路由组织
  2. Server Components:服务端渲染、数据获取、性能优化
  3. 数据流:缓存策略、重新验证、状态管理
  4. API设计:RESTful API、Server Actions、错误处理
  5. 最佳实践:TypeScript集成、SEO优化、性能监控

Next.js 13+的新特性为全栈开发提供了更强大的能力,通过合理使用这些特性,可以构建出高性能、可扩展的现代Web应用。