🚀 AI 全栈项目第一天:解锁 React 路由的“时空穿梭”术

224 阅读10分钟

大家好!欢迎来到 AI 全栈项目实战 的第一天。

在现代前端开发中,如果我们把 React 组件比作一个个独立的“平行宇宙”(页面),那么 React Router 就是连接这些宇宙的“虫洞”。没有它,我们只能在一个孤岛上打转;有了它,用户才能在不同的功能模块间自由穿梭。

今天,我们就结合项目代码,像剥洋葱一样,一层层揭开 React Router 的神秘面纱。准备好了吗?我们要发车了!🚗


⏳ 一、 前端路由的前世今生:从“切图仔”到“架构师”

在很久很久以前(其实也就十几年前),Web 开发的世界还是一片蛮荒之地。

1. 后端路由时代

那时候,路由的大权掌握在后端手里(PHP, JSP, ASP)。

  • 用户点击一个链接 -> 浏览器向服务器发送请求 -> 服务器拼接好完整的 HTML -> 返回给浏览器 -> 页面白屏刷新 -> 显示新内容。
  • 缺点:每次跳转页面都要刷新,体验就像看 PPT 时每翻一页都要黑屏一秒,非常“卡顿”。那时候的前端主要负责写 HTML/CSS,被戏称为“切图仔”。

2. 前端路由时代 (SPA)

随着 Ajax 的普及和 React/Vue 的崛起,单页应用 (SPA - Single Page Application) 诞生了。

  • 核心魔法:页面初始化时加载一次 HTML,之后的跳转不再请求整个页面,而是通过 JS 感知 URL 的变化,动态地把原本的 DOM 树“拆掉”,换上新的组件。
  • 体验:丝般顺滑,像原生 App 一样流畅。

前后端分离,前端有独立的 (html5) 路由,实现页面切换。前端会收到一个事件,将匹配的新路由显示在页面上。


⚔️ 二、 路由界的“红白玫瑰”:BrowserRouter vs HashRouter

在 React Router 中,有两种最常见的路由模式,它们就像两兄弟,性格迥异但各有所长。

1. BrowserRouter (HTML5 History API) 🌹

  • 长相http://example.com/product/123
  • 性格:优雅、漂亮、现代。它利用 HTML5 的 history.pushState API 来改变 URL 而不刷新页面。
  • 缺点:它比较“娇气”。如果你在二级页面刷新浏览器,服务器会以为你要请求这个路径的资源,结果找不到(404)。这需要后端(Nginx/Apache)配合,把所有请求都重定向回 index.html

2. HashRouter (Hash模式) 🏳️

  • 长相http://example.com/#/product/123
  • 性格:老实、可靠、兼容性强。URL 里带个 # 号(锚点)。
  • 优点# 后面的内容不会发送给服务器,所以随便刷新都不会 404。非常适合放在 GitHub Pages 或者没有后端配置权限的场景。
  • 缺点:URL 稍微丑了那么一点点。

💡 一个提升逼格的小技巧

观察下面代码:

import {
  BrowserRouter as Router, // ✨ 这里的重命名是点睛之笔
} from 'react-router-dom';

export default function App() {
  return (
    // 以后想换成 HashRouter,只需要改上面的 import,这里不用动
    <Router>
      {/* ... */}
    </Router>
  )
}

使用 as Router 进行重命名,不仅让代码语义更通顺(我们在使用“路由”,而不是具体的“浏览器路由”),还方便未来在两种模式间无缝切换。


🛠️ 三、 路由配置初体验:搭建骨架

在这个阶段,我们通常会将所有的逻辑写在 App.jsx 里(虽然我们后面会重构它,但先理解原理)。

一个最基础的路由配置流程如下:

  1. 编写页面组件:在 src/pages 下写好 Home.jsx, About.jsx 等。
  2. 引入组件import Home from './pages/Home'
  3. 配置路径:使用 <Routes><Route>
  4. 跳转页面:使用<Link to "/**"> 或者 <Navigate to "/**">
// 伪代码演示初级阶段
<Router>
 <Link to="/">Home</Link>
<Routes>
  <Route path="/" element={<Home />} />
  <Route path="/about" element={<About />} />
</Routes>
<Router>

注意Link,Routes与Route等一定要包裹在Router里面才能运行,因为它们都要依赖Router的Context机制。

🤔Link or? Navigate

<Link>navigate(通过 useNavigate() 获取)都用于实现页面导航,但使用方式和场景有所不同。<Link> 是一个声明式的 JSX 组件,通常用于页面中的可点击链接(如导航栏),其行为类似于 HTML 的 <a> 标签;而 navigate 是一个编程式导航函数,适用于在事件处理、表单提交、权限校验或副作用逻辑中动态跳转页面(例如登录成功后自动跳转)。两者都依赖于 <Router> 提供的上下文环境,并支持传递状态、替换历史记录等高级功能,其中 navigate 还能实现返回上一页(如 navigate(-1))等操作。简言之,<Link> 适合用户主动触发的静态跳转,navigate 则更适合由代码逻辑驱动的动态跳转。

