普通视图

发现新文章,点击刷新页面。
昨天 — 2026年4月5日首页

Next.js第六课 - 数据获取

2026年4月5日 09:33

上节我们学习了服务端组件和客户端组件的区别,本节来深入了解 Next.js 中的数据获取。Next.js 提供了灵活且强大的数据获取方式,掌握好这些知识能让你构建出性能优异的应用。

数据获取概述

在 Next.js 中,有多种数据获取方式:

  1. 静态生成(SSG)- 构建时生成 HTML
  2. 服务器端渲染(SSR)- 每次请求时生成 HTML
  3. 增量静态再生成(ISR)- 定期重新生成静态页面
  4. 客户端数据获取 - 在浏览器中获取数据

选择哪种方式取决于你的数据特性:是否经常变化、是否需要 SEO、用户是否需要看到最新数据等。

服务端组件数据获取

在服务端组件中,你可以直接使用 fetch 或任何数据获取库,这相比传统的 React 方式要简单很多。

基本数据获取

最简单的方式就是直接在组件中使用 async/await:

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

  if (!res.ok) {
    throw new Error('获取文章失败')
  }

  return res.json()
}

export default async function PostsPage() {
  const posts = await getPosts()

  return (
    <div>
      <h1>博客文章</h1>
      <ul>
        {posts.map((post: any) => (
          <li key={post.id}>
            <h2>{post.title}</h2>
            <p>{post.excerpt}</p>
          </li>
        ))}
      </ul>
    </div>
  )
}

直接访问数据库

服务端组件可以直接访问数据库,不需要创建 API 层:

// app/users/page.tsx
import { db } from '@/lib/db'

export default async function UsersPage() {
  const users = await db.user.findMany({
    orderBy: { createdAt: 'desc' },
  })

  return (
    <div>
      <h1>用户列表</h1>
      {users.map((user) => (
        <div key={user.id}>{user.name}</div>
      ))}
    </div>
  )
}

错误处理

处理数据获取中的错误是很重要的:

// app/products/[id]/page.tsx
export default async function ProductPage({
  params,
}: {
  params: { id: string }
}) {
  let product

  try {
    const res = await fetch(`https://api.example.com/products/${params.id}`)

    if (!res.ok) {
      if (res.status === 404) {
        notFound()
      }
      throw new Error('获取产品失败')
    }

    product = await res.json()
  } catch (error) {
    return <div>加载产品时出错</div>
  }

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </div>
  )
}

缓存和重新验证

Next.js 的缓存系统非常强大,理解它能让你的应用性能提升很多。

默认缓存行为

Next.js 默认会缓存 fetch 请求,这意味着相同的数据请求会被缓存起来,避免重复获取:

// 默认:自动缓存
export default async function Page() {
  const res = await fetch('https://api.example.com/data')
  const data = await res.json()

  return <div>{/* ... */}</div>
}

禁用缓存

如果数据需要实时更新,可以禁用缓存:

// 不缓存:每次都获取新数据
export default async function Page() {
  const res = await fetch('https://api.example.com/data', {
    cache: 'no-store',
  })
  const data = await res.json()

  return <div>{/* ... */}</div>
}

设置重新验证时间

最常用的方式是设置缓存时间:

// 缓存 10 秒后重新验证
export default async function Page() {
  const res = await fetch('https://api.example.com/data', {
    next: { revalidate: 10 },
  })
  const data = await res.json()

  return <div>{/* ... */}</div>
}

按标签重新验证

给数据加上标签,可以在数据更新时手动刷新缓存:

// 获取时添加标签
export default async function Page() {
  const res = await fetch('https://api.example.com/posts', {
    next: { tags: ['posts'] },
  })
  const posts = await res.json()

  return <div>{/* ... */}</div>
}

// 在 API 路由中手动重新验证
// app/api/revalidate/route.ts
import { revalidateTag } from 'next/cache'

export async function POST(request: Request) {
  const tag = await request.json()
  revalidateTag(tag)
  return Response.json({ revalidated: true })
}

渲染策略

静态渲染(默认)

静态渲染意味着页面在构建时就生成好了 HTML:

// app/blog/page.tsx
// 构建时生成静态 HTML
export default async function BlogPage() {
  const posts = await getPosts()

  return (
    <div>
      {posts.map((post) => (
        <PostCard key={post.id} post={post} />
      ))}
    </div>
  )
}

