MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Next.js嵌套路由的设计与实现方法

2022-10-262.1k 阅读

Next.js 嵌套路由概述

在现代前端应用开发中,路由是构建单页应用(SPA)的核心功能之一。Next.js 作为一个流行的 React 框架,提供了强大且灵活的路由系统,其中嵌套路由是其重要特性。嵌套路由允许开发者以一种层次化的方式组织页面结构,使应用的 URL 与组件结构相对应,提升代码的可维护性和用户体验。

例如,在一个电商应用中,可能有产品列表页面 /products,每个产品又有详细页面,如 /products/[productId]。而在产品详细页面中,可能还有评论、规格等子页面,这时就可以通过嵌套路由来实现,如 /products/[productId]/reviews/products/[productId]/specs。这种结构使得应用的导航和 URL 管理更加直观。

Next.js 的嵌套路由基于文件系统,通过在页面目录中创建嵌套的文件和文件夹结构来定义路由。这种基于文件系统的路由方式与传统的配置式路由(如 React Router 等)有所不同,它更简洁且易于上手。

嵌套路由基础:文件系统路由

创建基本嵌套路由

在 Next.js 项目中,创建嵌套路由非常简单。假设我们有一个项目结构如下:

pages/
├── products/
│   ├── index.js
│   └── [productId]/
│       ├── index.js
│       └── reviews.js
└── index.js

这里 pages/products/index.js 对应 /products 页面,这是产品列表页面。pages/products/[productId]/index.js 对应 /products/[productId] 页面,即单个产品的详细页面,其中 [productId] 是动态路由参数。而 pages/products/[productId]/reviews.js 对应 /products/[productId]/reviews 页面,用于展示产品的评论。

pages/products/[productId]/index.js 文件为例,其代码可能如下:

import React from 'react';

const ProductDetails = ({ params }) => {
  const { productId } = params;
  return (
    <div>
      <h1>Product Details: {productId}</h1>
      {/* 这里可以根据 productId 从 API 获取产品详细信息并展示 */}
    </div>
  );
};

export default ProductDetails;

在上述代码中,通过 params 对象获取动态路由参数 productId

动态路由参数的使用

动态路由参数在嵌套路由中非常关键。如上面的 [productId],它允许我们根据不同的产品 ID 展示不同的产品详细信息。在 Next.js 中,获取动态路由参数很方便。除了在页面组件中通过 params 获取外,还可以在 getStaticPropsgetServerSideProps 函数中获取。

例如,在 pages/products/[productId]/index.js 中使用 getStaticProps 获取动态路由参数并进行数据预取:

import React from'react';
import { getAllProducts } from '../lib/api';

const ProductDetails = ({ product }) => {
  return (
    <div>
      <h1>{product.title}</h1>
      <p>{product.description}</p>
    </div>
  );
};

export async function getStaticProps({ params }) {
  const { productId } = params;
  const product = await getAllProducts(productId);
  return {
    props: {
      product
    },
    revalidate: 60 // 每 60 秒重新验证数据(用于增量静态再生)
  };
}

export async function getStaticPaths() {
  const products = await getAllProducts();
  const paths = products.map(product => ({
    params: { productId: product.id.toString() }
  }));
  return { paths, fallback: false };
}

export default ProductDetails;

getStaticProps 中,通过 params 获取 productId,然后调用 API 获取对应的产品数据。getStaticPaths 函数用于生成静态页面路径,这里根据所有产品生成对应的路径。

嵌套布局与共享组件

创建嵌套布局

在嵌套路由中,常常需要在多个页面共享一些布局,比如导航栏、侧边栏等。Next.js 提供了一种简单的方式来实现嵌套布局。

假设我们有一个布局组件 ProductLayout,它包含一个导航栏和一个侧边栏,用于包裹产品相关的页面。在 pages/products 目录下创建 layout.js 文件:

import React from'react';
import Navbar from '../../components/Navbar';
import Sidebar from '../../components/Sidebar';

const ProductLayout = ({ children }) => {
  return (
    <div>
      <Navbar />
      <div className="container">
        <Sidebar />
        <div className="content">{children}</div>
      </div>
    </div>
  );
};

export default ProductLayout;

然后,在 pages/products/index.jspages/products/[productId]/index.js 等页面中引入这个布局组件:

import React from'react';
import ProductLayout from './layout';

const ProductList = () => {
  return (
    <ProductLayout>
      <h1>Product List</h1>
      {/* 产品列表内容 */}
    </ProductLayout>
  );
};

export default ProductList;

这样,所有产品相关页面都共享了 ProductLayout 的布局,包括导航栏和侧边栏。

