Next.js 教程系列(三)Next.js Pages Router 深度解析

397 阅读4分钟

前言

大家好,我是鲫小鱼。是一名不写前端代码的前端工程师,热衷于分享非前端的知识,带领切图仔逃离切图圈子,欢迎关注我,微信公众号:《鲫小鱼不正经》。欢迎点赞、收藏、关注,一键三连!!

第三章:Next.js Pages Router 深度解析

教程简介

在本章中,我们将深入探索 Next.js 传统的路由方式——Pages Router。尽管 Next.js 13 引入了更强大的 App Router,但 Pages Router 仍在许多现有项目中广泛使用,并且是理解 Next.js 路由核心机制的基石。我们将详细解析 Pages Router 的文件系统路由机制,学习如何创建各种复杂的路由模式,包括动态路由、嵌套路由和 Catch-all 路由,并掌握 next/link 和 useRouter 等核心 API 的高级用法。通过本章的学习,您将能够灵活地构建基于 Pages Router 的多页面应用程序,并为理解未来的 App Router 打下坚实基础。

理论讲解

1.1 Pages Router 的文件系统路由机制:约定优于配置

Next.js Pages Router 的核心思想是文件系统路由。这意味着您无需手动配置路由表,只需在 pages/ 目录下创建文件,Next.js 就会根据文件的路径和名称自动生成对应的路由。这种"约定优于配置"的原则大大简化了路由管理。

  • 基本路由pages/about.js 会自动映射到 /about 路径。
  • 根路由pages/index.js 映射到根路径 /
  • 嵌套路由pages/blog/first-post.js 映射到 /blog/first-post 路径。您可以在 pages 目录下创建子目录来实现嵌套路由。

1.2 如何处理复杂的路由模式

1.2.1 动态路由 (Dynamic Routes)

当您需要根据不同的数据 ID 或 Slug 生成多个页面时,可以使用动态路由。在文件名或目录名中使用方括号 [] 来表示动态部分。

  • 文件级动态路由pages/posts/[id].js 会匹配 /posts/1/posts/abc 等路径,id可以在页面组件中通过 useRouter 获取。
  • 目录级动态路由pages/users/[id]/profile.js 会匹配 /users/123/profileid同样可获取。
1.2.2 嵌套动态路由

您可以在动态路由中继续嵌套动态路由,以处理更复杂的 URL 结构。

  • pages/products/[category]/[slug].js:可以匹配 /products/electronics/iphone-14,其中 category 和 slug 都是动态参数。
1.2.3 Catch-all 路由 (Catch-all Routes)

Catch-all 路由用于捕获所有子路径。在文件名中使用三个点 [...slug].js

  • pages/docs/[...slug].js:会匹配 /docs/a/docs/a/b/docs/a/b/c 等路径。所有捕获的路径段将作为数组在 slug 参数中获取。
  • 可选 Catch-all 路由:通过双层方括号 [[...slug]].js,可以使其匹配包含路径参数和不包含路径参数的情况。例如,pages/blog/[[...slug]].js 既能匹配 /blog 也能匹配 /blog/a/b

1.3 路由优先级

当存在多个路由可能匹配同一个 URL 时,Next.js 会遵循一定的优先级规则:

  1. 静态路由:精确匹配优先,如 pages/about.js 优先于动态路由。
  2. 动态路由:如 pages/posts/[id].js
  3. Catch-all 路由:如 pages/docs/[...slug].js
  4. 可选 Catch-all 路由:如 pages/blog/[[...slug]].js

例如,pages/posts/new.js 会优先匹配 /posts/new,而不会被 pages/posts/[id].js捕获。

1.4 客户端导航与 next/link 组件

在 Pages Router 中,我们使用 next/link 组件进行客户端路由跳转。它能够预加载页面,实现平滑的页面过渡,而无需浏览器刷新。

  • 基本用法

    import Link from 'next/link';
    
    function HomePage() {
      return (
        <Link href="/about">
          <a>关于我们</a>
        </Link>
      );
    }
    
  • 带参数的动态路由

    import Link from 'next/link';
    
    function PostList() {
      const postId = 123;
      return (
        <Link href={`/posts/${postId}`}>
          <a>查看文章 {postId}</a>
        </Link>
      );
    }
    
  • passHref: 当子组件是自定义的 React 组件(而不是原生 DOM 元素 <a>)时,需要添加 passHref 属性,确保 href 属性能被正确传递。

  • prefetch: 默认情况下,next/link 会在视口中自动预加载链接的页面。您也可以通过 prefetch={false} 禁用预加载。

1.5 编程式导航与 useRouter 钩子

除了 next/link,我们还可以使用 next/router 中的 useRouter 钩子来实现编程式导航,例如在表单提交后跳转、条件跳转等。

import { useRouter } from 'next/router';