动态渲染

每次请求都重新渲染:

// app/dashboard/page.tsx
// 每次请求都渲染
export const dynamic = 'force-dynamic'

export default async function DashboardPage() {
  const user = await getCurrentUser()

  return <div>欢迎,{user.name}</div>
}

增量静态再生成(ISR)

结合静态和动态的优点:

// app/products/page.tsx
// 每 60 秒重新生成页面
export const revalidate = 60

export default async function ProductsPage() {
  const products = await getProducts()

  return (
    <div>
      {products.map((product) => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  )
}

并行数据获取

当需要获取多个独立的数据源时,应该并行获取以提高性能:

// app/dashboard/page.tsx
export default async function DashboardPage() {
  // 并行获取数据
  const [user, posts, analytics] = await Promise.all([
    fetchUser(),
    fetchPosts(),
    fetchAnalytics(),
  ])

  return (
    <div>
      <UserProfile user={user} />
      <PostsList posts={posts} />
      <AnalyticsChart data={analytics} />
    </div>
  )
}

客户端数据获取

虽然服务端数据获取更推荐,但有时候也需要在客户端获取数据:

使用 useEffect

传统的方式:

'use client'

import { useState, useEffect } from 'react'

export default function ClientDataFetching() {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  useEffect(() => {
    async function fetchData() {
      try {
        const res = await fetch('https://api.example.com/data')
        const json = await res.json()
        setData(json)
      } catch (err) {
        setError(err.message)
      } finally {
        setLoading(false)
      }
    }

    fetchData()
  }, [])

  if (loading) return <div>加载中...</div>
  if (error) return <div>错误: {error}</div>

  return <div>{/* 渲染数据 */}</div>
}

使用 SWR

SWR 是一个很流行的数据获取库:

'use client'

import useSWR from 'swr'

const fetcher = (url: string) => fetch(url).then((res) => res.json())

export default function Profile() {
  const { data, error, isLoading } = useSWR(
    'https://api.example.com/user',
    fetcher
  )

  if (error) return <div>加载失败</div>
  if (isLoading) return <div>加载中...</div>

  return <div>你好,{data.name}</div>
}

加载状态

Next.js 提供了优雅的加载状态处理方式:

loading.tsx 文件

创建 loading.tsx 文件会自动显示加载状态:

// app/posts/loading.tsx
export default function Loading() {
  return (
    <div className="animate-pulse">
      <div className="h-4 bg-gray-200 w-3/4 mb-4"></div>
      <div className="h-4 bg-gray-200 w-1/2 mb-4"></div>
      <div className="h-4 bg-gray-200 w-5/6"></div>
    </div>
  )
}

实用建议

这里分享几个在日常开发中特别实用的数据获取技巧。

优先使用服务端组件

实际开发中,我发现服务端数据获取不仅性能更好,代码也更简洁:

// 推荐这样做 - 服务端组件数据获取
export default async function Page() {
  const data = await fetchData()
  return <div>{data.title}</div>
}

// 除非有特殊需求,否则避免客户端数据获取
'use client'
export default function Page() {
  const [data, setData] = useState(null)
  useEffect(() => {
    fetchData().then(setData)
  }, [])
  return <div>{data?.title}</div>
}

并行获取数据

这个小技巧特别有用——如果多个数据源是独立的,应该并行获取来提升性能:

// 推荐这样做 - 并行获取,速度更快
const [user, posts] = await Promise.all([
  fetchUser(),
  fetchPosts(),
])

// 避免这种情况 - 串行获取会拖慢速度
const user = await fetchUser()
const posts = await fetchPosts() // 要等 user 完成才开始

使用适当的缓存策略

根据数据特性选择缓存策略,这个在实际项目中特别重要:

// 静态内容可以长时间缓存
const posts = await fetch('https://api.com/posts', {
  next: { revalidate: 3600 },
})

// 实时数据建议不缓存
const stockPrices = await fetch('https://api.com/stocks', {
  cache: 'no-store',
})

总结

本节我们详细学习了 Next.js 的数据获取机制,包括服务端和客户端获取、缓存策略、渲染策略等。掌握好这些知识,你就能根据不同的场景选择最合适的数据获取方式,构建出高性能的应用。

如果你对本节内容有任何疑问,欢迎在评论区提出来,我们一起学习讨论。

原文地址: blog.uuhb.cn/archives/ne…

昨天以前首页

Next.js第四课 - 链接与导航

2026年4月3日 11:41

上节我们学习了布局和页面的使用,本节来聊聊链接和导航。在 Web 应用中,页面之间的跳转是最基本也是最重要的功能之一。Next.js 提供了强大的导航系统,不仅使用简单,而且性能优化做得非常好。

Link 组件

Link 组件是 Next.js 中进行导航的主要方式,它扩展了 HTML 的 <a> 标签,提供客户端导航功能。很多初学者会问,为什么不直接用 <a> 标签呢?其实 <Link> 组件在底层做了很多优化,比如预加载、客户端导航等,能让页面切换更加流畅。

基本用法

使用 Link 组件非常简单,只需要导入并使用它:

import Link from 'next/link'

export default function Navigation() {
  return (
    <nav>
      <Link href="/">首页</Link>
      <Link href="/about">关于</Link>
      <Link href="/contact">联系</Link>
    </nav>
  )
}

动态链接

在实际开发中,我们经常需要根据数据动态生成链接,比如文章列表、产品列表等:

import Link from 'next/link'

export default function PostList({ posts }: { posts: Post[] }) {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <Link href={`/posts/${post.slug}`}>{post.title}</Link>
        </li>
      ))}
    </ul>
  )
}

