自在学
分类课程智能体订阅
分类课程AI导师价格
课程进度
6 / 14
上一节数据获取和渲染下一节API 路由
自在学

© 2025 - 2026 自在学,保留所有权利。

公网安备湘公网安备43020302000292号 | 湘ICP备2025148919号-1

关于我们隐私政策使用条款

© 2025 自在学,保留所有权利。

公网安备湘公网安备43020302000292号湘ICP备2025148919号-1

编程Next.js指南路由和导航

路由和导航

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

路由和导航


使用 Link 组件进行导航

在 Next.js 中,最常用的导航方式是使用 Link 组件。Link 组件提供了客户端导航,这意味着页面切换不会触发完整的页面刷新,而是只更新需要变化的部分,这大大提升了用户体验。 让我们从一个简单的例子开始:

tsx
// 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 属性来控制预取行为:

tsx
// 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>
    </nav>
  );
}

在这个例子中,首页链接会启用预取,关于页面链接会禁用预取。禁用预取对于不常用的页面或需要减少网络请求的场景很有用。

Link 组件的样式

Link 组件渲染为 <a> 标签,所以我们可以像普通链接一样给它添加样式:

tsx
// 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="text-blue-500 hover:text-blue-700">
        关于
      </Link>
    </nav>
  );
}

在 Next.js 13+ 中,Link 组件可以直接接受 className 属性,它会自动应用到内部的 <a> 标签上。

动态路由导航

当我们需要导航到动态路由时,我们可以直接在 href 中使用模板字符串:

tsx
// 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/${product.id}`}>
        查看详情
      </Link>
    </div>
  );
}

在这个例子中,我们使用模板字符串来构建动态路径。当用户点击链接时,会导航到 /products/[id] 路由,其中 [id] 是产品的 ID。


使用 useRouter Hook

有时候,我们需要在组件中编程式地导航,而不是使用 Link 组件。这时候,我们可以使用 useRouter hook。

useRouter 是一个客户端 hook,所以只能在客户端组件中使用:

tsx
// 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 = (e: React.FormEvent) => {
    e.preventDefault();
    router.push(`/search?q=${query}`);
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="搜索..."
      />
      <button type="submit">搜索</button>
    </form>
  );
}

在这个例子中,我们使用 useRouter hook 获取路由对象,然后使用 push 方法来导航到新页面。push 方法接受一个路径字符串,可以是相对路径或绝对路径。

useRouter 还提供了其他有用的方法:

tsx
'use client';
 
import { useRouter } from 'next/navigation';
 
export default function NavigationButtons() {
  const router = useRouter();
  
  return (
    <div>
      <button onClick={() => router.push('/about')}>
        前往关于页面
      </button>
      <button onClick={() => router.back()}>
        返回上一页
      </button>
      <button onClick={() => router.forward()}>
        前进一页
      </button>
      <button onClick={() => router.refresh()}>
        刷新页面
      </button>
      <button onClick={() => router.replace('/home')}>
        替换当前页面(不添加历史记录)
      </button>
    </div>
  );
}

back() 方法相当于浏览器的后退按钮,forward() 方法相当于前进按钮,refresh() 方法会刷新当前页面,replace() 方法会导航到新页面但不添加历史记录。


获取路由信息

有时候,我们需要在组件中获取当前路由的信息,比如路径名、查询参数等。Next.js 提供了几个 hooks 来获取这些信息。

usePathname

第一个是 usePathname hook,用于获取当前路径:

tsx
// 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 isActive = pathname === href;
  
  return (
    <Link
      href={href}
      className={isActive ? 'active' : ''}
    >
      {children}
    </Link>
  );
}

这个组件会根据当前路径自动添加 active 类,这对于导航栏的高亮显示很有用。

useSearchParams

第二个是 useSearchParams hook,用于获取查询参数:

tsx
// 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>
      <p>搜索关键词: {query}</p>
    </div>
  );
}

useSearchParams 返回一个 URLSearchParams 对象,我们可以使用 get 方法来获取特定的查询参数。

注意,useSearchParams 需要在 Suspense 边界内使用:

tsx
// 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 />}>
        <SearchResults />
      </Suspense>
    </div>
  );
}

这是因为查询参数可能在客户端才可用,所以需要 Suspense 来处理加载状态。


路由参数的使用

在动态路由中,我们可以通过 params 获取路由参数。在服务器组件中,params 是一个 Promise:

tsx
// app/products/[id]/page.tsx
export default async function ProductPage({
  params,
}: {
  params: Promise<{ id: string }>;
}) {
  const { id } = await params;
  
  return (
    <div>
      <h1>产品 {id}</h1>
    </div>
  );
}

在客户端组件中,我们可以使用 useParams hook:

