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

React 路由中的嵌套路由详解

2024-07-265.3k 阅读

React 路由基础回顾

在深入探讨 React 路由中的嵌套路由之前,先来回顾一下 React 路由的基础概念。React 路由是一个用于在 React 应用中实现页面导航和路由功能的库,最常用的是 react - router - dom,它适用于 Web 应用开发。

在 React 应用中,路由定义了 URL 与组件之间的映射关系。例如,当用户访问 /home 这个 URL 时,应用会渲染 Home 组件。基本的路由配置通常如下:

import React from 'react';
import { BrowserRouter as Router, Routes, Route } from'react - router - dom';
import Home from './components/Home';
import About from './components/About';

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </Router>
  );
}

export default App;

在上述代码中,BrowserRouter 提供了一个基于浏览器历史记录的路由环境。Routes 组件用于定义一组路由,而 Route 组件则定义了具体的路由路径和对应的渲染组件。path 属性指定了 URL 路径,element 属性指定了要渲染的 React 组件。

嵌套路由的概念

嵌套路由指的是在一个路由组件内部,再定义一组子路由。这在实际应用中非常常见,比如一个电商应用的产品详情页面,可能会有不同的子页面,如产品规格、用户评论等。这些子页面就可以通过嵌套路由来实现。

从 URL 角度来看,嵌套路由会使 URL 呈现层级结构。例如,产品详情页面的 URL 可能是 /product/123,而产品规格子页面的 URL 可能是 /product/123/specs,用户评论子页面的 URL 可能是 /product/123/reviews

实现嵌套路由的步骤

  1. 在父路由组件中定义子路由:首先,在父路由对应的组件中,需要引入 RoutesRoute 组件,并定义子路由。假设我们有一个 Product 组件作为父路由组件,代码如下:
import React from'react';
import { Routes, Route } from'react - router - dom';
import ProductSpecs from './ProductSpecs';
import ProductReviews from './ProductReviews';

function Product() {
  return (
    <div>
      <h1>Product Details</h1>
      <Routes>
        <Route path="specs" element={<ProductSpecs />} />
        <Route path="reviews" element={<ProductReviews />} />
      </Routes>
    </div>
  );
}

export default Product;

在这个 Product 组件中,定义了两个子路由,分别对应产品规格和用户评论。注意,这里子路由的 path 并没有写完整的 URL,而是相对路径。

  1. 配置父路由:在应用的主路由配置中,需要配置父路由,使得 Product 组件能够正确渲染。假设主路由配置如下:
import React from'react';
import { BrowserRouter as Router, Routes, Route } from'react - router - dom';
import Home from './components/Home';
import Product from './components/Product';

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/product/:productId" element={<Product />} />
      </Routes>
    </Router>
  );
}

export default App;

这里的 /:productId 是一个动态路由参数,表示产品的 ID。当用户访问 /product/123 这样的 URL 时,Product 组件会被渲染。

  1. 导航到子路由:为了能够导航到子路由,需要在 Product 组件中添加链接。例如:
import React from'react';
import { Link, Routes, Route } from'react - router - dom';
import ProductSpecs from './ProductSpecs';
import ProductReviews from './ProductReviews';

function Product() {
  return (
    <div>
      <h1>Product Details</h1>
      <ul>
        <li><Link to="specs">Product Specs</Link></li>
        <li><Link to="reviews">Product Reviews</Link></li>
      </ul>
      <Routes>
        <Route path="specs" element={<ProductSpecs />} />
        <Route path="reviews" element={<ProductReviews />} />
      </Routes>
    </div>
  );
}

export default Product;

这里使用 Link 组件创建了导航链接,to 属性的值对应子路由的 path

嵌套路由中的匹配原理