链接属性

Link 组件支持很多有用的属性,可以满足各种导航需求:

import Link from 'next/link'

export default function Links() {
  return (
    <>
      {/* 新标签页打开 */}
      <Link href="/about" target="_blank" rel="noopener noreferrer">
        关于(新窗口)
      </Link>

      {/* 替换当前历史记录 */}
      <Link href="/dashboard" replace>
        仪表盘
      </Link>

      {/* 滚动到特定位置 */}
      <Link href="/about#team" scroll={false}>
        关于(不滚动)
      </Link>

      {/* 自定义类名 */}
      <Link
        href="/contact"
        className="nav-link"
        activeClassName="active"
      >
        联系
      </Link>
    </>
  )
}

链接到动态段

如果需要链接到动态路由,可以使用对象形式:

import Link from 'next/link'

export default function ProductList() {
  return (
    <Link
      href={{
        pathname: '/products/[id]',
        query: { id: '123' },
      }}
    >
      产品详情
    </Link>
  )
}

编程式导航

除了使用 Link 组件,有时候我们需要在代码中控制导航,比如点击按钮后跳转、表单提交后跳转等。这时候就需要用到编程式导航。

useRouter Hook

useRouter Hook 提供了编程式导航的方法。需要注意的是,这个 Hook 只能在客户端组件中使用,记得在文件顶部加上 'use client'

'use client'

import { useRouter } from 'next/navigation'

export default function NavigationButtons() {
  const router = useRouter()

  return (
    <div>
      <button onClick={() => router.push('/dashboard')}>
        前往仪表盘
      </button>

      <button onClick={() => router.replace('/login')}>
        替换到登录页
      </button>

      <button onClick={() => router.back()}>
        返回
      </button>

      <button onClick={() => router.forward()}>
        前进
      </button>

      <button onClick={() => router.refresh()}>
        刷新
      </button>
    </div>
  )
}

带 URL 参数的导航

有时候我们需要在导航时携带查询参数,比如搜索关键词、分页信息等:

'use client'

import { useRouter, useSearchParams } from 'next/navigation'

export default function SearchNavigation() {
  const router = useRouter()
  const searchParams = useSearchParams()

  function navigateWithParams() {
    const params = new URLSearchParams(searchParams)
    params.set('page', '2')
    params.set('sort', 'asc')

    router.push(`/search?${params.toString()}`)
  }

  return <button onClick={navigateWithParams}>搜索</button>
}

导航后回调

很多初学者会问,怎么在导航后执行一些操作?需要注意的是,router.push 是异步的,但它不返回 Promise,所以不能直接用 await。如果需要在导航后执行操作,可以使用 useEffect 监听路径变化。

'use client'

import { useRouter } from 'next/navigation'

export default function NavigationWithCallback() {
  const router = useRouter()

  function navigateWithCallback() {
    router.push('/dashboard')
    // 注意:push 是异步的,但没有 Promise
    // 如需导航后执行操作,使用 useEffect 监听
  }

  return <button onClick={navigateWithCallback}>导航</button>
}

路由 Hooks

