链接与导航
深入理解 Next.js 的导航系统,掌握 Link 组件、useRouter 钩子、预取优化和客户端过渡的最佳实践。
概述
Next.js 提供了强大的导航系统,通过内置的预取、流式传输和客户端过渡确保导航快速且响应迅速。
导航工作原理
服务器渲染
Next.js 中的布局和页面默认是 React 服务器组件。在初始和后续导航时,服务器组件负载在服务器上生成后发送到客户端。
两种服务器渲染类型:
- 静态渲染(预渲染):在构建时或重新验证期间发生,结果被缓存
- 动态渲染:在请求时响应客户端请求发生
预取
预取是在用户导航到新路由之前在后台加载路由的过程。这使应用中的路由导航感觉即时,因为当用户点击链接时,渲染下一个路由的数据已经在客户端可用。
Next.js 在使用 <Link> 组件时自动预取进入用户视口的路由。
流式传输
流式传输允许在服务器组件准备就绪时逐步渲染它们。工作被分成块,并在准备就绪时流式传输到客户端。这使用户可以在整个内容完成渲染之前看到页面的部分内容。
客户端过渡
当导航到新路由时,Next.js 执行客户端过渡而不是完整的页面重新加载。这意味着:
- 浏览器不会重新加载页面
- 只有更改的路由段会重新渲染
- 共享布局保持挂载和交互
- React 状态被保留
Link 组件
<Link> 是扩展 HTML <a> 元素的 React 组件,提供预取和客户端导航。这是 Next.js 中路由间导航的主要方式。
基础用法
import Link from 'next/link'
export default function Page() {
return <Link href="/dashboard">仪表板</Link>
}动态路由
import Link from 'next/link'
export default function PostList({ posts }) {
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
<Link href={`/blog/${post.slug}`}>{post.title}</Link>
</li>
))}
</ul>
)
}对象形式的 href
import Link from 'next/link'
export default function Page() {
return (
<Link
href={{
pathname: '/about',
query: { name: 'test' },
}}
>
关于
</Link>
)
}Link Props
href(必需)
要导航到的路径或 URL。
// 字符串形式
<Link href="/dashboard">仪表板</Link>
// 对象形式
<Link href={{ pathname: '/blog', query: { id: '1' } }}>博客</Link>replace
替换浏览器历史记录栈中的当前条目,而不是添加新条目。默认为 false。
<Link href="/dashboard" replace>
仪表板
</Link>scroll
导航后是否滚动到页面顶部。默认为 true。
<Link href="/dashboard" scroll={false}>
仪表板
</Link>prefetch
控制链接的预取行为。
// 自动预取(默认)
<Link href="/dashboard">仪表板</Link>
// 禁用预取
<Link href="/dashboard" prefetch={false}>
仪表板
</Link>
// 显式启用预取
<Link href="/dashboard" prefetch={true}>
仪表板</Link>预取行为:
- 静态路由:预取完整的路由
- 动态路由:预取到第一个
loading.js文件的部分路由 prefetch={false}:仅在悬停时预取prefetch={true}或null:预取完整路由
onNavigate
导航开始时调用的回调函数。
'use client'
import Link from 'next/link'
export default function Page() {
return (
<Link
href="/dashboard"
onNavigate={(e) => {
console.log('导航到:', e.href)
}}
>
仪表板
</Link>
)
}传递其他属性
可以将 <a> 标签属性传递给 <Link>。
<Link href="/about" className="text-blue-500" target="_blank">
关于
</Link>活动链接
使用 usePathname() 确定链接是否活动。
'use client'
import { usePathname } from 'next/navigation'
import Link from 'next/link'
export function NavLinks() {
const pathname = usePathname()
return (
<nav>
<Link
className={pathname === '/' ? 'active' : ''}
href="/"
>
首页
</Link>
<Link
className={pathname === '/about' ? 'active' : ''}
href="/about"
>
关于
</Link>
</nav>
)
}滚动到特定 ID
<Link href="/dashboard#settings">设置</Link>禁用滚动恢复
<Link href="/dashboard" scroll={false}>
仪表板
</Link>useRouter 钩子
useRouter 钩子允许在客户端组件中以编程方式更改路由。
建议:除非有特定需求使用 useRouter,否则使用 <Link> 组件进行导航。
基础用法
'use client'
import { useRouter } from 'next/navigation'
export default function Page() {
const router = useRouter()
return (
<button onClick={() => router.push('/dashboard')}>
前往仪表板
</button>
)
}router.push()
执行客户端导航到提供的路由。在浏览器历史记录栈中添加新条目。
'use client'
import { useRouter } from 'next/navigation'
export default function Page() {
const router = useRouter()
return (
<button onClick={() => router.push('/dashboard')}>
仪表板
</button>
)
}禁用滚动到顶部:
router.push('/dashboard', { scroll: false })router.replace()
执行客户端导航到提供的路由,但不在浏览器历史记录栈中添加新条目。
'use client'
import { useRouter } from 'next/navigation'
export default function Page() {
const router = useRouter()
return (
<button onClick={() => router.replace('/dashboard')}>
仪表板
</button>
)
}router.refresh()
刷新当前路由。向服务器发出新请求,重新获取数据请求,并重新渲染服务器组件。
'use client'
import { useRouter } from 'next/navigation'
export default function Page() {
const router = useRouter()
return (
<button onClick={() => router.refresh()}>
刷新
</button>
)
}客户端会合并更新的 React 服务器组件负载,而不会丢失未受影响的客户端 React 状态(如 useState)或浏览器状态(如滚动位置)。
router.prefetch()
预取提供的路由以实现更快的客户端过渡。
'use client'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'
export default function Page() {
const router = useRouter()
useEffect(() => {
router.prefetch('/dashboard')
}, [router])
return <button onClick={() => router.push('/dashboard')}>仪表板</button>
}带失效回调:
router.prefetch('/dashboard', {
onInvalidate: () => {
console.log('预取的数据已失效')
},
})router.back()
导航回浏览器历史记录栈中的上一个路由。
'use client'
import { useRouter } from 'next/navigation'
export default function Page() {
const router = useRouter()
return (
<button onClick={() => router.back()}>
返回
</button>
)
}router.forward()
导航到浏览器历史记录栈中的下一个页面。
'use client'
import { useRouter } from 'next/navigation'
export default function Page() {
const router = useRouter()
return (
<button onClick={() => router.forward()}>
前进
</button>
)
}预取详解
预取使应用中不同路由之间的导航感觉即时。Next.js 根据应用代码中使用的链接智能地默认预取。
自动预取
Next.js 在使用 <Link> 组件时自动预取进入用户视口的路由。
import Link from 'next/link'
export default function Page() {
return (
<div>
{/* 这些链接会在进入视口时自动预取 */}
<Link href="/about">关于</Link>
<Link href="/blog">博客</Link>
</div>
)
}静态 vs 动态路由预取
| 静态页面 | 动态页面 | |
|---|---|---|
| 预取内容 | 完整路由 | 到第一个 loading.js 的部分路由 |
| 缓存时间 | 5 分钟 | 30 秒 |
手动预取
使用 router.prefetch() 手动预取路由。
'use client'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'
export default function Page() {
const router = useRouter()
useEffect(() => {
// 组件挂载时预取
router.prefetch('/dashboard')
}, [router])
return <button onClick={() => router.push('/dashboard')}>仪表板</button>
}悬停触发预取
仅在用户悬停时预取,减少资源使用。
'use client'
import Link from 'next/link'
import { useState } from 'react'
export function HoverPrefetchLink({
href,
children,
}: {
href: string
children: React.ReactNode
}) {
const [active, setActive] = useState(false)
return (
<Link
href={href}
prefetch={active ? null : false}
onMouseEnter={() => setActive(true)}
>
{children}
</Link>
)
}禁用预取
对于大量链接列表,可以禁用预取以避免不必要的资源使用。
<Link href={`/blog/${post.id}`} prefetch={false}>
{post.title}
</Link>重定向
redirect 函数
redirect 函数允许将用户重定向到另一个 URL。可以在服务器组件、路由处理程序和服务器操作中调用。
import { redirect } from 'next/navigation'
export default async function Page() {
const user = await getUser()
if (!user) {
redirect('/login')
}
return <div>欢迎,{user.name}</div>
}在服务器操作中:
'use server'
import { redirect } from 'next/navigation'
export async function createPost(formData: FormData) {
const post = await savePost(formData)
redirect(`/blog/${post.slug}`)
}redirect默认返回 307(临时重定向)状态码- 在服务器操作中返回 303(查看其他)
- 可以接受绝对 URL 并重定向到外部链接
permanentRedirect 函数
permanentRedirect 函数允许永久重定向用户到另一个 URL。
import { permanentRedirect } from 'next/navigation'
export default async function Page() {
const user = await getUser()
if (user.username !== user.canonicalUsername) {
permanentRedirect(`/users/${user.canonicalUsername}`)
}
return <div>用户资料</div>
}permanentRedirect默认返回 308(永久重定向)状态码- 常用于更改实体的规范 URL 后
- 可以接受绝对 URL 并重定向到外部链接
使用原生 History API
window.history.pushState
更新浏览器历史记录栈而不重新加载页面。
'use client'
import { useSearchParams } from 'next/navigation'
export function SortProducts() {
const searchParams = useSearchParams()
function updateSorting(sortOrder: string) {
const params = new URLSearchParams(searchParams.toString())
params.set('sort', sortOrder)
window.history.pushState(null, '', `?${params.toString()}`)
}
return (
<>
<button onClick={() => updateSorting('asc')}>升序</button>
<button onClick={() => updateSorting('desc')}>降序</button>
</>
)
}window.history.replaceState
替换浏览器历史记录栈中的当前条目。用户无法导航回上一个状态。
'use client'
import { usePathname } from 'next/navigation'
export function LocaleSwitcher() {
const pathname = usePathname()
function switchLocale(locale: string) {
const newPath = `/${locale}${pathname}`
window.history.replaceState(null, '', newPath)
}
return (
<>
<button onClick={() => switchLocale('en')}>English</button>
<button onClick={() => switchLocale('zh')}>中文</button>
</>
)
}导航优化
动态路由优化
对于没有 loading.js 的动态路由,可以添加加载状态以改善用户体验。
// app/blog/[slug]/loading.tsx
export default function Loading() {
return <div>加载中...</div>
}慢网络优化
使用 loading.js 和流式传输为慢网络提供即时反馈。
// app/dashboard/loading.tsx
export default function Loading() {
return (
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-1/4 mb-4" />
<div className="h-64 bg-gray-200 rounded" />
</div>
)
}实际应用示例
导航菜单
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
const links = [
{ href: '/', label: '首页' },
{ href: '/about', label: '关于' },
{ href: '/blog', label: '博客' },
{ href: '/contact', label: '联系' },
]
export function Navigation() {
const pathname = usePathname()
return (
<nav className="flex gap-4">
{links.map((link) => (
<Link
key={link.href}
href={link.href}
className={
pathname === link.href
? 'text-blue-600 font-bold'
: 'text-gray-600 hover:text-gray-900'
}
>
{link.label}
</Link>
))}
</nav>
)
}面包屑导航
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
export 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('/')}`
const isLast = index === segments.length - 1
return (
<span key={href} className="flex gap-2">
<span>/</span>
{isLast ? (
<span className="font-bold">{segment}</span>
) : (
<Link href={href}>{segment}</Link>
)}
</span>
)
})}
</nav>
)
}分页
'use client'
import Link from 'next/link'
import { useSearchParams } from 'next/navigation'
export 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={page === currentPage ? 'font-bold' : ''}
>
{page}
</Link>
))}
{currentPage < totalPages && (
<Link href={`?page=${currentPage + 1}`}>下一页</Link>
)}
</div>
)
}搜索功能
'use client'
import { useRouter, useSearchParams } from 'next/navigation'
import { useState } from 'react'
export function SearchBar() {
const router = useRouter()
const searchParams = useSearchParams()
const [query, setQuery] = useState(searchParams.get('q') || '')
function handleSearch(e: React.FormEvent) {
e.preventDefault()
const params = new URLSearchParams(searchParams.toString())
if (query) {
params.set('q', query)
} else {
params.delete('q')
}
router.push(`/search?${params.toString()}`)
}
return (
<form onSubmit={handleSearch}>
<input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="搜索..."
/>
<button type="submit">搜索</button>
</form>
)
}模态框导航
'use client'
import { useRouter } from 'next/navigation'
import { useEffect } from 'react'
export function Modal({ children }: { children: React.ReactNode }) {
const router = useRouter()
useEffect(() => {
// 按 ESC 键关闭
function handleEscape(e: KeyboardEvent) {
if (e.key === 'Escape') {
router.back()
}
}
window.addEventListener('keydown', handleEscape)
return () => window.removeEventListener('keydown', handleEscape)
}, [router])
return (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center">
<div className="bg-white p-6 rounded-lg max-w-2xl w-full">
<button
onClick={() => router.back()}
className="float-right text-gray-500"
>
✕
</button>
{children}
</div>
</div>
)
}最佳实践
1. 优先使用 Link 组件
对于声明式导航,始终使用 <Link> 组件。
// 推荐
<Link href="/dashboard">仪表板</Link>
// 避免
<a href="/dashboard">仪表板</a>2. 仅在必要时使用 useRouter
仅在需要编程式导航时使用 useRouter。
// 适用场景:表单提交后导航
async function handleSubmit() {
await saveData()
router.push('/success')
}
// 不必要:可以使用 Link
<button onClick={() => router.push('/about')}>关于</button>3. 合理使用预取
对于大量链接,考虑禁用或延迟预取。
// 大量链接列表
{posts.map((post) => (
<Link href={`/blog/${post.id}`} prefetch={false}>
{post.title}
</Link>
))}
// 或使用悬停预取
<HoverPrefetchLink href={`/blog/${post.id}`}>
{post.title}
</HoverPrefetchLink>4. 添加加载状态
为动态路由添加 loading.js 以改善用户体验。
// app/blog/[slug]/loading.tsx
export default function Loading() {
return <Skeleton />
}5. 使用活动链接样式
为当前页面的链接添加视觉反馈。
const pathname = usePathname()
<Link
href="/about"
className={pathname === '/about' ? 'active' : ''}
>
关于
</Link>总结
Next.js 的导航系统提供了强大而优化的路由体验:
- Link 组件:主要的导航方式,提供自动预取和客户端过渡
- useRouter 钩子:用于编程式导航和高级用例
- 预取:自动或手动预取路由以实现即时导航
- 重定向:使用
redirect和permanentRedirect进行服务器端重定向 - 优化:通过流式传输、加载状态和智能预取优化导航体验
理解并正确使用这些功能,可以构建快速、响应迅速的 Next.js 应用。
上次更新于