但是!随着项目变大,比如我们这个 AI 全栈项目,包含了 UserProfile, Product, Login, Pay 等等十几个页面。如果我们都在文件顶部 import 进来...

🛑 问题出现了: 用户只是想打开首页看一眼,结果浏览器把“支付页”、“后台管理页”的代码全下载下来了。首屏加载时间直接爆炸,用户体验极差。


⚡ 四、 性能救星:懒加载 (Lazy Loading)

为了解决上面的问题,React Router 配合 React 官方推出了“懒路由”方案。只有当用户真正点击了某个路由,才去加载对应的代码文件。

我们来看看如何优雅地处理这个问题:

1. 引入两兄弟:lazy 和 Suspense

import {
  lazy, // 😴 懒加载函数
  Suspense // ⏳ 悬念/等待组件
} from 'react';

2. 改造 Import 方式

不再是静态引入,而是动态引入:

// ❌ 以前:import Home from '../pages/Home'
// ✅ 现在:
const Home = lazy(() => import('../pages/Home')); 
const About = lazy(() => import('../pages/About'));
const Product = lazy(() => import('../pages/product'));
// ... 其他组件同理

3.包裹路由路径配置

lazy 依赖 Suspense 是因为懒加载本质上是异步的,而 React 需要 Suspense 来优雅地处理加载中的状态,避免白屏或崩溃,并提供良好的用户体验。两者配合,实现了代码分割与平滑加载的现代前端优化模式。

 <Suspense fallback={<LoadingFallback />}>
    <Routes>
      <Route path="/" element={<Home />} />
    </Routes>
 </Suspense>

这样,Webpack/Vite 打包时,会把每个页面拆分成独立的 chunk.js 文件,实现按需加载


📸 五、 路由“全家福”:五种路由形态解析

接下来是本文的硬核部分。在 src/router/index.jsx 中,我们几乎涵盖了 React Router 的所有用法。让我们结合 readme.md 里的知识点一一解析。

1. 普通路由

最简单的映射关系,一一对应。

<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />

2. 动态路由 (Dynamic Routing) 🆔

当路径中有一部分是变化的,比如用户 ID、商品 ID。

{/* http://domain/user/12345 */}
<Route path="/user/:id" element={<UserProfile />} />

UserProfile 组件内部,我们可以通过 useParams() 钩子拿到这个 id

3. 嵌套路由 (Nested Routing) 👨‍👦

这是 React Router 最强大的功能之一。比如商品模块,有列表页,有详情页,有新增页,它们可能共享一套布局(比如侧边栏)。

知识点

<Outlet> 是 React Router DOM 中的组件,用于在父路由元素中渲染其子路由匹配到的内容。

代码实战

<Route path="/products" element={<Product/>}>
  {/* 当访问 /products/new 时,渲染 NewProduct */}
  <Route path="new" element={<NewProduct />}/>
  
  {/* 当访问 /products/123 时,渲染 ProductDetail */}
  <Route path=":productId" element={<ProductDetail />}/>
</Route>

在父组件 Product 中,必须写上 <Outlet />,子路由的内容就会填入那个位置。

4. 鉴权路由 (Protected Route) 🛡️

有些页面(如支付页)是不能随便进的,必须登录。我们需要一个“保安”。

const ProtectRoute = lazy(() => import('../components/ProtectRoute'));

// ...

<Route path="/pay" element={
  {/* 💡 这里的逻辑是想看 Pay先过 ProtectRoute 这一关 */}
  <ProtectRoute>
    <Pay />
  </ProtectRoute>
}>
</Route>

ProtectRoute组件代码:

import { 
    Navigate
 } from 'react-router-dom';

export default function ProtectRoute({ children }) {// 组件包裹的内容就是children
    const isLoggedIn = localStorage.getItem('isLogin') === 'true';// 本地存储了登录状态
    if (!isLoggedIn) {
        return <Navigate to="/login" />
    }
    return (
        <>
            {children}
        </>
    )
}

ProtectRoute 组件内部通常会检查 Token,如果没有登录,直接用 <Navigate to="/login" /> 把用户踢到登录页。

5. 重定向路由 (Redirect) ➡️

随着版本迭代,旧的路径可能废弃了,但不能让老用户迷路。

{/* 访问 /old-path 自动跳转到 /new-path,replace 表示替换当前历史记录 */}
<Route path="/old-path" element={<Navigate replace to="/new-path" />}/>

6. 通配路由 (Wildcard) 4️⃣0️⃣4️⃣

