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

React 路由:实现单页应用的导航功能

2021-06-063.5k 阅读

React 路由基础概念

什么是单页应用(SPA)

单页应用(Single - Page Application,SPA)是一种在 Web 浏览器中实现的应用程序模型,它在页面加载后,通过动态更新页面内容而不是重新加载整个页面来响应用户交互。传统的多页应用(MPA)每次用户点击链接都会导致浏览器向服务器请求一个新的 HTML 页面,而 SPA 则不同,它在初始加载时获取一个 HTML 页面和相关的 JavaScript、CSS 资源,后续的页面导航和内容更新通过 JavaScript 动态操作 DOM 来实现。

例如,一个典型的 SPA 可能是一个社交媒体应用。当用户点击不同的标签,如“首页”“好友动态”“个人资料”时,页面并不会重新加载,而是在现有页面的基础上更新相应的内容区域。这种方式极大地提升了用户体验,使得应用的交互更加流畅,仿佛在使用一个原生应用。

React 路由在 SPA 中的作用

在 React 构建的 SPA 中,路由扮演着至关重要的角色。React 路由负责管理应用中的不同视图(组件)与 URL 之间的映射关系。当 URL 发生变化时,React 路由能够根据这个变化,在不重新加载整个页面的情况下,渲染对应的 React 组件。

假设我们有一个简单的博客应用,有首页展示最新文章列表,文章详情页展示具体文章内容。当用户在浏览器地址栏输入不同的 URL,如/ 跳转到首页,/article/123跳转到 id 为 123 的文章详情页,React 路由就负责根据这些 URL 来准确地渲染HomePage组件和ArticlePage组件。

