上节我们学习了布局和页面的使用,本节来聊聊链接和导航。在 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 的客户端导航是一个非常巧妙的设计,它的工作原理如下:
- 拦截
<a>标签点击事件 - 使用
fetch请求新页面的 JSON 数据 - 客户端渲染新页面
- 更新 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、各种导航模式等。掌握好这些知识,你就能构建出导航流畅、用户体验优秀的应用了。
如果你对本节内容有任何疑问,欢迎在评论区提出来,我们一起学习讨论。