Next.js 提供了一些实用的路由 Hooks,能帮助我们获取路由信息。

usePathname

获取当前路径名,这个在实现高亮菜单、面包屑导航等场景中很有用:

'use client'

import { usePathname } from 'next/navigation'
import Link from 'next/link'

export default function ActiveLink({
  href,
  children,
}: {
  href: string
  children: React.ReactNode
}) {
  const pathname = usePathname()
  const isActive = pathname === href

  return (
    <Link
      href={href}
      className={isActive ? 'text-blue-500 font-bold' : 'text-gray-700'}
    >
      {children}
    </Link>
  )
}

useSearchParams

获取当前 URL 的查询参数,常用于读取搜索关键词、分页信息等:

'use client'

import { useSearchParams } from 'next/navigation'

export default function ProductList() {
  const searchParams = useSearchParams()
  const page = searchParams.get('page') || '1'
  const sort = searchParams.get('sort') || 'desc'

  return (
    <div>
      <p>当前页: {page}</p>
      <p>排序: {sort}</p>
    </div>
  )
}

useParams

获取动态路由参数,比如文章的 slug、产品的 ID 等:

'use client'

import { useParams } from 'next/navigation'

export default function PostPage() {
  const params = useParams()
  const slug = params.slug as string

  return (
    <div>
      <h1>文章: {slug}</h1>
    </div>
  )
}

导航模式

下面分享几个在实际项目中常用的导航模式。

1. 面包屑导航

面包屑导航能让用户清楚自己当前所处的位置:

'use client'

import Link from 'next/link'
import { usePathname } from 'next/navigation'

export default function Breadcrumbs() {
  const pathname = usePathname()
  const segments = pathname.split('/').filter(Boolean)

  return (
    <nav className="flex gap-2">
      <Link href="/">首页</Link>
      {segments.map((segment, index) => {
        const href = '/' + segments.slice(0, index + 1).join('/')
        return (
          <span key={href}>
            <span className="mx-2">/</span>
            <Link href={href}>{segment}</Link>
          </span>
        )
      })}
    </nav>
  )
}

2. 标签页导航

使用查询参数实现标签页切换是一个常见且实用的模式:

'use client'

import { useSearchParams } from 'next/navigation'
import Link from 'next/link'

export default function Tabs() {
  const searchParams = useSearchParams()
  const tab = searchParams.get('tab') || 'overview'

  const tabs = [
    { id: 'overview', label: '概览' },
    { id: 'settings', label: '设置' },
    { id: 'analytics', label: '分析' },
  ]

  return (
    <div>
      <div className="flex gap-4 border-b">
        {tabs.map((t) => (
          <Link
            key={t.id}
            href={`?tab=${t.id}`}
            className={`pb-2 px-4 ${
              tab === t.id
                ? 'border-b-2 border-blue-500 text-blue-500'
                : 'text-gray-600'
            }`}
          >
            {t.label}
          </Link>
        ))}
      </div>
      {/* 标签页内容 */}
    </div>
  )
}

3. 分页导航

分页是列表页面必备的功能:

'use client'

import Link from 'next/link'
import { useSearchParams } from 'next/navigation'

export default function Pagination({ totalPages }: { totalPages: number }) {
  const searchParams = useSearchParams()
  const currentPage = Number(searchParams.get('page')) || 1

  return (
    <div className="flex gap-2">
      {currentPage > 1 && (
        <Link href={`?page=${currentPage - 1}`}>上一页</Link>
      )}

      {Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
        <Link
          key={page}
          href={`?page=${page}`}
          className={
            currentPage === page ? 'font-bold text-blue-500' : ''
          }
        >
          {page}
        </Link>
      ))}

      {currentPage < totalPages && (
        <Link href={`?page=${currentPage + 1}`}>下一页</Link>
      )}
    </div>
  )
}

4. 模态框导航(拦截路由)

拦截路由是 Next.js 的一个高级特性,可以实现模态框效果:

'use client'

import { useRouter } from 'next/navigation'
import { useState } from 'react'

export default function ModalExample() {
  const router = useRouter()
  const [isOpen, setIsOpen] = useState(false)

  function openModal(photoId: string) {
    // 拦截导航,显示模态框
    setIsOpen(true)
    router.push(`/photos/${photoId}`)
  }

  function closeModal() {
    setIsOpen(false)
    router.back()
  }

  return (
    <>
      <button onClick={() => openModal('123')}>查看照片</button>
      {isOpen && (
        <div className="modal">
          <button onClick={closeModal}>关闭</button>
          <div>照片内容</div>
        </div>
      )}
    </>
  )
}

