React Router 通关指南:从入门到鉴权,一个项目全搞定

告别页面白屏,拥抱 SPA 的流畅体验。本文带你从零搭建一个包含嵌套路由、动态路由、懒加载与登录鉴权的完整 React 项目。

写在前面:为什么我们需要前端路由?

在早期的 Web 开发中,路由是由后端控制的。每次点击链接,浏览器都会向服务器发送请求,服务器返回新的 HTML 页面。这个过程会导致页面刷新,出现"白屏"现象,用户体验非常不友好。

随着 前后端分离SPA(单页应用) 架构的兴起,前端路由 应运而生。它的核心思想是:一切皆资源(RESTful 理念) ,通过改变 URL 来切换页面,但不刷新浏览器

  • Hash 模式 (HashRouter) :利用 URL 的 hash#)部分。当 hash 改变时,页面不会重新加载,通过监听 hashchange 事件来渲染对应组件。
  • History 模式 (BrowserRouter) :利用 HTML5 的 History API(pushState, replaceState),使 URL 更加美观(不带 #),但需要后端配置做兜底。

在 React 生态中,react-router-dom 是官方的路由解决方案。今天,我们将通过一个实战 Demo,把路由的核心知识点一次讲透。

一、项目初始化与架构

1. 创建项目与安装依赖

bash

bash 复制代码
# 使用 Vite 快速创建 React 项目
npm create vite@latest react-router-demo -- --template react
cd react-router-demo
# 安装路由核心库
npm i react-router-dom

2. 目录结构解析

为了更好的维护性,我们按功能模块划分 pages 目录:

text

bash 复制代码
src/
├── assets/          # 静态资源
├── components/      # 公共组件
│   └── Navigation.jsx
├── pages/
│   ├── Home/
│   ├── About/
│   ├── Login/
│   ├── Pay/
│   ├── Products/
│   │   ├── Detail/  # 动态路由参数
│   │   └── New/     # 嵌套路由
│   ├── User/
│   │   └── UserProfile/
│   └── NotFound/
├── App.jsx          # 路由配置中枢
├── ProtectRoute.jsx # 高阶鉴权组件
└── main.jsx

二、路由基础配置与懒加载优化

App.jsx 中,我们使用 lazy + Suspense 实现代码分割。这意味着只有用户访问该页面时,对应的 JS 文件才会被加载,大大提升了首屏加载速度。

jsx

javascript 复制代码
import { lazy, Suspense } from 'react';
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';

// 懒加载组件:优化性能,非当前路由不加载
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const UserProfile = lazy(() => import('./pages/UserProfile'));
const Products = lazy(() => import('./pages/Products'));
const ProductDetail = lazy(() => import('./pages/Products/Detail'));
const NewProduct = lazy(() => import('./pages/Products/New'));
const Login = lazy(() => import('./pages/Login'));
const Pay = lazy(() => import('./pages/Pay'));
const NotFound = lazy(() => import('./pages/NotFound'));
// 鉴权组件也需要懒加载(虽然通常不推荐,这里为了演示)
const ProtectRoute = lazy(() => import('./ProtectRoute'));

function App() {
  return (
    <Router>
      <Suspense fallback={<div className="loading">Loading...</div>}>
        <Navigation /> {/* 导航栏组件 */}
        <div id="container">
          <Routes>
            {/* 基础路由:路径与组件一一对应 */}
            <Route path="/" element={<Home />} />
            <Route path="/about" element={<About />} />

            {/* 动态路由:使用 :id 匹配任意值,通过 useParams 获取 */}
            <Route path="/user/:id" element={<UserProfile />} />

            {/* 嵌套路由:父组件定义布局,子组件通过 <Outlet /> 渲染 */}
            <Route path="/products" element={<Products />}>
              <Route path=":productId" element={<ProductDetail />} />
              <Route path="new" element={<NewProduct />} />
            </Route>

            {/* 重定向:旧路径导航到新路径 */}
            <Route path="/old-path" element={<Navigate replace to="/new-path" />} />

            {/* 鉴权路由:保护 /pay 路径 */}
            <Route
              path="/pay"
              element={
                <ProtectRoute>
                  <Pay />
                </ProtectRoute>
              }
            />

            <Route path="/login" element={<Login />} />

            {/* 404 兜底:匹配所有未定义的路径,必须放在最后 */}
            <Route path="*" element={<NotFound />} />
          </Routes>
        </div>
      </Suspense>
    </Router>
  );
}

export default App;

注释解读

  • <Routes> 是路由表,它会根据当前 URL 匹配唯一的 <Route>
  • element 接收组件实例(带标签),而非传入构造函数。
  • * 通配符用于捕获所有未匹配的路径,做 404 页面。

三、核心功能深度解析

1. 嵌套路由与 Outlet

Products 组件中,我们并没有直接展示详情或新增表单,而是通过 <Outlet /> 占位。当 URL 变为 /products/123/products/new 时,子路由组件会渲染在这个出口位置。

jsx

javascript 复制代码
// src/pages/Products/index.jsx
import { Outlet } from 'react-router-dom';

const Products = () => {
  return (
    <>
      <h1>产品列表</h1>
      {/* 二级路由渲染出口:这里会显示 ProductDetail 或 NewProduct */}
      <Outlet />
    </>
  );
};

export default Products;

2. 动态路由与 useParams

/user/:id 这种格式叫动态路由。在子组件中,我们可以使用 useParams Hook 拿到 URL 上的具体值。

jsx

javascript 复制代码
// src/pages/User/UserProfile/index.jsx
import { useParams } from 'react-router-dom';

function UserProfile() {
  // 从 URL 中提取参数,hook 思想:召之即来
  let { id } = useParams();
  console.log('当前用户ID:', id);
  return <h2>UserProfile: {id}</h2>;
}

export default UserProfile;
  • 声明式重定向 :在路由表中使用 <Navigate to="/new-path" replace />
  • 编程式跳转 :在事件处理函数中使用 useNavigate()

以 404 页面为例,3 秒后自动跳回首页:

jsx

javascript 复制代码
// src/pages/NotFound/index.jsx
import { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';

const NotFound = () => {
  let navigate = useNavigate();
  useEffect(() => {
    const timer = setTimeout(() => {
      navigate('/'); // 等价于 window.location.href,但不会刷新页面
    }, 3000);
    return () => clearTimeout(timer);
  }, [navigate]);

  return <>Not Found,3秒后返回首页</>;
};

export default NotFound;

不要使用 <a> 标签,因为 <a> 会触发浏览器刷新。使用 react-router-dom 提供的 <Link><NavLink> 来实现无刷新跳转。

jsx

javascript 复制代码
// src/components/Navigation.jsx
import { Link } from 'react-router-dom';

function Navigation() {
  return (
    <nav>
      <ul>
        <li><Link to="/">Home</Link></li>
        <li><Link to="/about">About</Link></li>
        <li><Link to="/user/123">个人中心</Link></li>
        <li><Link to="/products/123">产品详情</Link></li>
        <li><Link to="/products/new">新增产品</Link></li>
        <li><Link to="/pay">支付(需登录)</Link></li>
      </ul>
    </nav>
  );
}

export default Navigation;

四、鉴权路由(核心实战)

前端鉴权的本质是状态管理 + 条件渲染 。我们利用 localStorage 模拟登录态,结合 Navigate 实现路由拦截。

1. 保护层组件 ProtectRoute

这是一个典型的高阶组件(HOC) ,它接收 children 作为要保护的页面。利用 props.children 的定制性,我们可以像弹窗组件一样嵌套任意内容。

jsx

javascript 复制代码
// src/ProtectRoute.jsx
import { Navigate, useLocation } from 'react-router-dom';

const ProtectRoute = ({ children }) => {
  const location = useLocation(); // 获取当前请求的路径

  // 1. 鉴权逻辑:从 localStorage 读取登录状态
  const isLogin = localStorage.getItem('isLogin') === 'true';
  console.log('鉴权校验:', isLogin);

  if (!isLogin) {
    // 2. 拦截:重定向到登录页,并携带当前路径来源 (from)
    // state 传参:登录后直接跳回来,提升用户体验
    return <Navigate to="/login" replace state={{ from: location.pathname }} />;
  }

  // 3. 放行:渲染被包裹的页面组件
  return <div>{children}</div>;
};

export default ProtectRoute;

2. 登录页逻辑:获取来源并重定向

登录页通过 useLocation 获取 state 中的 from 字段,登录成功后跳转回原页面。注意使用 replace: true 替换历史记录,防止用户通过"后退"按钮回到登录页(小白用户可能会困惑)。

jsx

ini 复制代码
// src/pages/Login/index.jsx
import { useNavigate, useLocation } from 'react-router-dom';

const Login = () => {
  const navigate = useNavigate();
  const location = useLocation();
  // 从 state 中读取来源,若无则默认跳转首页
  const from = location.state?.from || '/';

  function handleSubmit(e) {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    const username = formData.get('username');
    const password = formData.get('password');

    if (username === 'admin' && password === '123456') {
      // 1. 写入登录凭证
      localStorage.setItem('isLogin', 'true');
      // 2. 跳转回来源页,并替换历史记录,防止返回登录页
      navigate(from, { replace: true });
    } else {
      alert('用户名或密码错误!');
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <h1>登录</h1>
      <input name="username" placeholder="用户名: admin" required />
      <input name="password" type="password" placeholder="密码: 123456" required />
      <button type="submit">登录</button>
    </form>
  );
};

export default Login;

五、细节点与最佳实践

  1. children 的妙用ProtectRoute<Model> 弹窗组件都利用了 props.children,这使得组件的定制性极强,可以包裹任意复杂的 UI 结构。

  2. 状态管理

    • 无状态:HTTP 协议本身是无状态的。
    • 有状态 :我们通过 LocalStorage(存 isLogin)和请求头携带 Authorization Token 来模拟状态。
    • 注意 :实际生产环境中,登录态通常由后端颁发的 JWT Token 管理,存储在 CookieLocalStorage 中。
  3. 路由传参方式

    • 动态路由参数/user/:id,使用 useParams 获取。
    • 查询参数 (Query)/pay?orderId=123,使用 useSearchParams 获取。
    • 状态参数 (State)Navigatestate 属性,使用 useLocation 获取(如上文的 from 路径)。

总结

至此,我们完成了一个包含懒加载、嵌套路由、动态路由、编程式导航、鉴权拦截、登录重定向 以及 404 兜底的完整 React 路由项目。

掌握 React Router,不仅仅是学会使用 API,更是理解了单页应用架构的核心思想。希望这篇文章能帮你打通任督二脉,在实际项目中写出更优雅、更健壮的路由代码。

相关推荐
kyson_38 分钟前
如何做一款自适应的数据大屏:`vw/vh`、`rem`、`scale` 应该怎么选?
前端
雪隐43 分钟前
WPF + MVVM 实战系列02-告别 INPC 手写时代,做个体面的现代 WPF 人
前端·后端·c#
肉肉不吃 肉1 小时前
无渲染组件和组合式函数
前端·javascript·vue.js
未来智慧谷1 小时前
从 Atlas 关停复盘:AI 应用的三种载体形态怎么选(独立应用 / 浏览器扩展 / 桌面宿主)
前端·人工智能·ai·架构·浏览器
sugar__salt1 小时前
前端路由技术详解:从传统多页到 React SPA 的完整进化之路
前端·javascript·react.js·前端框架
Canace1 小时前
Harness Engineering 到底在做什么
前端·人工智能·ai编程
用户84298142418101 小时前
JS代码压缩实测:可减小体积、提高执行效率!
前端·javascript·后端
snow@li1 小时前
Vue Axios封装与SpringBoot Payload封装全景关联分析(前后端数据交互底层闭环)
前端·vue.js·spring boot
Csvn1 小时前
🐛 React StrictMode 下 useEffect 执行两次:不是 Bug,是特性
前端