在 Web 应用中,路由和导航是连接不同页面的桥梁。Next.js 提供了强大的路由系统,让我们可以轻松地实现页面间的导航,处理动态路由,以及管理应用的状态。在这一节课中,我们将探讨 Next.js 的路由和导航功能,了解如何使用 Link 组件进行客户端导航,如何使用路由 hooks 获取路由信息,以及如何处理各种路由场景。

在 Next.js 中,最常用的导航方式是使用 Link 组件。Link 组件提供了客户端导航,这意味着页面切换不会触发完整的页面刷新,而是只更新需要变化的部分,这大大提升了用户体验。
让我们从一个简单的例子开始:
|// app/components/Navigation.tsx import Link from 'next/link'; export default function Navigation() { return ( <nav> <Link href="/">首页</Link> <Link href="/about">关于</Link> <Link href="/products">产品</Link> </nav> ); }
这个例子展示了最基本的用法。我们导入 Link 组件,然后使用 href 属性指定目标路径。Link 组件会渲染为一个 <a> 标签,但提供了客户端导航的功能。
Link 组件的一个重要特性是预取(prefetching)。默认情况下,当 Link 组件进入视口时,Next.js 会自动预取目标页面的内容。这意味着当用户点击链接时,页面几乎可以立即加载,因为内容已经在后台准备好了。
我们可以通过 prefetch 属性来控制预取行为:
|// app/components/Navigation.tsx import Link from 'next/link'; export default function Navigation() { return ( <nav> <Link href="/" prefetch={true}>首页</Link> <Link href="/about" prefetch={false}>关于</Link>
在这个例子中,首页链接会启用预取,关于页面链接会禁用预取。禁用预取对于不常用的页面或需要减少网络请求的场景很有用。
Link 组件渲染为 <a> 标签,所以我们可以像普通链接一样给它添加样式:
|// app/components/Navigation.tsx import Link from 'next/link'; export default function Navigation() { return ( <nav className="flex gap-4"> <Link href="/" className="text-blue-500 hover:text-blue-700"> 首页 </Link> <Link href="/about" className=
在 Next.js 13+ 中,Link 组件可以直接接受 className 属性,它会自动应用到内部的 <a> 标签上。
当我们需要导航到动态路由时,我们可以直接在 href 中使用模板字符串:
|// app/components/ProductCard.tsx import Link from 'next/link'; export default function ProductCard({ product }: { product: { id: string; name: string } }) { return ( <div> <h3>{product.name}</h3> <Link href={
在这个例子中,我们使用模板字符串来构建动态路径。当用户点击链接时,会导航到 /products/[id] 路由,其中 [id] 是产品的 ID。
有时候,我们需要在组件中编程式地导航,而不是使用 Link 组件。这时候,我们可以使用 useRouter hook。
useRouter 是一个客户端 hook,所以只能在客户端组件中使用:
|// app/components/SearchForm.tsx 'use client'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; export default function SearchForm() { const router = useRouter(); const [query, setQuery] = useState(''); const handleSubmit = (
在这个例子中,我们使用 useRouter hook 获取路由对象,然后使用 push 方法来导航到新页面。push 方法接受一个路径字符串,可以是相对路径或绝对路径。
useRouter 还提供了其他有用的方法:
|'use client'; import { useRouter } from 'next/navigation'; export default function NavigationButtons() { const router = useRouter(); return ( <div> <button onClick={() => router.push('/about')}> 前往关于页面 </button>
back() 方法相当于浏览器的后退按钮,forward() 方法相当于前进按钮,refresh() 方法会刷新当前页面,replace() 方法会导航到新页面但不添加历史记录。
有时候,我们需要在组件中获取当前路由的信息,比如路径名、查询参数等。Next.js 提供了几个 hooks 来获取这些信息。
第一个是 usePathname hook,用于获取当前路径:
|// app/components/ActiveLink.tsx 'use client'; import Link from 'next/link'; import { usePathname } from 'next/navigation'; export default function ActiveLink({ href, children }: { href: string; children: React.ReactNode }) { const pathname = usePathname(); const
这个组件会根据当前路径自动添加 active 类,这对于导航栏的高亮显示很有用。
第二个是 useSearchParams hook,用于获取查询参数:
|// app/components/SearchResults.tsx 'use client'; import { useSearchParams } from 'next/navigation'; export default function SearchResults() { const searchParams = useSearchParams(); const query = searchParams.get('q'); return ( <div> <h1>搜索结果</h1> <
useSearchParams 返回一个 URLSearchParams 对象,我们可以使用 get 方法来获取特定的查询参数。
注意,useSearchParams 需要在 Suspense 边界内使用:
|// app/search/page.tsx import { Suspense } from 'react'; import SearchResults from './SearchResults'; function SearchResultsFallback() { return <div>加载中...</div>; } export default function SearchPage() { return ( <div> <Suspense fallback={<SearchResultsFallback />}>
这是因为查询参数可能在客户端才可用,所以需要 Suspense 来处理加载状态。
在动态路由中,我们可以通过 params 获取路由参数。在服务器组件中,params 是一个 Promise:
|// app/products/[id]/page.tsx export default async function ProductPage({ params, }: { params: Promise<{ id: string }>; }) { const { id } = await params; return ( <div> <h1>产品 {id}</h1>
在客户端组件中,我们可以使用 useParams hook:
|// app/products/[id]/components/ProductDetails.tsx 'use client'; import { useParams } from 'next/navigation'; export default function ProductDetails() { const params = useParams(); const id = params.id as string; return ( <div> <h1>产品 {id}</h1> </div
useParams 返回一个对象,包含所有动态路由参数。注意,参数值都是字符串类型,如果需要其他类型,需要进行类型转换。
在第二节课中,我们简单介绍了路由组和并行路由。现在让我们更深入地了解它们的使用。
路由组使用括号 () 来定义,它们不会影响 URL 结构,但允许我们为不同的路由组使用不同的布局:
|// app/(marketing)/layout.tsx export default function MarketingLayout({ children, }: { children: React.ReactNode; }) { return ( <div className="marketing-layout"> <header>营销页面头部</header> {children} </div> ); }
|// app/(shop)/layout.tsx export default function ShopLayout({ children, }: { children: React.ReactNode; }) { return ( <div className="shop-layout"> <header>商店页面头部</header> {children} </div> ); }
这样,app/(marketing)/about/page.tsx 会使用营销布局,app/(shop)/products/page.tsx 会使用商店布局,但它们的 URL 分别是 /about 和 /products,不包含路由组名称。
并行路由使用 @folder 命名约定,允许我们在同一个布局中同时渲染多个页面:
|// app/dashboard/@analytics/page.tsx export default function AnalyticsPage() { return <div>分析数据</div>; }
|// app/dashboard/@team/page.tsx export default function TeamPage() { return <div>团队信息</div>; }
|// app/dashboard/layout.tsx export default function DashboardLayout({ children, analytics, team, }: { children: React.ReactNode; analytics: React.ReactNode; team: React.ReactNode; }) { return ( <div>
在这个例子中,@analytics 和 @team 是并行路由槽。它们会在同一个布局中同时渲染,但不会影响主路由的 URL。
Next.js 支持路由拦截,这允许我们在不改变 URL 的情况下显示不同的内容。这对于模态框、侧边栏等场景很有用。
路由拦截使用 (.) 或 (..) 语法来实现:
|app/ ├── @modal/ │ └── (.)products/ │ └── [id]/ │ └── page.tsx # 拦截 /products/[id] └── products/ └── [id]/ └── page.tsx # 正常路由 /products/[id]
(.) 表示同一层级,(..) 表示上一层级,(..)(..) 表示上两层,以此类推。
|// app/@modal/(.)products/[id]/page.tsx export default function ProductModal({ params, }: { params: Promise<{ id: string }>; }) { const { id } = await params; return ( <div className="modal"> <h1>产品 {id}</
当用户从 /products 页面点击链接导航到 /products/123 时,会显示模态框而不是完整页面。URL 会更新为 /products/123,但页面内容会被拦截并显示为模态框。
Next.js 提供了内置的加载状态处理。我们可以使用 loading.tsx 文件来显示加载状态,使用 Suspense 来处理异步组件。
让我们看一个使用 Suspense 的例子:
|// app/products/page.tsx import { Suspense } from 'react'; async function ProductList() { const products = await fetchProducts(); return ( <ul> {products.map((product) => ( <li key={product.id}>{product.name}</li> ))} </
当 ProductList 组件正在加载数据时,Suspense 会显示 ProductListSkeleton。一旦数据加载完成,ProductList 会替换骨架屏。
有时候,我们需要保护某些路由,只允许已认证的用户访问。我们可以使用中间件或布局组件来实现路由保护。
使用布局组件保护路由:
|// app/dashboard/layout.tsx import { redirect } from 'next/navigation'; async function checkAuth() { // 检查用户是否已登录 return false; // 示例 } export default async function DashboardLayout({ children, }: { children: React.ReactNode; }) { const isAuthenticated =
在这个例子中,如果用户未登录,会被重定向到登录页面。我们会在后面的学习中再来讲解对于中间件的使用。
Next.js 提供了用于页面重定向的 redirect 函数。你可以在服务器组件(Server Component)、服务器 actions 或 API 中直接调用它来立即将用户导航到新的地址。
例如,当你检测到某个条件(如用户未登录)时,可以在组件逻辑内调用 redirect('/login')。被调用后,当前请求会中断,其它组件不会再继续渲染,用户将被直接导航到指定的路径。
下面是一个常见场景:在服务端组件内根据业务条件进行重定向:
|// app/old-page/page.tsx import { redirect } from 'next/navigation'; export default function OldPage() { redirect('/new-page'); }
当用户访问 /old-page 时,会被自动重定向到 /new-page。
我们也可以在 next.config.mjs 中配置永久重定向:
|// next.config.mjs const nextConfig = { async redirects() { return [ { source: '/old-page', destination: '/new-page', permanent: true, // 301 重定向 }, ]; }, }; export default nextConfig;
在这一节中,我们探讨了 Next.js 的路由和导航功能。我们学习了如何使用 Link 组件进行客户端导航,如何使用 useRouter hook 进行编程式导航,如何获取路由信息和参数,以及如何处理各种复杂的路由场景。
路由和导航是 Web 应用的基础功能,通过合理使用 Next.js 提供的路由功能,我们可以创建既高效又易用的导航体验。
在下一节课,我们将学习 API 路由和 Route Handlers,了解如何在 Next.js 中创建 API 端点,处理 HTTP 请求和响应,以及如何构建完整的后端功能。