客户端导航详解

Next.js 的客户端导航是一个非常巧妙的设计,它的工作原理如下:

  1. 拦截 <a> 标签点击事件
  2. 使用 fetch 请求新页面的 JSON 数据
  3. 客户端渲染新页面
  4. 更新 URL 和浏览器历史记录

这样做的好处是:

  • 更快的页面切换 - 不需要完全重新加载页面
  • 保持状态 - 组件状态得以保留
  • 更好的用户体验 - 无缝的导航体验

如果需要禁用客户端导航,可以这样:

import Link from 'next/link'

// 禁用 prefetch
<Link href="/about" prefetch={false}>
  关于
</Link>

// 完全禁用客户端导航
<Link href="/external" scroll={false}>
  外部链接
</Link>

导航过渡效果

页面切换动画

使用 React 的 useTransition 可以实现页面切换时的加载状态:

'use client'

import { useRouter } from 'next/navigation'
import { useState, useTransition } from 'react'

export default function NavigationWithTransition() {
  const router = useRouter()
  const [isPending, startTransition] = useTransition()

  function handleClick(path: string) {
    startTransition(() => {
      router.push(path)
    })
  }

  return (
    <nav>
      <button
        onClick={() => handleClick('/dashboard')}
        disabled={isPending}
      >
        {isPending ? '加载中...' : '仪表盘'}
      </button>
    </nav>
  )
}

使用 Motion 动画

如果你使用 framer-motion 这样的动画库,可以给导航添加流畅的过渡效果:

'use client'

import { motion } from 'framer-motion'
import Link from 'next/link'
import { usePathname } from 'next/navigation'

export default function AnimatedNav() {
  const pathname = usePathname()

  const links = [
    { href: '/', label: '首页' },
    { href: '/about', label: '关于' },
    { href: '/contact', label: '联系' },
  ]

  return (
    <nav>
      {links.map((link) => (
        <Link key={link.href} href={link.href}>
          <motion.span
            className={pathname === link.href ? 'active' : ''}
            whileHover={{ scale: 1.1 }}
            whileTap={{ scale: 0.95 }}
          >
            {link.label}
          </motion.span>
        </Link>
      ))}
    </nav>
  )
}

导航守卫

重定向

很多时候我们需要在用户访问某个页面前先检查一些条件,比如是否登录、是否有权限等。这时候可以使用中间件来实现重定向:

// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  // 检查认证
  const token = request.cookies.get('token')?.value

  if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  return NextResponse.next()
}

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

客户端守卫

在客户端也可以实现路由守卫:

'use client'

import { useRouter } from 'next/navigation'
import { useEffect } from 'react'

export default function ProtectedRoute() {
  const router = useRouter()

  useEffect(() => {
    const isAuthenticated = checkAuth()

    if (!isAuthenticated) {
      router.push('/login')
    }
  }, [router])

  return <div>受保护的内容</div>
}

实用建议

这里分享几个在日常开发中特别实用的技巧。

优先使用 Link 组件

实际开发中,声明式导航通常比命令式导航更清晰,所以我建议优先使用 Link 组件:

// 推荐这样做
<Link href="/about">关于</Link>

// 传统的写法也可以,但 Link 更好
<a href="/about">关于</a>

为动态链接添加密钥

这个小细节很容易忽略——渲染动态链接列表时,记得给每个 Link 添加 key:

{posts.map((post) => (
  <Link key={post.id} href={`/posts/${post.slug}`}>
    {post.title}
  </Link>
))}

处理加载状态

这个技巧特别有用——给用户明确的加载反馈能大大提升用户体验:

'use client'

import { useRouter, usePathname } from 'next/navigation'
import { useState, useEffect } from 'react'

export default function LoadingIndicator() {
  const router = useRouter()
  const pathname = usePathname()
  const [isLoading, setIsLoading] = useState(false)

  useEffect(() => {
    const handleStart = () => setIsLoading(true)
    const handleComplete = () => setIsLoading(false)

    router.events?.on('routeChangeStart', handleStart)
    router.events?.on('routeChangeComplete', handleComplete)

    return () => {
      router.events?.off('routeChangeStart', handleStart)
      router.events?.off('routeChangeComplete', handleComplete)
    }
  }, [router])

  return isLoading ? <div>加载中...</div> : null
}