兜底方案,当上面的路由都没匹配上时,显示 404。

<Route path="*" element={<NotFound />} />

🎨 六、 极致的用户体验:LoadingFallback

既然用了懒加载,网络请求是需要时间的。在组件下载下来之前,页面不仅不能白屏,还得给用户一点反馈。

这就轮到 Suspense 出场了,它包裹在 <Routes> 外层:

<Suspense fallback={<LoadingFallback />}>
  <Routes>
     {/* ...路由配置... */}
  </Routes>
</Suspense>

我们可以写一个炫酷的 CSS 动画转圈圈。这一个小小的细节,能让应用的质感提升一个档次。 LoadingFallback代码

关于module_css可以看🎨 CSS 这种“烂大街”的技术,怎么在 React 和 Vue 里玩出花来?

import styles from './index.module.css'

export default function LoadingFallback() {
    return (
        <div className={styles.container}>
            <div className={styles.spinner}>
                <div className={styles.circle}></div>
                <div className={`${styles.circle} ${styles.inner}`}></div>//设置两个className
            </div>
            <p className={styles.text}>Loading...</p>
        </div>
    )
}
.container {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100vh;
  background-color: rgba(255, 255, 255, 0.9);
}
.spinner {
  position: relative;
  width: 60px;
  height: 60px;
}
.circle {
  position: absolute;
  width: 100%;
  height: 100%;
  border: 4px solid transparent;
  border-top-color: #3498db;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}
.circle.inner {
  width: 70%;
  height: 70%;
  top: 15%;
  left: 15%;
  border-top-color: #e74c3c;
  animation: spin 0.8s linear infinite reverse;
}
/* 关键帧动画 */
@keyframes spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

.text {
  margin-top: 20px;
  color: #2c3e50;
  font-size: 18px;
  font-weight: 500;
  animation: pulse 1.5s ease-in-out infinite;
}

@keyframes pulse {
  0% {
    opacity: 0.6;
  }
  50% {
    opacity: 1;
  }
  100% {
    opacity: 0.6;
  }
}

🧹 七、 代码重构:各司其职

最开始我们可能把所有代码都堆在 App.jsx 里。现在为了项目结构清晰,我们进行了分离:

  1. 路由配置独立:所有的 Route 定义移到了 src/router/index.jsxRouterConfig 组件中。
  2. 导航菜单独立:菜单链接移到了 src/components/Navigation.jsx

现在的 App.jsx 简直清爽得令人感动:

// src/App.jsx
import { BrowserRouter as Router } from 'react-router-dom';
import Navigation from './components/Navigation';
import RouterConfig from './router';

export default function App() {
  return (
    <Router>
      <Navigation />   {/* 顶部导航 */}
      <RouterConfig /> {/* 路由内容渲染区 */}
    </Router>
  )
}

这就是关注点分离(Separation of Concerns)的美学!


🎯 八、 锦上添花:高亮当前菜单

最后,我们还要解决一个痛点:用户怎么知道自己当前在哪个页面? 导航栏对应的菜单项应该高亮显示(变红)。

我们来实现一个高级的 isActive 判断逻辑:

import { useResolvedPath, useMatch } from 'react-router-dom';

const isActive = (to) => {
    // 1. 解析目标路径,处理相对路径等情况,得到标准的 location 对象
    const resolvedPath = useResolvedPath(to); 
    
    // 2. 使用 useMatch 进行严格匹配
    // path: 当前浏览器地址栏的 pathname
    // end: true 表示精确匹配(比如 /about 不会匹配 /about/me)
    const match = useMatch({
        path: resolvedPath.pathname,
        end: true
    })
    
    // 3. 匹配上了就返回 'active' 类名
    return match ? 'active' : '';
}

为什么不用简单的字符串比较? 因为路由可能是复杂的(比如带有查询参数、Hash),或者使用了相对路径。useResolvedPathuseMatch 是 React Router 提供的专业工具,能处理各种边缘情况,比手写 location.pathname === to 健壮得多。


📝 总结

今天我们从路由的历史讲起,深入分析了 React Router 的配置、懒加载优化、各种路由类型的实战应用,最后还做了一波代码重构和体验优化。

但这只是 AI 全栈项目的冰山一角!在 readme.md 的技能树中,我们还有:

  • Zustand (状态管理)
  • NestJS (后端开发)
  • LangChain (AI 集成)
  • ...

前端路由只是我们构建复杂应用的第一块基石。掌握了它,你就拥有了构建多页面复杂应用骨架的能力。

课后作业:尝试在项目中添加一个新的页面 /dashboard,并为其配置懒加载和路由守卫,看看你能不能独立完成?

我们在下一章见!👋


本文基于 AI Fullstack 课程实战代码编写,不仅是教程,更是实战记录。