function MyComponent() {
  const router = useRouter();

  const handleClick = () => {
    // 跳转到指定路径
    router.push('/dashboard');

    // 带查询参数跳转
    router.push({
      pathname: '/search',
      query: { keyword: 'nextjs' },
    });

    // 替换当前历史记录
    router.replace('/login');

    // 返回上一页
    router.back();
  };

  // 获取路由参数
  const { id } = router.query; // 如果是 /posts/[id] 页面

  return <button onClick={handleClick}>跳转</button>;
}

代码示例

2.1 创建一个多层级博客系统 (/posts/[category]/[slug].js)

我们将创建一个简单的多层级博客系统,演示如何利用动态路由和嵌套动态路由来组织文章。

  1. 创建目录和文件结构

    pages/
    └── posts/
        └── [category]/
            └── [slug].tsx
    
  2. pages/posts/[category]/[slug].tsx 内容

    // pages/posts/[category]/[slug].tsx
    import { useRouter } from 'next/router';
    import Head from 'next/head';
    import Link from 'next/link';
    
    const postsData: { [key: string]: { [key: string]: { title: string; content: string } } } = {
      frontend: {
        'nextjs-intro': {
          title: 'Next.js 入门',
          content: '这是关于 Next.js 入门的第一篇文章。',
        },
        'react-hooks': {
          title: 'React Hooks 详解',
          content: '深入理解 React Hooks 的使用和原理。',
        },
      },
      backend: {
        'nodejs-api': {
          title: 'Node.js API 开发',
          content: '使用 Node.js 构建 RESTful API。',
        },
      },
    };
    
    export default function BlogPost() {
      const router = useRouter();
      const { category, slug } = router.query;
    
      if (!category || !slug) {
        return <p>加载文章中...</p>;
      }
    
      const post = postsData[category as string]?.[slug as string];
    
      if (!post) {
        return <h1>404 - 文章未找到</h1>;
      }
    
      return (
        <>
          <Head>
            <title>{post.title}</title>
          </Head>
          <div>
            <Link href="/">
              <a>返回首页</a>
            </Link>
            <h1>{post.title}</h1>
            <p>分类: {category}</p>
            <div>{post.content}</div>
          </div>
        </>
      );
    }
    
  3. 创建文章列表页 (pages/posts/index.tsx)

    // pages/posts/index.tsx
    import Head from 'next/head';
    import Link from 'next/link';
    
    const categories = {
      frontend: [
        { slug: 'nextjs-intro', title: 'Next.js 入门' },
        { slug: 'react-hooks', title: 'React Hooks 详解' },
      ],
      backend: [
        { slug: 'nodejs-api', title: 'Node.js API 开发' },
      ],
    };
    
    export default function PostsList() {
      return (
        <>
          <Head>
            <title>所有文章</title>
          </Head>
          <div>
            <h1>所有文章</h1>
            {Object.entries(categories).map(([categoryName, posts]) => (
              <div key={categoryName}>
                <h2>{categoryName}</h2>
                <ul>
                  {posts.map((post) => (
                    <li key={post.slug}>
                      <Link href={`/posts/${categoryName}/${post.slug}`}>
                        <a>{post.title}</a>
                      </Link>
                    </li>
                  ))}
                </ul>
              </div>
            ))}
          </div>
        </>
      );
    }
    

现在,运行 pnpm dev,访问 /posts/frontend/nextjs-intro 或 /posts/backend/nodejs-api,您将看到对应的文章内容。同时,访问 /posts 可以看到文章列表,并通过 next/link 跳转。

2.2 演示 next/link 和 useRouter 的高级用法

我们将创建一个包含查询参数的页面,并使用 useRouter 进行编程式导航和获取参数。

  1. 创建 pages/search.tsx 文件

    // pages/search.tsx
    import { useRouter } from 'next/router';
    import Head from 'next/head';
    
    export default function SearchPage() {
      const router = useRouter();
      const { keyword, category } = router.query;
    
      return (
        <>
          <Head>
            <title>搜索结果</title>
          </Head>
          <div>
            <h1>搜索结果</h1>
            <p>搜索关键词: {keyword || '无'}</p>
            <p>搜索分类: {category || '无'}</p>
            <button onClick={() => router.push('/posts')}>
              返回文章列表
            </button>
            <button onClick={() => router.back()}>返回上一页</button>
          </div>
        </>
      );
    }
    
  2. 在 pages/index.tsx (首页) 中添加搜索链接和编程式导航示例

    // pages/index.tsx (部分修改)
    import Head from 'next/head';
    import Link from 'next/link';
    import { useRouter } from 'next/router'; // 导入 useRouter
    
    export default function Home() {
      const router = useRouter();
    
      const handleSearch = () => {
        router.push({
          pathname: '/search',
          query: { keyword: 'Next.js', category: '前端' },
        });
      };
    
      return (
        <>
          <Head>
            <title>我的 Next.js 教程</title>
            <meta name="description" content="Next.js 教程第一章" />
            <link rel="icon" href="/favicon.ico" />
          </Head>
          <main>
            <h1>欢迎来到我的 Next.js 教程!</h1>
            <p>这是一个使用 Next.js 构建的简单首页。</p>
            {/* 使用 Link 跳转到文章列表 */}
            <p>
              <Link href="/posts">
                <a>查看所有文章</a>
              </Link>
            </p>
            {/* 使用编程式导航进行搜索 */}
            <p>
              <button onClick={handleSearch}>
                搜索 Next.js 文章
              </button>
            </p>
          </main>
        </>
      );
    }
    