共享组件的数据传递

有时候,共享组件(如导航栏、侧边栏)可能需要从页面组件获取数据。比如,导航栏可能需要根据当前页面的标题来更新其显示内容。

可以通过 React 的上下文(Context)或者属性传递来实现。以属性传递为例,假设 Navbar 组件需要一个 pageTitle 属性:

// Navbar.js
import React from'react';

const Navbar = ({ pageTitle }) => {
  return (
    <nav>
      <h2>{pageTitle}</h2>
      {/* 其他导航栏内容 */}
    </nav>
  );
};

export default Navbar;

ProductLayout 中传递 pageTitle

import React from'react';
import Navbar from '../../components/Navbar';
import Sidebar from '../../components/Sidebar';

const ProductLayout = ({ children, pageTitle }) => {
  return (
    <div>
      <Navbar pageTitle={pageTitle} />
      <div className="container">
        <Sidebar />
        <div className="content">{children}</div>
      </div>
    </div>
  );
};

export default ProductLayout;

然后在页面组件中传入 pageTitle

import React from'react';
import ProductLayout from './layout';

const ProductList = () => {
  return (
    <ProductLayout pageTitle="Product List">
      <h1>Product List</h1>
      {/* 产品列表内容 */}
    </ProductLayout>
  );
};

export default ProductList;

这样就实现了页面组件向共享组件的数据传递。

嵌套路由的导航与链接

使用 Link 组件进行导航

在 Next.js 中,使用 next/link 组件进行页面导航。在嵌套路由中,Link 组件的使用方法与普通路由类似,但需要注意正确构建链接路径。

例如,在产品列表页面 pages/products/index.js 中,要链接到单个产品详细页面:

import React from'react';
import Link from 'next/link';
import { getAllProducts } from '../lib/api';

const ProductList = ({ products }) => {
  return (
    <div>
      <h1>Product List</h1>
      <ul>
        {products.map(product => (
          <li key={product.id}>
            <Link href={`/products/${product.id}`}>
              <a>{product.title}</a>
            </Link>
          </li>
        ))}
      </ul>
    </div>
  );
};

export async function getStaticProps() {
  const products = await getAllProducts();
  return {
    props: {
      products
    }
  };
}

export default ProductList;

在上述代码中,通过 Link 组件的 href 属性指定链接路径为 /products/[productId],这样当用户点击链接时,会导航到对应的产品详细页面。

动态生成链接

在一些场景下,需要动态生成链接,比如在产品详细页面中链接到评论页面。在 pages/products/[productId]/index.js 中:

import React from'react';
import Link from 'next/link';

const ProductDetails = ({ params }) => {
  const { productId } = params;
  return (
    <div>
      <h1>Product Details: {productId}</h1>
      <Link href={`/products/${productId}/reviews`}>
        <a>View Reviews</a>
      </Link>
    </div>
  );
};

export default ProductDetails;

这里根据当前产品的 productId 动态生成了指向评论页面的链接 /products/[productId]/reviews

嵌套路由的数据获取策略

getStaticProps 在嵌套路由中的应用

getStaticProps 是 Next.js 中用于在构建时获取数据并将其作为属性传递给页面组件的函数。在嵌套路由中,它同样非常有用。

以产品详细页面为例,我们可以在 pages/products/[productId]/index.js 中使用 getStaticProps 来获取产品的详细信息:

import React from'react';
import { getProductById } from '../lib/api';

const ProductDetails = ({ product }) => {
  return (
    <div>
      <h1>{product.title}</h1>
      <p>{product.description}</p>
    </div>
  );
};

export async function getStaticProps({ params }) {
  const { productId } = params;
  const product = await getProductById(productId);
  return {
    props: {
      product
    }
  };
}

export async function getStaticPaths() {
  const products = await getAllProducts();
  const paths = products.map(product => ({
    params: { productId: product.id.toString() }
  }));
  return { paths, fallback: false };
}

export default ProductDetails;

getStaticProps 中,通过 params 获取 productId,然后调用 API 获取产品详细信息。getStaticPaths 函数用于生成静态页面路径,这样 Next.js 会在构建时为每个产品生成静态页面。

getServerSideProps 与嵌套路由

getServerSideProps 用于在每次请求时获取数据。在一些场景下,比如数据需要实时获取,或者数据依赖于用户会话等,就需要使用 getServerSideProps

例如,在产品评论页面 pages/products/[productId]/reviews.js 中,如果评论数据需要根据用户登录状态获取不同的内容:

import React from'react';
import { getReviewsByProductId } from '../lib/api';

