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

0 阅读5分钟

上节我们学习了布局和页面的使用,本节来聊聊链接和导航。在 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