现在,运行 pnpm dev,点击首页的"搜索 Next.js 文章"按钮,会跳转到 /search?keyword=Next.js&category=前端 页面,并显示对应的查询参数。

实战项目

3.1 实现一个内容管理系统 (CMS) 的前端导航

目标:构建一个模拟 CMS 的前端导航,包含文章列表、文章详情和分类页,利用 Pages Router 实现。

  1. 项目结构 (如果您按照代码示例创建了,则已具备大部分):

    pages/
    ├── index.tsx         // 首页
    ├── posts/
    │   ├── index.tsx     // 文章列表页
    │   └── [category]/
    │       └── [slug].tsx // 文章详情页 (动态路由)
    ├── categories/
    │   └── [name].tsx    // 分类详情页 (动态路由)
    └── about.tsx         // 关于页面
    
  2. pages/categories/[name].tsx (分类详情页):

    // pages/categories/[name].tsx
    import { useRouter } from 'next/router';
    import Head from 'next/head';
    import Link from 'next/link';
    
    const allPosts = {
      frontend: [
        { slug: 'nextjs-intro', title: 'Next.js 入门' },
        { slug: 'react-hooks', title: 'React Hooks 详解' },
      ],
      backend: [
        { slug: 'nodejs-api', title: 'Node.js API 开发' },
      ],
    };
    
    export default function CategoryPage() {
      const router = useRouter();
      const { name } = router.query;
    
      if (!name) {
        return <p>加载分类中...</p>;
      }
    
      const postsInCategory = allPosts[name as string];
    
      if (!postsInCategory) {
        return <h1>404 - 分类未找到</h1>;
      }
    
      return (
        <>
          <Head>
            <title>{name} 分类</title>
          </Head>
          <div>
            <h1>分类: {name}</h1>
            <ul>
              {postsInCategory.map((post) => (
                <li key={post.slug}>
                  <Link href={`/posts/${name}/${post.slug}`}>
                    <a>{post.title}</a>
                  </Link>
                </li>
              ))}
            </ul>
            <Link href="/posts">
              <a>返回所有文章</a>
            </Link>
          </div>
        </>
      );
    }
    
  3. 更新 pages/posts/index.tsx,添加分类链接:

    // pages/posts/index.tsx (更新部分)
    import Head from 'next/head';
    import Link from 'next/link';
    
    const categoriesData = {
      frontend: [
        { slug: 'nextjs-intro', title: 'Next.js 入门' },
        { slug: 'react-hooks', title: 'React Hooks 详解' },
      ],
      backend: [
        { slug: 'nodejs-api', title: 'Node.js API 开发' },
      ],
    };
    
    export default function PostsList() {
      return (
        <>
          <Head>
            <title>所有文章</title>
          </Head>
          <div>
            <h1>所有文章</h1>
            {Object.entries(categoriesData).map(([categoryName, posts]) => (
              <div key={categoryName}>
                <h2>
                  <Link href={`/categories/${categoryName}`}>
                    <a>{categoryName}</a>
                  </Link>
                </h2>
                <ul>
                  {posts.map((post) => (
                    <li key={post.slug}>
                      <Link href={`/posts/${categoryName}/${post.slug}`}>
                        <a>{post.title}</a>
                      </Link>
                    </li>
                  ))}
                </ul>
              </div>
            ))}
          </div>
        </>
      );
    }
    

现在,您可以通过以下方式测试您的 CMS 导航:

  • 访问 /posts 查看所有文章和分类链接。
  • 点击分类名称(如 "frontend")跳转到分类详情页 /categories/frontend
  • 点击文章标题跳转到文章详情页 /posts/frontend/nextjs-intro

通过本章的学习,您已经对 Next.js Pages Router 的文件系统路由机制有了全面的理解,并掌握了动态路由、嵌套路由、Catch-all 路由的创建和使用。同时,您也熟练运用了 next/link 和 useRouter 进行页面导航。下一章我们将探讨 Next.js 13+ 引入的 App Router 核心概念,它为 Next.js 的全栈能力带来了革命性的变化。

最后感谢阅读!欢迎关注我,微信公众号:《鲫小鱼不正经》。欢迎点赞、收藏、关注,一键三连!!!