在 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 请求和响应,以及如何构建完整的后端功能。