总结

本节我们详细学习了 Next.js 的链接和导航系统,包括 Link 组件、编程式导航、路由 Hooks、各种导航模式等。掌握好这些知识,你就能构建出导航流畅、用户体验优秀的应用了。

如果你对本节内容有任何疑问,欢迎在评论区提出来,我们一起学习讨论。

原文地址:https://blog.uuhb.cn/archives/Next-js-04.html

Next.js第二课 - 项目结构详解 - 优栈

2026年4月1日 19:00

上节我们搭建好了 Next.js 开发环境,本节就来详细了解一下 Next.js 的项目结构。很多初学者刚打开项目时会看到一堆文件和文件夹,不知道每个都是干什么的。别担心,本节会带你理清这些目录和文件的用途,让你对项目结构有一个清晰的认识。

项目结构概览

my-nextjs-app/
├── app/                          # App Router(主要工作目录)
│   ├── (auth)/                   # 路由组(不影响 URL)
│   │   ├── login/
│   │   │   └── page.tsx
│   │   └── register/
│   │       └── page.tsx
│   ├── api/                      # API 路由
│   │   └── users/
│   │       └── route.ts
│   ├── blog/                     # 应用路由
│   │   ├── [slug]/              # 动态路由
│   │   │   └── page.tsx
│   │   └── page.tsx
│   ├── layout.tsx               # 根布局
│   ├── page.tsx                 # 首页
│   ├── loading.tsx              # 加载状态
│   ├── error.tsx                # 错误处理
│   ├── not-found.tsx            # 404 页面
│   └── globals.css              # 全局样式
├── components/                   # 共享组件
│   ├── ui/                      # UI 基础组件
│   │   ├── Button.tsx
│   │   ├── Input.tsx
│   │   └── Card.tsx
│   └── layout/                  # 布局组件
│       ├── Header.tsx
│       └── Footer.tsx
├── lib/                         # 工具函数
│   ├── utils.ts
│   ├── api.ts
│   └── constants.ts
├── hooks/                       # 自定义 Hooks
│   ├── useAuth.ts
│   └── useData.ts
├── types/                       # TypeScript 类型
│   └── index.ts
├── public/                      # 静态资源
│   ├── images/
│   ├── fonts/
│   └── favicon.ico
├── styles/                      # 样式文件(可选)
│   └── globals.css
├── .env.local                   # 环境变量
├── .eslintrc.json              # ESLint 配置
├── .gitignore                  # Git 忽略文件
├── next.config.js              # Next.js 配置
├── package.json                # 项目配置
├── tsconfig.json               # TypeScript 配置
└── README.md                   # 项目说明

image.png

核心目录详解

1. app/ 目录 - App Router

app/ 目录是 Next.js 13+ 推荐的新路由系统,基于 React Server Components 构建。这是你工作中最常打交道的目录,绝大部分页面和路由都会放在这里。

特殊文件

文件 用途 必需
layout.tsx 定义布局和 UI 可选
page.tsx 定义路由的独特 UI 必需(可访问路由)
loading.tsx 加载时的 UI 替换 可选
error.tsx 错误边界 UI 可选
not-found.tsx 404 页面 可选
route.ts API 端点 API 路由必需

示例结构

app/
├── (marketing)/              # 路由组
│   ├── about/
│   │   └── page.tsx         # /about
│   ├── layout.tsx           # 营销页面共享布局
│   └── page.tsx             # /
├── (shop)/                   # 另一个路由组
│   ├── account/
│   │   └── page.tsx         # /account
│   └── layout.tsx           # 商店页面共享布局
├── products/
│   ├── [id]/                # 动态段
│   │   └── page.tsx         # /products/123
│   └── page.tsx             # /products
├── api/
│   └── users/
│       └── route.ts         # /api/users (API)
├── layout.tsx               # 根布局(所有页面共享)
└── page.tsx                 # 首页

2. components/ 目录

这里存放可复用的 React 组件。当你发现一段 UI 代码在多个页面重复出现时,就可以把它抽取成一个组件放到这里。随着项目变大,良好的组件组织会让代码更容易维护。