const ProductReviews = ({ reviews }) => {
  return (
    <div>
      <h1>Product Reviews</h1>
      <ul>
        {reviews.map(review => (
          <li key={review.id}>{review.text}</li>
        ))}
      </ul>
    </div>
  );
};

export async function getServerSideProps({ req, params }) {
  const { productId } = params;
  const user = req.user; // 假设这里可以从请求中获取用户信息
  const reviews = await getReviewsByProductId(productId, user);
  return {
    props: {
      reviews
    }
  };
}

export default ProductReviews;

getServerSideProps 中,通过 params 获取 productId,并根据 req 中的用户信息获取相应的评论数据。

嵌套路由的错误处理

404 页面处理

在嵌套路由中,当用户访问不存在的页面时,需要显示 404 页面。Next.js 提供了内置的 404 页面支持。只需在 pages 目录下创建 404.js 文件:

import React from'react';

const NotFound = () => {
  return (
    <div>
      <h1>404 - Page Not Found</h1>
    </div>
  );
};

export default NotFound;

当 Next.js 无法找到匹配的页面时,会自动渲染这个 404 页面。

其他错误处理

除了 404 错误,在嵌套路由的数据获取过程中也可能出现其他错误,比如 API 调用失败。可以在 getStaticPropsgetServerSideProps 中进行错误处理。

例如,在 pages/products/[productId]/index.jsgetStaticProps 中:

import React from'react';
import { getProductById } from '../lib/api';

const ProductDetails = ({ product, error }) => {
  if (error) {
    return (
      <div>
        <h1>Error</h1>
        <p>{error.message}</p>
      </div>
    );
  }
  return (
    <div>
      <h1>{product.title}</h1>
      <p>{product.description}</p>
    </div>
  );
};

export async function getStaticProps({ params }) {
  try {
    const { productId } = params;
    const product = await getProductById(productId);
    return {
      props: {
        product
      }
    };
  } catch (error) {
    return {
      props: {
        error
      },
      notFound: true // 也可以设置 notFound 为 true 直接返回 404 页面
    };
  }
}

export async function getStaticPaths() {
  const products = await getAllProducts();
  const paths = products.map(product => ({
    params: { productId: product.id.toString() }
  }));
  return { paths, fallback: false };
}

export default ProductDetails;

在上述代码中,通过 try - catch 捕获 getProductById 可能抛出的错误,并将错误信息传递给页面组件进行显示。

嵌套路由的 SEO 优化

页面标题与元数据设置

对于嵌套路由的页面,正确设置页面标题和元数据对于 SEO 非常重要。可以使用 next/head 组件来设置这些信息。

例如,在产品详细页面 pages/products/[productId]/index.js 中:

import React from'react';
import Head from 'next/head';
import { getProductById } from '../lib/api';

const ProductDetails = ({ product }) => {
  return (
    <div>
      <Head>
        <title>{product.title} - Product Details</title>
        <meta name="description" content={product.description} />
      </Head>
      <h1>{product.title}</h1>
      <p>{product.description}</p>
    </div>
  );
};

export async function getStaticProps({ params }) {
  const { productId } = params;
  const product = await getProductById(productId);
  return {
    props: {
      product
    }
  };
}

export async function getStaticPaths() {
  const products = await getAllProducts();
  const paths = products.map(product => ({
    params: { productId: product.id.toString() }
  }));
  return { paths, fallback: false };
}

export default ProductDetails;

通过 Head 组件设置页面标题和描述元数据,这样搜索引擎可以更好地理解页面内容。

规范链接(Canonical Links)

在嵌套路由中,如果存在相似内容的页面(比如不同参数的产品列表页面),设置规范链接可以避免搜索引擎认为是重复内容。

例如,在产品列表页面 pages/products/index.js 中:

import React from'react';
import Head from 'next/head';
import { getAllProducts } from '../lib/api';

const ProductList = ({ products }) => {
  return (
    <div>
      <Head>
        <link rel="canonical" href="/products" />
        <title>Product List</title>
        <meta name="description" content="List of all products" />
      </Head>
      <h1>Product List</h1>
      <ul>
        {products.map(product => (
          <li key={product.id}>
            {product.title}
          </li>
        ))}
      </ul>
    </div>
  );
};

export async function getStaticProps() {
  const products = await getAllProducts();
  return {
    props: {
      products
    }
  };
}

export default ProductList;

通过 link 标签设置规范链接为 /products,告诉搜索引擎这是该内容的主要 URL。

嵌套路由的性能优化

代码拆分与懒加载

在嵌套路由中,随着应用规模的增大,代码体积可能会变得很大。Next.js 支持代码拆分和懒加载,以提高性能。

