本系列文章将围绕Next.js技术栈,旨在为AI Agent开发者提供一套完整的客户端侧工程实践指南。
上一章《项目结构与文件系统约定》介绍了文件系统路由的基本概念,但完整的路由系统包含更多维度:页面间的导航机制、URL 参数的读取与处理、访问权限控制等。本章将系统性地讲解这些核心功能。
一、页面导航机制
Next.js 提供了两种导航方式,分别适用于不同的使用场景。
1. Link 组件:声明式导航
<Link> 组件是 Next.js 中实现页面跳转的首选方案,相较于原生 <a> 标签具有显著优势。
(1)工作原理对比
原生 <a> 标签:触发完整的页面刷新流程
- 浏览器向服务器重新请求 HTML
- 重新下载并执行 JavaScript
- 清空所有应用状态
- 用户体验存在明显的中断感
<Link> 组件:实现客户端导航(Client-Side Navigation)
- JavaScript 拦截默认跳转行为
- 仅获取新页面所需的数据
- 局部更新 DOM,保留应用状态
- 导航过程流畅,无白屏闪烁
typescript
import Link from 'next/link'
export default function Navigation() {
return (
<nav>
<Link href="/">首页</Link>
<Link href="/blog">博客</Link>
<Link href="/about">关于</Link>
</nav>
)
}
支持动态路径构建:
typescript
{posts.map(post => (
<Link key={post.id} href={`/blog/${post.slug}`}>
{post.title}
</Link>
))}
(2)Link 组件的高级属性
prefetch(预取)
默认情况下,当链接进入视口时,Next.js 会自动预取目标页面的数据。这种优化使得用户点击后几乎无感知延迟即可看到内容。对于访问频率低或数据量大的页面,可禁用预取以节省资源:
typescript
<Link href="/rarely-visited-page" prefetch={false}>
低频访问页面
</Link>
replace(替换历史记录)
默认导航会在浏览器历史记录中添加新条目。某些场景下需要替换当前记录而非追加,例如登录成功后跳转至首页,防止用户通过后退按钮返回登录页:
typescript
<Link href="/" replace>
登录后返回首页
</Link>
scroll(滚动行为控制)
导航完成后默认滚动至页面顶部。若需保持当前滚动位置(如分页筛选场景),可禁用此行为:
typescript
<Link href="/blog?tag=react" scroll={false}>
筛选 React 标签(保持滚动位置)
</Link>
2. useRouter Hook:编程式导航
<Link> 适用于用户主动点击的场景,而 useRouter Hook 则用于代码逻辑触发的导航,如表单提交成功后的跳转、身份验证失败后的重定向等。
typescript
'use client'
import { useRouter } from 'next/navigation'
export default function LoginForm() {
const router = useRouter()
const handleSubmit = async (formData: FormData) => {
const result = await login(formData)
if (result.success) {
// 使用 replace 避免用户后退时返回登录页
router.replace('/')
}
}
return <form action={handleSubmit}>...</form>
}
useRouter API 概览
typescript
const router = useRouter()
router.push('/dashboard') // 导航至指定路径,添加历史记录
router.replace('/dashboard') // 导航至指定路径,替换历史记录
router.back() // 后退(等同于浏览器后退按钮)
router.forward() // 前进
router.refresh() // 刷新当前路由(重新获取服务端数据)
router.prefetch('/heavy-page') // 手动预取指定页面
重要提示 :
useRouter应从next/navigation导入,而非next/router(后者属于已废弃的 Pages Router)。这是初学者常见的错误,TypeScript 可能不会报错,但运行时行为会异常。
二、URL 参数处理
URL 中携带的信息主要分为两类:路径参数 (Dynamic Segments)和查询参数(Search Params)。Next.js 提供了相应的工具来读取和处理这些信息。
1. 路径参数(Dynamic Segments)
路径参数指 URL 路径中的动态部分,如 /blog/my-post 中的 my-post。
(1)服务端组件中读取
在服务端组件中,路径参数通过 params prop 传递:
typescript
// src/app/blog/[slug]/page.tsx
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await getPostBySlug(slug)
return (
<article>
<h1>{post.title}</h1>
{/* 文章内容 */}
</article>
)
}
注意 :在 Next.js 15+ 版本中,
params是一个 Promise,需要使用await解包。
(2)客户端组件中读取
在客户端组件中,使用 useParams Hook:
typescript
'use client'
import { useParams } from 'next/navigation'
export function PostActions() {
const params = useParams<{ slug: string }>()
// params.slug 即为当前 URL 中的路径参数值
return <button>分享文章</button>
}
2. 查询参数(Search Params)
查询参数位于 URL 的 ? 之后,常用于筛选、搜索、分页等场景,如 /blog?page=2&tag=react。
(1)服务端组件中读取
typescript
// src/app/blog/page.tsx
export default async function BlogPage({
searchParams,
}: {
searchParams: Promise<{ page?: string; tag?: string; q?: string }>
}) {
const { page = '1', tag, q } = await searchParams
const posts = await getPosts({
page: parseInt(page),
tag,
query: q,
})
return (
<div>
{q && <p>搜索结果:"{q}"</p>}
{/* 文章列表 */}
</div>
)
}
(2)客户端组件中读取与更新
在客户端组件中,结合 useSearchParams、usePathname 和 useRouter 实现查询参数的读取与更新:
typescript
'use client'
import { useSearchParams, useRouter, usePathname } from 'next/navigation'
interface TagFilterProps {
tags: string[]
}
export function TagFilter({ tags }: TagFilterProps) {
const searchParams = useSearchParams()
const pathname = usePathname()
const router = useRouter()
const currentTag = searchParams.get('tag')
const handleTagClick = (tag: string) => {
const params = new URLSearchParams(searchParams.toString())
if (tag === currentTag) {
params.delete('tag') // 取消选中
} else {
params.set('tag', tag)
params.delete('page') // 切换标签时重置页码
}
router.push(`${pathname}?${params.toString()}`)
}
return (
<div className="flex gap-2 flex-wrap">
{tags.map(tag => (
<button
key={tag}
onClick={() => handleTagClick(tag)}
className={`px-3 py-1 rounded-full text-sm ${
tag === currentTag
? 'bg-blue-500 text-white'
: 'bg-gray-100 text-gray-700'
}`}
>
{tag}
</button>
))}
</div>
)
}
使用查询参数的优势:
- URL 状态可分享,用户可将筛选结果发送给他人
- 页面刷新后状态不丢失
- 支持浏览器前进/后退操作
- 有利于 SEO,搜索引擎可索引不同的筛选视图
三、当前导航项高亮
导航栏中高亮显示当前页面是常见需求,可通过 usePathname Hook 实现:
typescript
'use client'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
const navItems = [
{ href: '/', label: '首页' },
{ href: '/blog', label: '博客' },
{ href: '/about', label: '关于' },
]
export function Navbar() {
const pathname = usePathname()
return (
<nav className="flex gap-6">
{navItems.map(item => {
// 首页精确匹配,其他页面前缀匹配
const isActive = item.href === '/'
? pathname === '/'
: pathname.startsWith(item.href)
return (
<Link
key={item.href}
href={item.href}
className={`text-sm font-medium transition-colors ${
isActive
? 'text-blue-600 border-b-2 border-blue-600'
: 'text-gray-600 hover:text-gray-900'
}`}
>
{item.label}
</Link>
)
})}
</nav>
)
}
四、静态参数预生成(generateStaticParams)
对于动态路由,如果已知所有可能的参数值(如博客文章的所有 slug),可使用 generateStaticParams 在构建阶段预生成静态页面(SSG),而非每次请求时实时渲染。
typescript
// src/app/blog/[slug]/page.tsx
// 构建阶段执行,返回需要预生成的所有参数组合
export async function generateStaticParams() {
const posts = await getAllPosts()
return posts.map(post => ({ slug: post.slug }))
}
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const post = await getPostBySlug(slug)
return (
<article>
<h1>{post.title}</h1>
{/* 文章内容 */}
</article>
)
}
优势:
- 生成纯静态 HTML 文件,CDN 直接分发
- 访问速度极快,无需服务器计算
- 降低服务器负载
- 适用于博客、文档、产品列表等内容相对稳定的场景
五、并行路由(Parallel Routes)
某些复杂界面需要同时展示多个独立的内容区域,每个区域拥有独立的加载状态和错误处理。例如仪表盘中同时显示用户统计、销售图表和最新订单。Next.js提供了并行路由来处理这类需求。
1. 目录结构
使用 @ 前缀创建命名插槽(Slots):
bash
src/app/dashboard/
├── layout.tsx # 布局组件
├── page.tsx # 主页面
├── @stats/
│ ├── page.tsx # 统计数据
│ └── loading.tsx # 加载状态
├── @chart/
│ ├── page.tsx # 图表数据
│ └── loading.tsx # 加载状态
└── @recent/
├── page.tsx # 最新订单
└── loading.tsx # 加载状态
2. 布局组件实现
并行路由的插槽会跟children属性一起传递给布局组件:
typescript
// src/app/dashboard/layout.tsx
export default function DashboardLayout({
children,
stats,
chart,
recent,
}: {
children: React.ReactNode
stats: React.ReactNode
chart: React.ReactNode
recent: React.ReactNode
}) {
return (
<div className="grid grid-cols-2 gap-6">
<div className="col-span-2">{stats}</div>
<div>{chart}</div>
<div>{recent}</div>
</div>
)
}
核心价值 :各插槽独立加载,互不阻塞 。@stats 数据加载完成即可显示,无需等待 @chart 或 @recent,显著提升用户体验。
六、拦截路由(Intercepting Routes)
在某些场景下,我们希望用户点击一个链接时,不是跳转到一个全新的页面,而是在当前页面的上下文中(例如通过一个模态框)展示目标内容。同时,这个内容又拥有自己独立的 URL,可以被直接访问或分享。Next.js 的拦截路由正是为了解决这种"上下文相关导航"而设计的。
1. 核心概念
拦截路由允许你拦截一个原本要跳转的路由,并在当前布局中渲染一个替代组件,例如:
- 拦截时 :用户从
/photos点击一张照片,URL 变为/photos/123,但内容以模态框形式叠加在/photos页面上。 - 直接访问时 :用户直接在浏览器地址栏输入
/photos/123或刷新页面,则会完整渲染/photos/123的独立页面。
这完美实现了类似 Instagram 或 Dribbble 的图片浏览体验:在信息流中点击是弹窗,直接访问链接是详情页。
2. 目录结构与命名约定
拦截路由通过特殊的文件夹命名来实现,使用括号 () 和点 . 来表示相对路径关系:
(.):匹配同一层级的路由。(..):匹配上一层级的路由。(..)(..):匹配上上层级的路由。(...):匹配根目录app/下的路由。
通常,拦截路由会与并行路由 (@slot)结合使用,将拦截到的内容渲染在模态框插槽中。
3. 实战案例:图片详情模态框
假设我们有一个图片列表页 /photos,点击任意图片应弹出详情模态框,URL 变为 /photos/[id]。
文件结构如下:
bash
src/app/
├── layout.tsx # 根布局
├── photos/
│ ├── page.tsx # 图片列表页 (/photos)
│ └── [id]/
│ └── page.tsx # 图片详情页 (/photos/[id]) - 直接访问时渲染
└── @modal/ # 并行路由插槽,用于模态框
└── (..)photos/ # 拦截上一层级的 photos 路由
└── [id]/
└── page.tsx # 拦截后的模态框组件
代码实现:
typescript
// src/app/photos/[id]/page.tsx
// 这是 /photos/[id] 的独立页面,直接访问时显示
export default function PhotoPage({ params }: { params: { id: string } }) {
return (
<div className="p-8">
<h1>照片详情 #{params.id}</h1>
<Photo image-id={params.id}/> <!-- 假如已存在该组件-->
<p>这是照片的完整详情页面。</p>
</div>
)
}
// src/app/@modal/(..)photos/[id]/page.tsx
// 这是拦截路由,从 /photos 跳转时显示
export default function PhotoModal({ params }: { params: { id: string } }) {
return (
<dialog open className="fixed inset-0 bg-black/80 flex items-center justify-center">
<div className="relative">
<Photo image-id={params.id}/> <!-- 假如已存在该组件-->
<button className="absolute top-4 right-4 text-white">关闭</button>
</div>
</dialog>
)
}
布局组件配置:
为了让模态框能正确显示,需要在根布局中定义 modal 插槽。
typescript
// src/app/layout.tsx
export default function RootLayout({
children,
modal,
}: {
children: React.ReactNode
modal: React.ReactNode
}) {
return (
<html>
<body>
{children}
{modal}
</body>
</html>
)
}
处理未匹配状态:
当用户直接访问 /photos/123 时,@modal 插槽没有匹配到任何内容,Next.js 会尝试渲染 default.tsx。为了避免显示 404,我们创建一个返回 null 的默认文件。
typescript
// src/app/@modal/default.tsx
export default function Default() {
return null
}
4. 核心价值
- 保持上下文:用户在浏览列表时不会丢失当前位置,体验更流畅。
- 可分享的 URL:模态框中的内容拥有独立的 URL,可以直接复制链接分享给他人。
- 渐进增强:直接访问链接时,内容依然可以完整展示,保证了功能的健壮性。
七、中间件:路由守卫与权限控制
保护需要身份验证才能访问的页面,最优雅的实现方式是使用中间件(Middleware),在请求到达页面组件之前进行拦截和验证。
1. 中间件实现
在项目根目录(与 src/ 同级)创建 middleware.ts:
typescript
// middleware.ts
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { auth } from '@/lib/auth' // 认证工具
export default async function middleware(request: NextRequest) {
const session = await auth()
const { pathname } = request.nextUrl
// 定义需要保护的路径
const protectedPaths = ['/dashboard', '/profile', '/settings']
const isProtected = protectedPaths.some(path => pathname.startsWith(path))
// 未登录用户访问受保护路径,重定向至登录页
if (isProtected && !session) {
const loginUrl = new URL('/login', request.url)
loginUrl.searchParams.set('callbackUrl', pathname)
return NextResponse.redirect(loginUrl)
}
// 已登录用户访问登录页,重定向至首页
if (session && pathname === '/login') {
return NextResponse.redirect(new URL('/', request.url))
}
return NextResponse.next()
}
// 配置中间件匹配规则
// 排除静态资源和 API 路由(它们有独立的权限控制)
export const config = {
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
}
当然中间件除了做权限认证外,所有需要在用户访问与服务器层之间实现的逻辑,都可以使用中间件来承担。
2. 中间件的优势
- 在服务器端执行,安全性高
- 统一处理权限逻辑,避免遗漏
- 在页面组件渲染前拦截,性能更优
- 支持复杂的条件判断和重定向策略
八、重定向机制
Next.js 提供了多种重定向方式,适用于不同场景。
1. 组件内重定向
typescript
import { redirect, permanentRedirect } from 'next/navigation'
// 临时重定向(HTTP 307)
// 适用场景:登录后跳转、表单提交后跳转
async function ProtectedPage() {
const user = await getUser()
if (!user) {
redirect('/login')
}
// ...
}
// 永久重定向(HTTP 308)
// 适用场景:URL 迁移,告知搜索引擎更新索引
function OldBlogPost() {
permanentRedirect('/blog/new-url-here')
}
2. 配置文件重定向
对于批量 URL 重定向(如域名迁移),可在 next.config.ts 中统一配置:
typescript
// next.config.ts
const nextConfig = {
async redirects() {
return [
{
source: '/old-blog/:slug',
destination: '/blog/:slug',
permanent: true, // true = 308 永久重定向,false = 307 临时重定向
},
{
source: '/team',
destination: '/about#team',
permanent: false,
},
]
},
}
export default nextConfig
配置文件重定向的优势:
- 在请求层面处理,效率更高
- 对 SEO 更友好
- 集中管理,便于维护
九、路由系统架构总览
将本章所学内容整合为完整的请求处理流程:
Next.js 的路由系统不仅是"URL 到页面的映射",而是一个完整的请求处理流水线,每个环节都提供了扩展点供开发者定制。
十、本章小结
通过本章学习,你应该掌握了:
- 两种导航方式(Link 组件与 useRouter)的使用场景
- 路径参数和查询参数的读取与处理方法
- 静态参数预生成的优化策略
- 并行路由、拦截路由实现复杂布局的技巧
- 中间件实现路由守卫的最佳实践
- 不同重定向方式的适用场景
对客户端而言光有界面还不行,还得有数据。下一章《Next.js数据获取与缓存策略》将深入探讨数据获取机制------这是 Next.js 与传统 React 应用差异最大的核心特性之一。