components/
├── ui/                       # 基础 UI 组件
│   ├── Button.tsx
│   ├── Input.tsx
│   ├── Modal.tsx
│   └── Table.tsx
├── layout/                   # 布局组件
│   ├── Header.tsx
│   ├── Footer.tsx
│   ├── Sidebar.tsx
│   └── Navigation.tsx
├── features/                 # 功能组件
│   ├── UserProfile.tsx
│   ├── ProductCard.tsx
│   └── CommentList.tsx
└── forms/                    # 表单组件
    ├── LoginForm.tsx
    └── ContactForm.tsx

组件示例

// components/ui/Button.tsx
interface ButtonProps {
  children: React.ReactNode
  onClick?: () => void
  variant?: 'primary' | 'secondary'
}

export function Button({ children, onClick, variant = 'primary' }: ButtonProps) {
  return (
    <button
      onClick={onClick}
      className={`px-4 py-2 rounded ${
        variant === 'primary'
          ? 'bg-blue-500 text-white'
          : 'bg-gray-200 text-gray-800'
      }`}
    >
      {children}
    </button>
  )
}

3. lib/ 目录

这里存放工具函数、API 客户端、常量等辅助代码。把不属于任何特定业务逻辑的通用代码放在这里是个好习惯。

lib/
├── utils/                    # 工具函数
│   ├── format.ts            # 格式化函数
│   ├── validation.ts        # 验证函数
│   └── helpers.ts           # 辅助函数
├── api/                      # API 客户端
│   ├── client.ts
│   ├── users.ts
│   └── products.ts
├── db/                       # 数据库相关
│   ├── connect.ts
│   └── queries.ts
└── constants.ts              # 常量定义

工具函数示例

// lib/utils/format.ts

export function formatDate(date: Date): string {
  return new Intl.DateTimeFormat('zh-CN', {
    year: 'numeric',
    month: 'long',
    day: 'numeric',
  }).format(date)
}

export function formatCurrency(amount: number): string {
  return new Intl.NumberFormat('zh-CN', {
    style: 'currency',
    currency: 'CNY',
  }).format(amount)
}

4. hooks/ 目录

存放自定义 React Hooks。如果你有一些状态逻辑需要在多个组件中复用,就可以封装成自定义 Hook 放在这里。

hooks/
├── useAuth.ts                # 认证相关
├── useData.ts                # 数据获取
├── useForm.ts                # 表单处理
└── useLocalStorage.ts        # 本地存储

Hook 示例

// hooks/useAuth.ts
'use client'

import { useState, useEffect } from 'react'

export function useAuth() {
  const [isAuthenticated, setIsAuthenticated] = useState(false)
  const [user, setUser] = useState(null)

  useEffect(() => {
    // 检查认证状态
    const token = localStorage.getItem('token')
    setIsAuthenticated(!!token)
  }, [])

  return { isAuthenticated, user }
}

5. public/ 目录

这里存放静态资源,比如图片、字体、favicon 等。放在 public 目录下的文件可以直接通过 URL 访问,不需要 import。

public/
├── images/
│   ├── logo.png
│   └── banner.jpg
├── fonts/
│   └── custom-font.woff2
├── favicon.ico
└── robots.txt

使用方式

// 在组件中引用
<Image src="/images/logo.png" alt="Logo" width={200} height={100} />

6. 根配置文件

next.config.js

/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
  swcMinify: true,

  // 图片优化配置
  images: {
    domains: ['example.com'],
    formats: ['image/avif', 'image/webp'],
  },

  // 环境变量
  env: {
    CUSTOM_KEY: process.env.CUSTOM_KEY,
  },

  // 重定向
  async redirects() {
    return [
      {
        source: '/old-path',
        destination: '/new-path',
        permanent: true,
      },
    ]
  },
}

module.exports = nextConfig

tsconfig.json

