大家好!欢迎来到 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 一样流畅。
正如项目文档 readme.md 中所说:
前后端分离,前端有独立的 (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 里(虽然我们后面会重构它,但先理解原理)。
一个最基础的路由配置流程如下:
-
编写页面组件:在
src/pages 下写好 Home.jsx, About.jsx 等。
-
引入组件:
import Home from './pages/Home'。
-
配置路径:使用
<Routes> 和 <Route>。
-
跳转页面:使用
<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 里。现在为了项目结构清晰,我们进行了分离:
-
路由配置独立:所有的
Route 定义移到了 src/router/index.jsx 的 RouterConfig 组件中。
-
导航菜单独立:菜单链接移到了
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),或者使用了相对路径。useResolvedPath 和 useMatch 是 React Router 提供的专业工具,能处理各种边缘情况,比手写 location.pathname === to 健壮得多。
📝 总结
今天我们从路由的历史讲起,深入分析了 React Router 的配置、懒加载优化、各种路由类型的实战应用,最后还做了一波代码重构和体验优化。
但这只是 AI 全栈项目的冰山一角!在 readme.md 的技能树中,我们还有:
-
Zustand (状态管理)
-
NestJS (后端开发)
-
LangChain (AI 集成)
- ...
前端路由只是我们构建复杂应用的第一块基石。掌握了它,你就拥有了构建多页面复杂应用骨架的能力。
课后作业:尝试在项目中添加一个新的页面 /dashboard,并为其配置懒加载和路由守卫,看看你能不能独立完成?
我们在下一章见!👋
本文基于 AI Fullstack 课程实战代码编写,不仅是教程,更是实战记录。