React 路由的核心概念

  1. 路由(Route):定义了一个 URL 路径和一个 React 组件之间的映射关系。例如,路径/about对应AboutPage组件。
  2. 链接(Link):用于创建可点击的导航链接,点击链接会改变 URL,但不会触发页面的完整刷新。
  3. 历史(History):React 路由使用历史对象来跟踪 URL 的变化。它有几种不同的模式,如浏览器历史模式(使用 HTML5 的pushStatepopstate API)和哈希模式(URL 中使用#符号)。

React Router 的安装与基本使用

安装 React Router

React Router 有几个不同的版本,目前较常用的是 React Router v6。可以使用 npm 或 yarn 来安装:

npm install react-router-dom@6
# 或者
yarn add react-router-dom@6

这里的react - router - dom是用于 web 应用的 React Router 库。如果是开发原生移动应用(如 React Native),则需要使用react - router - native

基本路由配置

在 React 应用中,首先要在应用的入口点设置路由。假设我们有一个简单的 React 应用,结构如下:

src/
├── App.js
├── index.js
├── HomePage.js
├── AboutPage.js

index.js中,我们导入BrowserRouter(用于浏览器历史模式)和RoutesRoute等组件:

import React from'react';
import ReactDOM from'react - dom';
import { BrowserRouter as Router, Routes, Route } from'react - router - dom';
import App from './App';
import HomePage from './HomePage';
import AboutPage from './AboutPage';

ReactDOM.render(
  <Router>
    <Routes>
      <Route path="/" element={<HomePage />} />
      <Route path="/about" element={<AboutPage />} />
    </Routes>
  </Router>,
  document.getElementById('root')
);

在上述代码中:

  1. BrowserRouter(这里使用别名Router)提供了路由所需的上下文,它管理着应用的历史记录。
  2. Routes组件用于定义一组路由。
  3. Route组件定义了具体的路径和对应的组件。path属性指定 URL 路径,element属性指定当路径匹配时要渲染的 React 组件。

使用 Link 组件创建导航链接

App.js中,我们可以使用Link组件来创建导航链接:

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

function App() {
  return (
    <div>
      <nav>
        <ul>
          <li>
            <Link to="/">Home</Link>
          </li>
          <li>
            <Link to="/about">About</Link>
          </li>
        </ul>
      </nav>
      {/* 路由渲染的内容将显示在这里 */}
    </div>
  );
}

export default App;

Link组件的to属性指定了链接要跳转的路径。当用户点击Link时,URL 会相应地改变,React Router 会根据新的 URL 渲染对应的组件。

深入 React Router 功能

嵌套路由

在实际应用中,经常会有嵌套的路由结构。例如,一个电商应用可能有产品分类页面,每个分类下又有具体的产品列表和产品详情页。

假设我们有一个Product模块,结构如下:

src/
├── App.js
├── index.js
├── Product/
│   ├── ProductList.js
│   ├── ProductDetail.js
│   ├── ProductCategory.js

首先在index.js中设置外层路由:

import React from'react';
import ReactDOM from'react - dom';
import { BrowserRouter as Router, Routes, Route } from'react - router - dom';
import App from './App';
import ProductCategory from './Product/ProductCategory';

ReactDOM.render(
  <Router>
    <Routes>
      <Route path="/products" element={<ProductCategory />}>
        {/* 这里开始定义嵌套路由 */}
      </Route>
    </Routes>
  </Router>,
  document.getElementById('root')
);

然后在ProductCategory.js中定义嵌套路由:

import React from'react';
import { Routes, Route, Link } from'react - router - dom';
import ProductList from './ProductList';
import ProductDetail from './ProductDetail';

function ProductCategory() {
  return (
    <div>
      <h1>Product Category</h1>
      <nav>
        <ul>
          <li>
            <Link to="list">Product List</Link>
          </li>
          <li>
            <Link to="detail/1">Product Detail</Link>
          </li>
        </ul>
      </nav>
      <Routes>
        <Route path="list" element={<ProductList />} />
        <Route path="detail/:productId" element={<ProductDetail />} />
      </Routes>
    </div>
  );
}

export default ProductCategory;

在上述代码中,ProductCategory组件内部又定义了RoutesRoute。注意Linkto属性,对于嵌套路由,路径可以是相对路径。Routepath属性也使用相对路径,这里detail/:productId中的:productId是一个动态参数,我们可以通过这个参数来获取不同产品的详情。

动态路由参数

动态路由参数允许我们在 URL 中传递动态的值。比如在上述产品详情页的例子中,detail/:productId中的:productId就是动态参数。

ProductDetail.js中,我们可以获取这个参数:

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

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

export default ProductDetail;

useParams是 React Router 提供的一个 Hook,它返回一个包含所有动态参数的对象。通过解构,我们可以轻松获取productId

路由匹配与优先级

React Router v6 使用一种基于路径匹配的算法。当一个 URL 进来时,它会按照Routes中定义的Route顺序进行匹配。例如:

<Routes>
  <Route path="/products/detail/:productId" element={<ProductDetail />} />
  <Route path="/products" element={<ProductList />} />
</Routes>

如果 URL 是/products/detail/123,它会首先匹配到/products/detail/:productId这个路由。如果将顺序颠倒:

<Routes>
  <Route path="/products" element={<ProductList />} />
  <Route path="/products/detail/:productId" element={<ProductDetail />} />
</Routes>

那么/products/detail/123会先匹配到/products,因为/products路径更短,更容易匹配。所以在定义路由时,要注意路径的顺序,确保更具体的路径在前面,更通用的路径在后面。

React Router 的导航控制

使用 navigate 进行编程式导航

除了使用Link组件进行声明式导航,React Router 还提供了编程式导航的方式,即使用navigate函数。

在一个组件中,我们可以通过useNavigate Hook 来获取navigate函数:

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

function SomeComponent() {
  const navigate = useNavigate();

  const handleClick = () => {
    navigate('/about');
  };

  return (
    <div>
      <button onClick={handleClick}>Go to About Page</button>
    </div>
  );
}

export default SomeComponent;

在上述代码中,当按钮被点击时,navigate('/about')会将 URL 导航到/about,从而渲染AboutPage组件。navigate函数还可以接受一些其他参数,比如navigate(-1)可以模拟浏览器的后退操作。

导航守卫

导航守卫可以在导航发生之前或之后执行一些逻辑。在 React Router v6 中,可以通过useNavigateuseLocation等 Hook 来实现类似导航守卫的功能。

例如,假设我们有一个需要用户登录才能访问的页面。我们可以在导航到这个页面之前检查用户是否登录:

import React, { useEffect } from'react';
import { useNavigate, useLocation } from'react - router - dom';

function ProtectedRoute() {
  const navigate = useNavigate();
  const location = useLocation();
  const isLoggedIn = false; // 这里假设一个判断用户登录的逻辑

  useEffect(() => {
    if (!isLoggedIn) {
      navigate('/login', { replace: true, state: { from: location } });
    }
  }, [isLoggedIn, navigate, location]);

  return null;
}

export default ProtectedRoute;

在上述代码中,useEffect会在组件挂载和更新时执行。如果用户未登录,navigate('/login', { replace: true, state: { from: location } })会将用户导航到登录页,并且使用replace选项避免在历史记录中留下当前未授权的页面记录。state中记录了用户原本要去的页面,以便登录后可以重定向回去。

React Router 与数据加载

在路由切换时加载数据

在很多情况下,我们需要在路由切换到某个页面时加载相关的数据。例如,当进入产品详情页时,需要从 API 获取产品的详细信息。

我们可以使用 React 的useEffect Hook 结合路由参数来实现数据加载。以ProductDetail.js为例:

import React, { useEffect, useState } from'react';
import { useParams } from'react - router - dom';

function ProductDetail() {
  const { productId } = useParams();
  const [product, setProduct] = useState(null);

  useEffect(() => {
    const fetchProduct = async () => {
      try {
        const response = await fetch(`https://api.example.com/products/${productId}`);
        const data = await response.json();
        setProduct(data);
      } catch (error) {
        console.error('Error fetching product:', error);
      }
    };

    fetchProduct();
  }, [productId]);

  return (
    <div>
      {product? (
        <div>
          <h1>{product.title}</h1>
          <p>{product.description}</p>
        </div>
      ) : (
        <p>Loading...</p>
      )}
    </div>
  );
}

export default ProductDetail;

在上述代码中,useEffect依赖于productId,当productId变化时(即路由切换到不同产品详情页),会触发fetchProduct函数从 API 获取数据,并更新product状态。

数据预加载

为了提升用户体验,我们可以在用户尚未导航到某个页面时就提前加载数据,这就是数据预加载。

一种实现方式是在Link组件的onMouseEnter事件中触发数据预加载。假设我们有一个产品列表页,列表项链接到产品详情页:

import React, { useState, useEffect } from'react';
import { Link } from'react - router - dom';

function ProductListItem({ product }) {
  const [preloadedData, setPreloadedData] = useState(null);

  const preloadProduct = async () => {
    try {
      const response = await fetch(`https://api.example.com/products/${product.id}`);
      const data = await response.json();
      setPreloadedData(data);
    } catch (error) {
      console.error('Error preloading product:', error);
    }
  };

  return (
    <li>
      <Link to={`/products/detail/${product.id}`} onMouseEnter={preloadProduct}>
        {product.title}
      </Link>
      {/* 如果数据已经预加载,可以在这里做一些优化展示 */}
    </li>
  );
}

export default ProductListItem;

当用户鼠标悬停在链接上时,preloadProduct函数会提前从 API 获取产品详情数据。这样当用户真正点击链接进入详情页时,数据可能已经加载好,减少了等待时间。

React Router 的不同模式

浏览器历史模式

浏览器历史模式是 React Router 默认使用的模式,通过 HTML5 的pushStatepopstate API 来实现 URL 的变化和页面的更新。在前面的例子中,我们使用BrowserRouter就是启用了浏览器历史模式:

import { BrowserRouter as Router } from'react - router - dom';

<Router>
  {/* 路由配置 */}
</Router>

这种模式的优点是 URL 看起来更加美观和自然,没有哈希符号(#)。例如,https://example.com/abouthttps://example.com/#/about更符合用户习惯。但是,使用这种模式需要服务器端的支持,因为当用户直接访问某个深层路径(如https://example.com/products/detail/123)时,服务器需要返回正确的 HTML 页面,否则会出现 404 错误。

哈希模式

哈希模式使用 URL 中的哈希部分(#后面的内容)来管理路由。在 React Router 中,可以使用HashRouter来启用哈希模式:

import { HashRouter as Router } from'react - router - dom';

<Router>
  {/* 路由配置 */}
</Router>

哈希模式的优点是不需要服务器端额外的配置,因为哈希部分不会被发送到服务器。例如,https://example.com/#/about,服务器看到的 URL 始终是https://example.com/。但是,哈希模式的 URL 看起来不够简洁,并且在某些场景下可能会有兼容性问题。

React Router 的性能优化

代码拆分与懒加载路由组件

在大型应用中,为了提高初始加载性能,可以对路由组件进行代码拆分和懒加载。React Router v6 支持使用React.lazySuspense来实现这一功能。

假设我们有一个Dashboard组件,它是一个较大的组件,不希望在应用启动时就加载:

import React, { lazy, Suspense } from'react';
import { Routes, Route } from'react - router - dom';

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

function App() {
  return (
    <Routes>
      <Route path="/dashboard" element={
        <Suspense fallback={<div>Loading...</div>}>
          <Dashboard />
        </Suspense>
      } />
    </Routes>
  );
}

export default App;

在上述代码中,React.lazy(() => import('./Dashboard'))使用动态导入来懒加载Dashboard组件。Suspense组件用于在组件加载时显示一个加载指示器(这里是Loading...)。这样,只有当用户导航到/dashboard路径时,Dashboard组件才会被加载,从而提高了应用的初始加载速度。

避免不必要的重渲染

在 React Router 应用中,要注意避免路由相关组件的不必要重渲染。例如,当路由参数变化时,如果组件没有依赖这些参数,不应该重新渲染。

假设我们有一个NavBar组件,它只负责显示导航链接,不依赖路由参数:

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

const NavBar = React.memo(() => {
  return (
    <nav>
      <ul>
        <li>
          <Link to="/">Home</Link>
        </li>
        <li>
          <Link to="/about">About</Link>
        </li>
      </ul>
    </nav>
  );
});

export default NavBar;

这里使用React.memo来包裹NavBar组件,这样当组件的 props 没有变化时(在这个例子中,NavBar没有接收任何 props),它不会重新渲染,从而提高了性能。

React Router 在实际项目中的应用案例

构建一个博客应用

  1. 路由结构设计
    • 首页:/,展示最新文章列表。
    • 文章详情页:/article/:articleId,展示具体文章内容。
    • 分类页:/category/:categoryName,展示某个分类下的文章列表。
  2. 代码实现
    • index.js中设置路由:
import React from'react';
import ReactDOM from'react - dom';
import { BrowserRouter as Router, Routes, Route } from'react - router - dom';
import App from './App';
import HomePage from './HomePage';
import ArticlePage from './ArticlePage';
import CategoryPage from './CategoryPage';

ReactDOM.render(
  <Router>
    <Routes>
      <Route path="/" element={<HomePage />} />
      <Route path="/article/:articleId" element={<ArticlePage />} />
      <Route path="/category/:categoryName" element={<CategoryPage />} />
    </Routes>
  </Router>,
  document.getElementById('root')
);
  • HomePage.js中,我们可以展示文章列表,并使用Link组件创建到文章详情页和分类页的链接:
import React from'react';
import { Link } from'react - router - dom';

const articles = [
  { id: 1, title: 'Article 1', category: 'Technology' },
  { id: 2, title: 'Article 2', category: 'Lifestyle' }
];

function HomePage() {
  return (
    <div>
      <h1>Home Page</h1>
      <ul>
        {articles.map(article => (
          <li key={article.id}>
            <Link to={`/article/${article.id}`}>{article.title}</Link> - <Link to={`/category/${article.category}`}>{article.category}</Link>
          </li>
        ))}
      </ul>
    </div>
  );
}

export default HomePage;
  • ArticlePage.js中,通过useParams获取articleId,并从 API 加载文章内容:
import React, { useEffect, useState } from'react';
import { useParams } from'react - router - dom';

function ArticlePage() {
  const { articleId } = useParams();
  const [article, setArticle] = useState(null);

  useEffect(() => {
    const fetchArticle = async () => {
      try {
        const response = await fetch(`https://api.example.com/articles/${articleId}`);
        const data = await response.json();
        setArticle(data);
      } catch (error) {
        console.error('Error fetching article:', error);
      }
    };

    fetchArticle();
  }, [articleId]);

  return (
    <div>
      {article? (
        <div>
          <h1>{article.title}</h1>
          <p>{article.content}</p>
        </div>
      ) : (
        <p>Loading...</p>
      )}
    </div>
  );
}

export default ArticlePage;
  • CategoryPage.js类似,通过useParams获取categoryName,并从 API 加载该分类下的文章列表。

企业级应用中的权限控制路由

  1. 需求分析:在企业级应用中,不同角色的用户有不同的权限。例如,管理员可以访问所有页面,普通用户只能访问部分页面。
  2. 实现方案
    • 首先,创建一个AuthRouter组件,用于判断用户权限并控制路由:
import React, { useEffect } from'react';
import { Routes, Route, Navigate, useNavigate, useLocation } from'react - router - dom';
import Dashboard from './Dashboard';
import AdminPanel from './AdminPanel';
import Login from './Login';

const userRole = 'user'; // 这里假设通过某种方式获取用户角色

function AuthRouter() {
  const navigate = useNavigate();
  const location = useLocation();

  useEffect(() => {
    if (userRole === 'user' && location.pathname === '/admin') {
      navigate('/login', { replace: true, state: { from: location } });
    }
  }, [userRole, navigate, location]);

  return (
    <Routes>
      <Route path="/" element={<Dashboard />} />
      {userRole === 'admin' && <Route path="/admin" element={<AdminPanel />} />}
      <Route path="/login" element={<Login />} />
      <Route path="*" element={<Navigate to="/" />} />
    </Routes>
  );
}

export default AuthRouter;
  • index.js中使用AuthRouter
import React from'react';
import ReactDOM from'react - dom';
import { BrowserRouter as Router } from'react - router - dom';
import AuthRouter from './AuthRouter';

ReactDOM.render(
  <Router>
    <AuthRouter />
  </Router>,
  document.getElementById('root')
);

在上述代码中,AuthRouter组件在渲染路由时,会根据userRole判断用户权限。如果普通用户尝试访问/admin路径,会被重定向到登录页。并且只有管理员角色才能看到/admin路径对应的AdminPanel组件。

通过以上对 React 路由的详细介绍,从基础概念到高级应用,再到实际项目案例,希望能帮助开发者全面掌握 React 路由在单页应用导航功能实现中的应用,打造出更加高效、用户体验良好的前端应用。