{
  "compilerOptions": {
    "target": "ES2017",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true,
    "plugins": [
      {
        "name": "next"
      }
    ],
    "paths": {
      "@/*": ["./*"]
    }
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

路由组织最佳实践

1. 使用路由组

路由组 (group-name) 是 Next.js 的一个很有用的特性,它不会影响 URL 路径,但可以帮助你更好地组织代码和共享布局。比如你想把某些页面放在一起管理,但又不想改变 URL 结构,就可以用路由组。

app/
├── (marketing)/              # /about, /contact
│   ├── about/
│   │   └── page.tsx
│   ├── contact/
│   │   └── page.tsx
│   └── layout.tsx           # 营销页面共享布局
├── (dashboard)/              # /dashboard, /settings
│   ├── dashboard/
│   │   └── page.tsx
│   ├── settings/
│   │   └── page.tsx
│   └── layout.tsx           # 需要认证的布局
└── page.tsx                 # 首页

2. 动态路由

动态路由在实际开发中非常常见,比如博客文章页、用户详情页等。使用方括号 [param] 就可以创建动态路由段,Next.js 会自动匹配并解析参数。

app/
├── blog/
│   ├── [slug]/              # /blog/hello-world
│   │   └── page.tsx
│   └── page.tsx             # /blog
├── products/
│   ├── [category]/          # /products/electronics
│   │   └── page.tsx
│   └── page.tsx             # /products
└── users/
    └── [id]/                # /users/123
        ├── [action]/        # /users/123/edit
        │   └── page.tsx
        └── page.tsx         # /users/123

3. 并行和拦截路由

并行路由和拦截路由是 Next.js 的高级特性,可以实现一些复杂的交互效果,比如模态框、并行加载多个页面等。这些特性在实际项目中非常有用,但理解起来可能需要一点时间。

app/
├── @dashboard/               # 并行路由槽
│   └── page.tsx
├── (.)modal/                 # 拦截路由
│   └── photo/[id]/page.tsx
├── dashboard/
│   └── page.tsx
└── layout.tsx

文件命名约定

路由相关

模式 说明 示例 URL
folder/page.tsx 标准路由 /folder
folder/[slug]/page.tsx 动态路由 /folder/value
folder/[[...slug]]/page.tsx 捕获所有路由 /folder/a/b/c
(group)/page.tsx 路由组 /page
folder/(.)modal/... 拦截路由 -

特殊文件

文件 说明
_filename.tsx 私有文件,不创建路由
filename.server.tsx 仅在服务器运行
filename.client.tsx 仅在客户端运行

代码组织建议

1. 按功能组织

按功能组织是一种常见的项目结构方式,把相关的功能放在一起。这种方式适合中小型项目,代码结构清晰易懂。

app/
├── (auth)/
│   ├── login/
│   ├── register/
│   └── forgot-password/
├── (dashboard)/
│   ├── overview/
│   ├── analytics/
│   └── settings/
└── (public)/
    ├── about/
    ├── contact/
    └── pricing/

2. 按层级组织

按层级组织适合大型项目,比如有 API 版本管理、多级管理后台等场景。这种方式可以让结构更有层次感。

app/
├── api/
│   ├── v1/
│   │   ├── users/
│   │   └── posts/
│   └── v2/
│       └── users/
└── admin/
    └── users/
        ├── [id]/
        └── new/

3. 组件分层

组件分层是一种借鉴原子设计的组织方式,把组件按照复杂度分成原子、分子、组织、模板等层级。这种方式适合 UI 组件库或者设计系统比较完善的项目。

components/
├── atoms/                    # 最小单元
│   ├── Button.tsx
│   └── Input.tsx
├── molecules/                # 组合原子
│   ├── SearchBar.tsx
│   └── FormField.tsx
├── organisms/                # 复杂组件
│   ├── Header.tsx
│   └── ProductCard.tsx
└── templates/                # 页面模板
    └── BlogLayout.tsx

环境变量

环境变量用来存储一些敏感信息或者配置,比如数据库连接字符串、API 密钥等。创建 .env.local 文件来存放这些信息,记得把这个文件加到 .gitignore 里,不要提交到代码仓库。

# 数据库
DATABASE_URL=postgresql://...

# API 密钥
API_KEY=your_api_key
API_SECRET=your_api_secret

# 应用配置
NEXT_PUBLIC_APP_URL=http://localhost:3000

访问方式:

// 服务器端
const dbUrl = process.env.DATABASE_URL

// 客户端(必须以 NEXT_PUBLIC_ 开头)
const appUrl = process.env.NEXT_PUBLIC_APP_URL

总结

本节我们详细了解了 Next.js 的项目结构,包括各个目录的用途、路由组织方式、以及一些最佳实践。掌握项目结构是学好 Next.js 的基础,建议你多花点时间理解这些内容。

如果你对本节内容有任何疑问,欢迎在评论区提出来,我们一起学习讨论。

原文链接:https://blog.uuhb.cn/archives/Next-js-02.html

❌
❌