在 React 路由中,嵌套路由的匹配是基于路径的。当用户访问一个 URL 时,路由系统会从根路由开始,逐步匹配路由路径。例如,当用户访问 /product/123/specs 时:

  1. 首先,路由系统会匹配到主路由中的 /product/:productId,并渲染 Product 组件。
  2. 然后,在 Product 组件内部,路由系统会继续匹配子路由。由于 URL 中剩余的路径是 /specs,所以会匹配到 Product 组件内部定义的 path="specs" 的子路由,并渲染 ProductSpecs 组件。

嵌套路由的参数传递

  1. 动态参数传递:在父路由中定义的动态参数,在子路由组件中也可以获取到。例如,在上述的 Product 组件及其子路由的场景中,假设 ProductSpecs 组件需要获取产品 ID,可以通过以下方式:
import React from'react';
import { useParams } from'react - router - dom';

function ProductSpecs() {
  const { productId } = useParams();
  return (
    <div>
      <h2>Product Specs for Product {productId}</h2>
      {/* 产品规格内容 */}
    </div>
  );
}

export default ProductSpecs;

这里使用 useParams 钩子函数获取到了父路由中的 productId 参数。

  1. 通过组件属性传递:除了通过动态参数获取数据,还可以在父路由组件渲染子路由组件时,通过属性传递数据。例如,在 Product 组件中:
import React from'react';
import { Routes, Route } from'react - router - dom';
import ProductSpecs from './ProductSpecs';
import ProductReviews from './ProductReviews';

function Product() {
  const productData = {
    name: 'Sample Product',
    description: 'This is a sample product'
  };
  return (
    <div>
      <h1>Product Details</h1>
      <Routes>
        <Route path="specs" element={<ProductSpecs product={productData} />} />
        <Route path="reviews" element={<ProductReviews product={productData} />} />
      </Routes>
    </div>
  );
}

export default Product;

ProductSpecs 组件中就可以通过属性接收数据:

import React from'react';

function ProductSpecs({ product }) {
  return (
    <div>
      <h2>Product Specs for {product.name}</h2>
      <p>{product.description}</p>
      {/* 产品规格内容 */}
    </div>
  );
}

export default ProductSpecs;

嵌套路由与布局

在实际应用中,嵌套路由常常与布局组件结合使用。布局组件可以定义页面的整体结构,如导航栏、侧边栏和内容区域等。例如,一个应用可能有一个 AppLayout 组件作为整体布局:

import React from'react';
import { Outlet } from'react - router - dom';

function AppLayout() {
  return (
    <div>
      <header>
        <h1>My App</h1>
      </header>
      <nav>
        {/* 导航链接 */}
      </nav>
      <main>
        <Outlet />
      </main>
      <footer>
        <p>&copy; 2023 My App</p>
      </footer>
    </div>
  );
}

export default AppLayout;

在主路由配置中,可以将 AppLayout 作为父组件,然后在其内部定义子路由:

import React from'react';
import { BrowserRouter as Router, Routes, Route } from'react - router - dom';
import AppLayout from './components/AppLayout';
import Home from './components/Home';
import Product from './components/Product';

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<AppLayout />}>
          <Route index element={<Home />} />
          <Route path="product/:productId" element={<Product />} />
        </Route>
      </Routes>
    </Router>
  );
}

export default App;

这里的 Outlet 组件用于渲染子路由对应的组件。例如,当访问 / 时,Home 组件会渲染在 AppLayoutOutlet 位置;当访问 /product/123 时,Product 组件会渲染在 Outlet 位置。

嵌套路由中的嵌套布局

在一些复杂的应用中,可能会出现嵌套布局的情况。例如,在 Product 组件内部,可能也有自己的布局,比如左侧显示产品图片,右侧显示产品详情和子路由内容。

首先,定义一个 ProductLayout 组件:

import React from'react';
import { Outlet } from'react - router - dom';

function ProductLayout() {
  return (
    <div style={{ display: 'flex' }}>
      <div style={{ width: '30%' }}>
        {/* 产品图片 */}
      </div>
      <div style={{ width: '70%' }}>
        <Outlet />
      </div>
    </div>
  );
}

export default ProductLayout;

然后,在 Product 组件中使用这个布局组件:

