最近在啃 React 的全家桶,学到 react-router-dom (v6) 这块时,感觉概念稍微有点多。以前只知道写静态页面(切图),现在接触单页应用(SPA),发现前端竟然也能自己控制路由跳转,不用每次都求着后端改 URL,感觉打开了新世界的大门!
为了防止自己学了忘,我把这几天的学习心得、代码实现和踩过的坑整理了一下,希望能帮到跟我一样的人。
一、 为什么要用前端路由?
以前写传统网页(多页应用)时,每次点个链接,浏览器都要去找服务器重新请求这一页的 HTML,页面会“白”一下,体验不太连贯。
现在的单页应用(SPA),页面加载完后,路由切换全归前端管:
- URL 变了。
- 页面不刷新。
- JS 监听到变化,把对应的组件渲染到页面上。
这就是 react-router-dom 帮我们干的事。
二、 两种路由模式:选谁?
在 index.js 或者 App.js 里包裹路由时,我有两个选择,特意查了一下区别:
-
HashRouter (
#/)- 样子:URL 后面带着
#,比如localhost:3000/#/home。 - 评价:虽然丑了点(像个锚点),但是它性格“温柔” ,兼容性贼好,老浏览器也能跑,部署上线时不需要后端配合,一般不会 404。
- 样子:URL 后面带着
-
BrowserRouter (
/)- 样子:
localhost:3000/home,长得跟后端路由一样,正规、漂亮。 - 评价:它是基于 HTML5 History API 的。现在主流都用这个,但如果上线,需要后端配置一下(把所有请求都指向 index.html),否则刷新页面可能会 404。
- 样子:
我的选择:作为“颜控”,我用了 BrowserRouter,并且为了代码可读性,给它起了个别名 Router。
JavaScript
// App.js
import { BrowserRouter as Router } from 'react-router-dom';
import RouterConfig from './router';
// ...其他引入
export default function App() {
return (
<Router>
<Navigation /> {/* 导航栏放在这里,切换路由时它不动 */}
<RouterConfig /> {/* 这里是变的区域 */}
</Router>
)
}
三、 路由懒加载
刚开始学的时候,我把所有组件都 import 进来了。后来发现,如果页面很多,第一次打开网页会加载巨慢。
于是我学到了 懒加载 (Lazy Load) :不到那个页面,就不加载那个 JS 文件。
这里要配合 React 的 Suspense 组件,不然网络慢的时候页面空着会报错。我还特意手写了个 CSS 动画的 LoadingFallback 组件(代码在最后),让等待过程稍微优雅一点。
JavaScript
// router/index.js
import { lazy, Suspense } from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import LoadingFallback from '../components/LoadingFallback';
// 这里的 import 要放在 lazy 里
const Home = lazy(() => import('../pages/Home'));
const About = lazy(() => import('../pages/About'));
const UserProfile = lazy(() => import('../pages/UserProfile'));
// ...
export default function RouterConfig() {
return (
<Suspense fallback={<LoadingFallback/>}>
<Routes>
{/* 具体的路由规则写在下面 */}
</Routes>
</Suspense>
)
}
四、 几种常见的路由写法
在 Routes 里面配置规则,感觉就像是在搭积木。整理了几个我常用的场景:
1. 动态路由 (怎么传参?)
比如用户详情页,每个人 ID 不一样,不能写死。
- 配置:用
:id占位。 - 获取:用
useParams钩子。
JavaScript
{/* 路由配置 */}
<Route path="/user/:id" element={<UserProfile />} />
JavaScript
// UserProfile.js 组件内部
import { useParams } from 'react-router-dom';
export default function UserProfile() {
const { id } = useParams(); // 拿到路由里的参数
return <>用户ID是: {id}</>;
}
2. 嵌套路由
这一块我理解了好久。比如“产品页”,它下面有“产品详情”、“新增产品”。它们应该共用一个父级的布局(比如都有个产品列表的侧边栏)。
关键点:父组件里要写一个 <Outlet />,子路由的内容就会显示在 <Outlet /> 的位置。
JavaScript
{/* 父路由 */}
<Route path="/products" element={<Product/>}>
{/* 子路由1:/products/new */}
<Route path="new" element={<NewProduct />}/>
{/* 子路由2:/products/123 */}
<Route path=":productId" element={<ProductDetail />}/>
</Route>
3. 重定向与 404
以前好像是用 Redirect,v6 变成了 <Navigate />。
JavaScript
{/* 访问旧路径,自动跳到新路径 */}
<Route path="/old-path" element={<Navigate replace to="/new-path"/>}/>
{/* 通配符 *,匹配不到的都去 404 页面 */}
<Route path="*" element={<NotFound />}/>
五、 路由守卫:做一个鉴权组件
很多页面(比如支付页)是不能随便进的,必须登录。我写了一个高阶组件 ProtectRoute 来包裹这些页面。
逻辑很简单:判断 localStorage 里有没有登录标记。如果没有,就强制跳回登录页。
JavaScript
// components/ProtectRoute.js
import { Navigate } from 'react-router-dom';
export default function ProtectRoute({ children }) {
const isLoggedIn = localStorage.getItem('isLogin') === 'true';
if (!isLoggedIn) {
// 没登录?踢回登录页
return <Navigate to="/login"/>;
}
// 登录了?正常显示
return <>{children}</>;
}
使用的时候就这样包一下:
JavaScript
<Route path="/pay" element={
<ProtectRoute>
<Pay />
</ProtectRoute>
}/>
六、 进阶:手写导航高亮
虽然官方有 NavLink,但我为了练习 Hooks,试着自己写了一个判断高亮的逻辑。用到了 useResolvedPath 和 useMatch。
这样我可以精确控制:是“完全匹配”才高亮,还是只要“包含路径”就高亮。
JavaScript
// Navigation.js
const isActive = (to) => {
const resolvedPath = useResolvedPath(to);
// end: true 代表要精确匹配
const match = useMatch({ path: resolvedPath.pathname, end: true });
return match ? 'active' : ''; // 返回 class 名
}
效果
七、 有意思的小功能
1. 404 页面自动跳转
在 NotFound 页面,我加了个定时器,6秒后自动把用户送回首页。这里用到了 useNavigate。
JavaScript
let navigate = useNavigate();
useEffect(() => {
// 6秒后回家
const timer = setTimeout(() => {
navigate('/');
}, 6000);
return () => clearTimeout(timer); // 记得清除定时器,防止内存泄漏
}, [navigate]);
2. 纯 CSS 的 Loading 动画
为了配合懒加载,我整了个 CSS Module 的动画,看着还挺像模像样的(代码就不全贴了,主要是用了 @keyframes 做旋转)。
学习总结
折腾下来,对 React Router 算是更加了解了。
- 单页应用体验确实好,但也需要处理加载性能问题(懒加载)。
- v6 版本的变化还是挺大的,比如
Switch变成了Routes,Redirect变成了Maps,感觉写法上更直观了。 - Outlet 真的是嵌套路由的神器。