tsx
// 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 结构,但允许我们为不同的路由组使用不同的布局:

tsx
// app/(marketing)/layout.tsx
export default function MarketingLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <div className="marketing-layout">
      <header>营销页面头部</header>
      {children}
    </div>
  );
}
tsx
// 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 命名约定,允许我们在同一个布局中同时渲染多个页面:

tsx
// app/dashboard/@analytics/page.tsx
export default function AnalyticsPage() {
  return <div>分析数据</div>;
}
tsx
// app/dashboard/@team/page.tsx
export default function TeamPage() {
  return <div>团队信息</div>;
}
tsx
// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
  analytics,
  team,
}: {
  children: React.ReactNode;
  analytics: React.ReactNode;
  team: React.ReactNode;
}) {
  return (
    <div>
      {children}
      <div className="sidebar">
        {analytics}
        {team}
      </div>
    </div>
  );
}

在这个例子中,@analytics 和 @team 是并行路由槽。它们会在同一个布局中同时渲染,但不会影响主路由的 URL。


路由拦截

Next.js 支持路由拦截,这允许我们在不改变 URL 的情况下显示不同的内容。这对于模态框、侧边栏等场景很有用。

路由拦截使用 (.) 或 (..) 语法来实现:

shell
app/
├── @modal/
│   └── (.)products/
│       └── [id]/
│           └── page.tsx    # 拦截 /products/[id]
└── products/
    └── [id]/
        └── page.tsx        # 正常路由 /products/[id]

(.) 表示同一层级,(..) 表示上一层级,(..)(..) 表示上两层,以此类推。

tsx
// 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}</h1>
      <Link href="/products">关闭</Link>
    </div>
  );
}

当用户从 /products 页面点击链接导航到 /products/123 时,会显示模态框而不是完整页面。URL 会更新为 /products/123,但页面内容会被拦截并显示为模态框。


路由过渡和加载状态

Next.js 提供了内置的加载状态处理。我们可以使用 loading.tsx 文件来显示加载状态,使用 Suspense 来处理异步组件。

让我们看一个使用 Suspense 的例子:

tsx
// 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>
      ))}
    </ul>
  );
}
 
function ProductListSkeleton() {
  return (
    <ul>
      {[1, 2, 3].map((i) => (
        <li key={i}>加载中...</li>
      ))}
    </ul>
  );
}
 
export default function ProductsPage() {
  return (
    <div>
      <h1>产品列表</h1>
      <Suspense fallback={<ProductListSkeleton />}>
        <ProductList />
      </Suspense>
    </div>
  );
}

当 ProductList 组件正在加载数据时,Suspense 会显示 ProductListSkeleton。一旦数据加载完成,ProductList 会替换骨架屏。


路由保护

有时候,我们需要保护某些路由,只允许已认证的用户访问。我们可以使用中间件或布局组件来实现路由保护。

使用布局组件保护路由:

tsx
// 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 = await checkAuth();
  
  if (!isAuthenticated) {
    redirect('/login');
  }
  
  return <div>{children}</div>;
}

在这个例子中,如果用户未登录,会被重定向到登录页面。我们会在后面的学习中再来讲解对于中间件的使用。


路由重定向

Next.js 提供了用于页面重定向的 redirect 函数。你可以在服务器组件(Server Component)、服务器 actions 或 API 中直接调用它来立即将用户导航到新的地址。 例如,当你检测到某个条件(如用户未登录)时,可以在组件逻辑内调用 redirect('/login')。被调用后,当前请求会中断,其它组件不会再继续渲染,用户将被直接导航到指定的路径。

下面是一个常见场景:在服务端组件内根据业务条件进行重定向:

tsx
// app/old-page/page.tsx
import { redirect } from 'next/navigation';
 
export default function OldPage() {
  redirect('/new-page');
}

当用户访问 /old-page 时,会被自动重定向到 /new-page。

我们也可以在 next.config.mjs 中配置永久重定向:

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

  • 使用 Link 组件进行导航
    • Link 组件的样式
    • 动态路由导航
  • 使用 useRouter Hook
  • 获取路由信息
    • usePathname
    • useSearchParams
  • 路由参数的使用
  • 路由组和并行路由
  • 路由拦截
  • 路由过渡和加载状态
  • 路由保护
  • 路由重定向
  • 小节回顾

目录

  • 使用 Link 组件进行导航
    • Link 组件的样式
    • 动态路由导航
  • 使用 useRouter Hook
  • 获取路由信息
    • usePathname
    • useSearchParams
  • 路由参数的使用
  • 路由组和并行路由
  • 路由拦截
  • 路由过渡和加载状态
  • 路由保护
  • 路由重定向
  • 小节回顾