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

103 阅读7分钟

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

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

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

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

一、项目初始化与架构

1. 创建项目与安装依赖

bash

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

2. 目录结构解析

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

text

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

二、HashRouter 与 BrowserRouter:生产环境如何选?

在 React 中,react-router-dom 提供了两种路由模式。虽然它们都能实现 SPA,但底层原理和使用场景截然不同。很多新手在这里选错,导致后续部署踩坑。

特性HashRouterBrowserRouter
实现原理监听 window.hashchange 事件利用 HTML5 History API (pushState/replaceState)
URL 形态https://domain.com/#/pay(带 #,略显丑陋)https://domain.com/pay(干净美观,符合 RESTful)
服务端配置不需要,直接打开 HTML 即可运行需要配置 Nginx/Apache 重写规则,防止刷新 404
SEO 友好度较差(# 后的内容通常不被搜索引擎爬取)良好(利于搜索引擎优化)
兼容性兼容所有浏览器(包括老 IE)需现代浏览器(IE10+)

代码实现(一键换壳)

两种模式在代码层面的切换极其简单——只需修改包裹根组件的顶层容器,内部的所有 <Routes>、<Link>、useNavigate 等逻辑完全不用动。

jsx

// 方案一:Hash 模式(省心,适合 B 端内部管理系统)
import { HashRouter as Router } from 'react-router-dom';

// 方案二:History 模式(优雅,适合 C 端官网/营销页)
import { BrowserRouter as Router } from 'react-router-dom';

function App() {
  return (
    <Router>
      {/* 其余路由配置完全一致 */}
    </Router>
  );
}

避坑指南(生产环境必看)

  • 选 HashRouter:适合后台管理系统、内部工具(对 SEO 无要求,懒得折腾 Nginx)。缺点是 URL 带 #,分享链接时稍显不专业。
  • 选 BrowserRouter:适合面向用户的官网、博客、电商前台。切记,打包部署后,必须让运维同学配置 Nginx 的 try_files 重写规则:

nginx

location / {
  try_files $uri $uri/ /index.html;
}

否则,用户在 /pay 页面刷新时,服务器会去查找 pay 这个资源文件,因为不存在而返回 404 Not Found。


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

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

jsx

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

// 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

// 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;

3. Navigate 与 useNavigate 编程式跳转

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

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

jsx

// 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;

4. 导航栏组件:Link 的使用

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

jsx

// 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

// 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

// 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 管理,存储在 Cookie 或 LocalStorage 中。
  3. 路由传参方式:

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

总结

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

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