导航与路由过渡
在 Next.js 中,导航不仅仅是跳转页面——它涉及 Link 预加载、useRouter 编程式导航、usePathname/useSearchParams 状态同步、以及 View Transitions 动画。本章逐一讲清这些导航 API 的使用场景、底层行为和最佳实践。
1. Link 组件:声明式导航
1.1 基础使用
import Link from 'next/link'
export function Navigation() {
return (
<nav>
<Link href="/chat">对话</Link>
<Link href="/knowledge">知识库</Link>
<Link href="/settings">设置</Link>
</nav>
)
}<Link> 的核心行为:
- 渲染为
<a>标签(SEO 友好) - 客户端导航(不刷新页面,布局状态保持)
- 自动预加载(Link 进入视口时预加载目标页面数据)
1.2 动态链接
// 动态路由
<Link href={`/chat/${chatId}`}>查看对话</Link>
// 带查询参数
<Link href={{ pathname: '/chat', query: { tab: 'history' } }}>
历史记录
</Link>
// 带 Hash
<Link href="/docs#installation">安装章节</Link>1.3 Prefetch 预加载控制
// 默认:预加载静态部分(布局 + loading 组件)
<Link href="/settings">设置</Link>
// 禁用预加载(节省带宽,适用于大量链接的列表)
<Link href={`/chat/${id}`} prefetch={false}>
{title}
</Link>
// 完整预加载(包括动态数据,适用于高频访问链接)
<Link href="/chat" prefetch={true}>对话</Link>1.4 活跃链接样式
Next.js 没有内置 activeClassName,需要结合 usePathname 实现:
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { cn } from '@/lib/utils'
const navItems = [
{ href: '/chat', label: '对话', icon: MessageSquare },
{ href: '/knowledge', label: '知识库', icon: BookOpen },
{ href: '/settings', label: '设置', icon: Settings },
]
export function SidebarNav() {
const pathname = usePathname()
return (
<nav className="space-y-1">
{navItems.map((item) => (
<Link
key={item.href}
href={item.href}
className={cn(
'flex items-center gap-3 rounded-lg px-3 py-2 text-sm',
pathname.startsWith(item.href)
? 'bg-primary/10 text-primary font-medium'
: 'text-muted-foreground hover:bg-accent',
)}
>
<item.icon className="h-4 w-4" />
{item.label}
</Link>
))}
</nav>
)
}1.5 Link vs <a> 标签
| 场景 | 使用 | 原因 |
|---|---|---|
| 站内导航 | <Link> | 客户端导航 + 预加载 |
| 外部链接 | <a> | 需要完整页面加载 |
| 下载文件 | <a download> | Link 不支持 download |
| 邮件链接 | <a href="mailto:"> | 非 HTTP 协议 |
| 新标签页 | <Link target="_blank"> | Link 也支持 target |
2. useRouter:编程式导航
2.1 基础使用
'use client'
import { useRouter } from 'next/navigation'
export function ChatActions({ chatId }: { chatId: string }) {
const router = useRouter()
async function handleDelete() {
await deleteChat(chatId)
router.push('/chat') // 导航到对话列表
}
return <Button onClick={handleDelete}>删除对话</Button>
}2.2 所有导航方法
const router = useRouter()
// 导航到指定路径
router.push('/chat')
// 替换当前历史记录(不能后退)
router.replace('/chat')
// 后退
router.back()
// 前进
router.forward()
// 刷新当前页面的 Server Component 数据(不刷新整个页面)
router.refresh()
// 预加载指定路径
router.prefetch('/settings')2.3 router.refresh() 的妙用
router.refresh() 会重新执行当前路由的 Server Components(重新获取数据),但不刷新整个页面(Client Component 状态保持):
'use client'
import { useRouter } from 'next/navigation'
export function SendMessageButton({ chatId }: { chatId: string }) {
const router = useRouter()
async function handleSend(message: string) {
await sendMessage(chatId, message) // Server Action
router.refresh() // 重新获取消息列表(Server Component 数据刷新)
// Client Component 的输入框状态保持!
}
return <Button onClick={() => handleSend('Hello')}>发送</Button>
}2.4 useRouter vs Link
| 场景 | 推荐 |
|---|---|
| 静态导航链接 | <Link> — 语义化、SEO、预加载 |
| 表单提交后跳转 | router.push() — 编程式 |
| 删除后返回列表 | router.replace() — 不保留历史 |
| 数据更新后刷新 | router.refresh() — 只刷新 Server 数据 |
| 条件性导航 | router.push() — 根据逻辑决定去哪 |
3. 路由状态 Hooks
3.1 usePathname
获取当前 URL 的路径部分:
'use client'
import { usePathname } from 'next/navigation'
export function Breadcrumb() {
const pathname = usePathname()
// pathname = "/chat/abc/messages"
const segments = pathname.split('/').filter(Boolean)
// segments = ["chat", "abc", "messages"]
return (
<nav className="flex gap-2 text-sm text-muted-foreground">
{segments.map((segment, index) => (
<span key={index}>
{index > 0 && ' / '}
{segment}
</span>
))}
</nav>
)
}3.2 useSearchParams
获取和操作 URL 查询参数:
'use client'
import { useSearchParams, usePathname, useRouter } from 'next/navigation'
export function ChatFilter() {
const searchParams = useSearchParams()
const pathname = usePathname()
const router = useRouter()
const currentModel = searchParams.get('model') ?? 'all'
function setFilter(model: string) {
const params = new URLSearchParams(searchParams.toString())
if (model === 'all') {
params.delete('model')
} else {
params.set('model', model)
}
router.push(`${pathname}?${params.toString()}`)
}
return (
<div className="flex gap-2">
{['all', 'gpt-4o', 'claude-3.5'].map((model) => (
<Button
key={model}
variant={currentModel === model ? 'default' : 'outline'}
onClick={() => setFilter(model)}
>
{model}
</Button>
))}
</div>
)
}3.3 useParams
获取动态路由参数(客户端版的 params):
'use client'
import { useParams } from 'next/navigation'
export function ChatHeader() {
const params = useParams<{ id: string }>()
// params.id = "abc"
return <h1>对话 {params.id}</h1>
}3.4 Server Component 中获取路由信息
Server Component 不能使用 Hooks。路由信息通过 props 传入:
// Server Component — 通过 props 获取
export default async function ChatPage({
params,
searchParams,
}: {
params: Promise<{ id: string }>
searchParams: Promise<{ tab?: string }>
}) {
const { id } = await params
const { tab } = await searchParams
// ...
}也可以使用 next/headers:
import { headers } from 'next/headers'
export default async function Page() {
const headersList = await headers()
const referer = headersList.get('referer')
// ...
}4. 导航事件与加载状态
4.1 useTransition 实现导航加载状态
'use client'
import { useTransition } from 'react'
import { useRouter } from 'next/navigation'
export function NavigationButton({ href, children }: { href: string; children: React.ReactNode }) {
const router = useRouter()
const [isPending, startTransition] = useTransition()
return (
<button
onClick={() => startTransition(() => router.push(href))}
disabled={isPending}
className={isPending ? 'opacity-50' : ''}
>
{isPending ? '加载中...' : children}
</button>
)
}4.2 全局导航进度条
使用 next-nprogress-bar 或自定义实现:
// components/navigation-progress.tsx
'use client'
import { usePathname } from 'next/navigation'
import { useEffect, useState } from 'react'
import NProgress from 'nprogress'
export function NavigationProgress() {
const pathname = usePathname()
useEffect(() => {
NProgress.done()
}, [pathname])
return null
}更推荐使用 loading.tsx——它是 Next.js 内置的、基于 Suspense 的加载状态方案,不需要额外库。
5. View Transitions API
5.1 原生 View Transitions
Next.js 16 实验性支持浏览器原生 View Transitions API:
// next.config.ts
const nextConfig: NextConfig = {
experimental: {
viewTransition: true,
},
}/* globals.css */
::view-transition-old(root) {
animation: fade-out 0.2s ease-out;
}
::view-transition-new(root) {
animation: fade-in 0.2s ease-in;
}
@keyframes fade-out {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}5.2 基于 Framer Motion 的页面过渡
更成熟的方案——使用 Framer Motion:
// app/(dashboard)/template.tsx
'use client'
import { motion } from 'framer-motion'
export default function Template({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.2, ease: 'easeOut' }}
>
{children}
</motion.div>
)
}注意:template.tsx 在每次导航时重新挂载(不同于 layout.tsx),这正是页面过渡动画所需要的。
6. 常见导航模式
6.1 登录后跳回原页面
// middleware.ts — 保存 callbackUrl
if (!isLoggedIn) {
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('callbackUrl', pathname)
return NextResponse.redirect(loginUrl)
}
// app/(auth)/login/page.tsx — 登录成功后跳回
'use client'
export function LoginForm() {
const searchParams = useSearchParams()
const callbackUrl = searchParams.get('callbackUrl') ?? '/chat'
const router = useRouter()
async function onSubmit(data: LoginData) {
const result = await signIn('credentials', { ...data, redirect: false })
if (result?.ok) {
router.push(callbackUrl)
}
}
}6.2 表单提交后导航
'use client'
import { useRouter } from 'next/navigation'
import { createChat } from './actions'
export function NewChatForm() {
const router = useRouter()
async function handleSubmit(formData: FormData) {
const result = await createChat(formData)
if (result.data) {
router.push(`/chat/${result.data.id}`)
}
}
return (
<form action={handleSubmit}>
<input name="title" required />
<button type="submit">创建对话</button>
</form>
)
}6.3 URL 状态管理
将 UI 状态同步到 URL,实现可分享、可书签的状态:
'use client'
import { useRouter, usePathname, useSearchParams } from 'next/navigation'
import { useCallback } from 'react'
export function useQueryState(key: string, defaultValue: string = '') {
const router = useRouter()
const pathname = usePathname()
const searchParams = useSearchParams()
const value = searchParams.get(key) ?? defaultValue
const setValue = useCallback(
(newValue: string) => {
const params = new URLSearchParams(searchParams.toString())
if (newValue === defaultValue) {
params.delete(key)
} else {
params.set(key, newValue)
}
const queryString = params.toString()
router.push(queryString ? `${pathname}?${queryString}` : pathname)
},
[key, defaultValue, pathname, searchParams, router],
)
return [value, setValue] as const
}
// 使用
function ChatPage() {
const [tab, setTab] = useQueryState('tab', 'messages')
// URL: /chat/abc?tab=settings
// tab = "settings"
}本章小结
- Link 组件:声明式导航,自动预加载,配合
usePathname实现活跃链接样式 - useRouter:编程式导航,
push/replace/back/refresh四个核心方法 - 路由状态 Hooks:
usePathname(路径)、useSearchParams(查询参数)、useParams(动态参数) - router.refresh():只刷新 Server Component 数据,Client 状态保持——AI 对话场景的关键
- View Transitions:原生 API 实验性支持 + Framer Motion
template.tsx方案更成熟 - URL 状态管理:将 UI 状态同步到 URL,实现可分享、可书签的体验
下一章讲路由高级模式。