import React from'react';
import { Routes, Route } from'react - router - dom';
import ProductLayout from './ProductLayout';
import ProductSpecs from './ProductSpecs';
import ProductReviews from './ProductReviews';

function Product() {
  return (
    <ProductLayout>
      <Routes>
        <Route path="specs" element={<ProductSpecs />} />
        <Route path="reviews" element={<ProductReviews />} />
      </Routes>
    </ProductLayout>
  );
}

export default Product;

这样,当访问产品的子路由时,如 /product/123/specsProductSpecs 组件会渲染在 ProductLayoutOutlet 位置,实现了嵌套布局。

处理嵌套路由中的 404 页面

在嵌套路由中,同样需要处理用户访问不存在的路径的情况,即 404 页面。在 React 路由中,可以通过定义一个 path="*" 的路由来实现。

例如,在主路由配置中:

import React from'react';
import { BrowserRouter as Router, Routes, Route } from'react - router - dom';
import AppLayout from './components/AppLayout';
import Home from './components/Home';
import Product from './components/Product';
import NotFound from './components/NotFound';

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<AppLayout />}>
          <Route index element={<Home />} />
          <Route path="product/:productId" element={<Product />} />
          <Route path="*" element={<NotFound />} />
        </Route>
      </Routes>
    </Router>
  );
}

export default App;

当用户访问一个不存在的路径时,NotFound 组件会被渲染。在嵌套路由内部,也可以类似地定义 path="*" 的子路由来处理子路由中的 404 情况。例如,在 Product 组件中:

import React from'react';
import { Routes, Route } from'react - router - dom';
import ProductLayout from './ProductLayout';
import ProductSpecs from './ProductSpecs';
import ProductReviews from './ProductReviews';
import ProductNotFound from './ProductNotFound';

function Product() {
  return (
    <ProductLayout>
      <Routes>
        <Route path="specs" element={<ProductSpecs />} />
        <Route path="reviews" element={<ProductReviews />} />
        <Route path="*" element={<ProductNotFound />} />
      </Routes>
    </ProductLayout>
  );
}

export default Product;

这样,当用户访问 /product/123/unknown 时,ProductNotFound 组件会被渲染,提供了更细致的 404 处理。

嵌套路由与导航守卫

导航守卫在 React 路由中用于在导航发生前后执行一些逻辑,比如验证用户是否登录、检查权限等。在嵌套路由中,导航守卫同样适用。

react - router - dom v6 中,可以使用 useNavigateuseLocation 钩子函数来实现类似导航守卫的功能。例如,假设需要在用户访问产品评论子路由前,检查用户是否登录:

import React from'react';
import { Link, Routes, Route, useNavigate, useLocation } from'react - router - dom';
import ProductSpecs from './ProductSpecs';
import ProductReviews from './ProductReviews';
import Login from './Login';

function Product() {
  const navigate = useNavigate();
  const location = useLocation();
  const isLoggedIn = true; // 假设这里有实际的登录检查逻辑

  if (location.pathname.includes('reviews') &&!isLoggedIn) {
    navigate('/login');
  }

  return (
    <div>
      <h1>Product Details</h1>
      <ul>
        <li><Link to="specs">Product Specs</Link></li>
        <li><Link to="reviews">Product Reviews</Link></li>
      </ul>
      <Routes>
        <Route path="specs" element={<ProductSpecs />} />
        <Route path="reviews" element={<ProductReviews />} />
      </Routes>
    </div>
  );
}

export default Product;

在上述代码中,通过 useNavigate 可以进行导航操作,useLocation 可以获取当前的 URL 信息。当用户尝试访问产品评论子路由且未登录时,会被导航到登录页面。