例如,在产品详细页面中,如果有一些不常用的功能模块(如产品 3D 展示),可以进行懒加载。假设我们有一个 Product3D 组件:

import React, { lazy, Suspense } from'react';
import { getProductById } from '../lib/api';

const Product3D = lazy(() => import('./Product3D'));

const ProductDetails = ({ product }) => {
  return (
    <div>
      <h1>{product.title}</h1>
      <p>{product.description}</p>
      <Suspense fallback={<div>Loading 3D model...</div>}>
        <Product3D product={product} />
      </Suspense>
    </div>
  );
};

export async function getStaticProps({ params }) {
  const { productId } = params;
  const product = await getProductById(productId);
  return {
    props: {
      product
    }
  };
}

export async function getStaticPaths() {
  const products = await getAllProducts();
  const paths = products.map(product => ({
    params: { productId: product.id.toString() }
  }));
  return { paths, fallback: false };
}

export default ProductDetails;

通过 lazySuspense 组件实现 Product3D 组件的懒加载,只有当需要展示 3D 模型时才加载该组件的代码。

静态资源优化

对于嵌套路由页面中的静态资源(如图像、样式文件等),可以进行优化。比如使用 Next.js 内置的图像优化功能,对图像进行自动压缩和响应式处理。

在页面中引入图像:

import React from'react';
import Image from 'next/image';
import { getProductById } from '../lib/api';

const ProductDetails = ({ product }) => {
  return (
    <div>
      <h1>{product.title}</h1>
      <Image
        src={product.imageUrl}
        alt={product.title}
        width={300}
        height={200}
      />
      <p>{product.description}</p>
    </div>
  );
};

export async function getStaticProps({ params }) {
  const { productId } = params;
  const product = await getProductById(productId);
  return {
    props: {
      product
    }
  };
}

export async function getStaticPaths() {
  const products = await getAllProducts();
  const paths = products.map(product => ({
    params: { productId: product.id.toString() }
  }));
  return { paths, fallback: false };
}

export default ProductDetails;

next/image 组件会自动对图像进行优化,根据设备屏幕大小加载合适尺寸的图像,提高页面加载性能。

嵌套路由的高级应用

动态嵌套路由

除了固定的嵌套路由结构,Next.js 还支持动态嵌套路由。例如,在一个博客应用中,可能有不同分类的文章,每个分类下又有不同年份的文章列表,再到具体文章页面。

项目结构可能如下:

pages/
├── blog/
│   ├── [category]/
│   │   ├── [year]/
│   │   │   ├── index.js
│   │   │   └── [articleId]/
│   │   │       └── index.js
│   └── index.js
└── index.js

这里 pages/blog/[category]/[year]/index.js 可以展示某个分类下某一年份的文章列表,pages/blog/[category]/[year]/[articleId]/index.js 展示具体文章内容。

pages/blog/[category]/[year]/index.js 中获取动态路由参数:

import React from'react';

const ArticleList = ({ params }) => {
  const { category, year } = params;
  return (
    <div>
      <h1>Articles in {category} - {year}</h1>
      {/* 这里可以根据 category 和 year 获取文章列表并展示 */}
    </div>
  );
};

export default ArticleList;

通过这种动态嵌套路由,可以灵活地构建复杂的应用结构。

嵌套路由与国际化

在国际化应用中,嵌套路由也需要支持多语言。可以通过在路由中添加语言参数来实现。

例如,项目结构如下:

pages/
├── [lang]/
│   ├── products/
│   │   ├── index.js
│   │   └── [productId]/
│   │       ├── index.js
│   │       └── reviews.js
│   └── index.js
└── index.js

pages/[lang]/products/index.js 中:

import React from'react';

const ProductList = ({ params }) => {
  const { lang } = params;
  return (
    <div>
      <h1>{lang === 'en'? 'Product List' : '产品列表'}</h1>
      {/* 这里根据 lang 进行语言相关的内容展示 */}
    </div>
  );
};

export default ProductList;

通过在路由中添加 [lang] 参数,实现不同语言版本的嵌套路由页面展示。同时,可以结合国际化库(如 next - i18next)来更好地管理多语言内容。

综上所述,Next.js 的嵌套路由提供了强大且灵活的功能,通过合理的设计与实现,可以构建出高效、可维护且用户体验良好的前端应用。从基础的文件系统路由创建,到布局共享、导航、数据获取、错误处理、SEO 优化、性能优化以及高级应用,每个环节都相互关联,共同构成了一个完整的应用架构。开发者在实际应用中,应根据项目需求充分利用这些特性,打造出优秀的前端产品。