嵌套路由在大型项目中的应用场景

  1. 电子商务应用:如前文提到的,产品详情页面可以使用嵌套路由展示不同的子内容,如产品规格、用户评论、相关推荐等。此外,在订单流程中,也可以使用嵌套路由,例如 /order/123/step1/order/123/step2 等,每个步骤对应一个子路由组件,实现订单流程的分步展示和操作。
  2. 企业级应用:在企业级应用中,模块通常具有层级结构。例如,一个项目管理应用可能有项目详情页面,在项目详情页面中,又有任务列表、文档管理、团队成员等子页面,这些都可以通过嵌套路由来实现。同时,嵌套路由可以结合权限管理,不同权限的用户看到的子路由内容不同。
  3. 多语言和多主题应用:对于多语言和多主题的应用,嵌套路由可以用于处理不同语言和主题下的页面结构。例如,/en/home/zh/home 可以是不同语言版本的首页,而在每个语言版本内部,又可以有嵌套的子路由,并且可以根据主题切换不同的样式。

嵌套路由的性能优化

  1. 代码拆分:在嵌套路由组件较多的情况下,使用代码拆分可以提高应用的加载性能。例如,可以使用 React.lazy 和 Suspense 来实现组件的懒加载。假设 ProductSpecsProductReviews 组件比较大,可以这样处理:
import React, { lazy, Suspense } from'react';
import { Routes, Route } from'react - router - dom';

const ProductSpecs = lazy(() => import('./ProductSpecs'));
const ProductReviews = lazy(() => import('./ProductReviews'));

function Product() {
  return (
    <div>
      <h1>Product Details</h1>
      <Routes>
        <Route path="specs">
          <Suspense fallback={<div>Loading...</div>}>
            <Route index element={<ProductSpecs />} />
          </Suspense>
        </Route>
        <Route path="reviews">
          <Suspense fallback={<div>Loading...</div>}>
            <Route index element={<ProductReviews />} />
          </Suspense>
        </Route>
      </Routes>
    </div>
  );
}

export default Product;

这样,只有在用户访问到对应的子路由时,相关组件才会被加载,减少了初始加载的代码量。

  1. 避免不必要的渲染:使用 React.memo 或 useMemo 来避免组件的不必要渲染。例如,如果 ProductSpecs 组件的渲染依赖于父组件传递的某个属性,且该属性在组件更新时没有变化,可以使用 React.memo 来优化:
import React from'react';

const ProductSpecs = React.memo(({ product }) => {
  return (
    <div>
      <h2>Product Specs for {product.name}</h2>
      <p>{product.description}</p>
      {/* 产品规格内容 */}
    </div>
  );
});

export default ProductSpecs;

这样,当 product 属性没有变化时,ProductSpecs 组件不会重新渲染,提高了性能。

嵌套路由的常见问题及解决方法

  1. 路由路径冲突:在定义嵌套路由时,可能会出现路径冲突的问题。例如,在父路由和子路由中定义了相同的路径。解决方法是仔细检查路由路径,确保路径的唯一性。同时,可以使用相对路径来定义子路由,避免与父路由路径产生混淆。
  2. 导航跳转异常:有时在嵌套路由中进行导航跳转时,可能会出现跳转到错误页面或跳转不生效的情况。这通常是由于路径配置错误或导航方法使用不当导致的。检查导航链接的 to 属性是否正确,以及路由配置是否与预期一致。如果使用 useNavigate 进行编程式导航,确保正确传入了目标路径。
  3. 嵌套层次过深:在一些复杂应用中,可能会出现嵌套路由层次过深的情况,这会导致代码难以维护和理解。可以通过合理的组件拆分和路由设计来减少嵌套层次。例如,将一些相关的子路由组件合并到一个更高层次的组件中,或者使用动态路由加载来简化路由结构。

总结

React 路由中的嵌套路由是构建复杂应用的重要技术,通过合理的使用嵌套路由,可以实现清晰的页面层级结构和丰富的功能。在实际应用中,需要注意路由的配置、参数传递、布局设计、性能优化以及常见问题的解决。掌握嵌套路由技术,能够使 React 应用的开发更加高效和灵活,满足各种业务需求。通过上述详细的讲解和代码示例,希望读者能够深入理解并熟练运用 React 路由中的嵌套路由。