从前端路由的起源到 React Router 实战
一、传统的多页面时代
在互联网的早期,万物互联靠的是链接。
ini
<a href="http://127.0.0.1:5500/lj-ai/fe/history/demo/index.html">首页</a>
<a href="http://127.0.0.1:5500/lj-ai/fe/history/demo/about.html">关于我们</a>
点击一个链接,浏览器的工作流程是这样的:
- URL 作为浏览器访问代理,向服务器发起 HTTP 请求
- 服务器处于伺服状态 ,收到请求后返回
text/html响应 - 浏览器拿到响应数据,重新渲染整个页面
- 浏览器历史记录中插入一条记录
这就是传统的多页面应用。每次点击链接,浏览器都会经历"白一下"的过程------尤其网速慢的时候,整个页面重新渲染的体验非常糟糕。
到了移动互联网时代,用户已经习惯了 App 那种流畅的体验,传统多页面的"全量刷新"模式显得越来越没有必要。
二、单页应用(SPA)的诞生
核心思想很简单:怎么把丰富的内容在一个网页里显示?
答案是通过 DOM 编程------根据不同的 URL,动态地将对应内容渲染到同一个容器中:
bash
/ → index.html 的 content DOM 放到 #container
/about → about.html 的 content DOM 放到 #container
但这里有一个关键问题:怎么改变 URL 而不触发页面跳转?
三、Hash 路由------前端路由的基石
答案藏在 URL 的结构中:
ruby
http(s)://www.baidu.com/u/123?a=1&b=2#/page1
└─protocol─┘ └───host───┘ └path┘ └─queryString─┘└─hash─┘
URL 中 # 开始的部分叫做 hash 。它的特点是:改变 hash 部分,URL 会变化,但页面不会刷新。
hash 最初的设计目的是做锚链接------在传统的 PC 长页面中标记某个位置,像电梯一样直达:
css
<a name="top"></a>
<a href="#bottom">去到底部</a>
<div style="height:100vh;background-color: yellow;"></div>
<a href="#top">回到顶部</a>
<div style="height:100vh;background-color: red;"></div>
<a name="bottom"></a>
前端开发者巧妙地利用了这一特性来做前端路由 :使用 #/、#/about 这样的 hash 值,既能让 URL 改变(满足 URL 与资源的一一对应关系),又不会触发页面重新渲染。
当 hash 部分改变时,浏览器会触发 hashchange 事件,我们只需要监听这个事件,然后替换对应的 DOM 或组件即可:
javascript
window.addEventListener('hashchange', function(event) {
console.log('hash 改变了');
console.log(event.newHash);
console.log(event.oldHash);
})
四、手写一个 Hash 路由器
javascript
class HashRouter {
constructor() {
// 前端路由集合------前后端分离后,前端也要有独立的路由
this.routes = {};
// 监听 hashchange 事件
// 注意:回调中 this 默认指向 window,需要用 bind 修正 this 指向
window.addEventListener('hashchange', this.load.bind(this));
}
// 注册路由:将 hash 路径与回调函数关联
register(hash, callback) {
this.routes[hash] = callback;
}
// 加载路由:根据当前 hash 找到对应的回调并执行
load() {
let hash = location.hash.slice(1); // 去掉开头的 #
let handler;
if (!hash) {
// hash 为空时的处理
} else {
handler = this.routes[hash];
}
handler.call(this);
}
}
// 使用
let router = new HashRouter();
let container = document.getElementById('container');
router.register('/page1', () => container.innerHTML = '页面一');
router.register('/page2', () => container.innerHTML = '页面二');
router.register('/page3', () => container.innerHTML = '页面三');
这个简易的 HashRouter 包含了前端路由的核心思想:
routes对象存储路由映射表register方法注册路由load方法根据当前 hash 匹配并执行对应的回调- 通过
hashchange事件驱动页面切换
SPA 极大地提升了用户体验------不再有页面白屏闪烁,内容的切换变得流畅自如。
五、React 集成前端路由
在 React 生态中,react-router-dom 是官方提供的路由解决方案。React 开发"全家桶"的典型组合是:
- React ------ 组件开发、响应式 UI 界面
- react-router-dom ------ 给应用添加前端路由
- zustand / pinia ------ 状态管理
5.1 基本路由配置
javascript
import { HashRouter as Router, Routes, Route } from 'react-router-dom';
<Router>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Router>
HashRouter:底层基于location.hash和hashchange事件,即我们前面学习的 hash 路由机制Routes:路由配置的容器,内部包含多个RouteRoute:单条路由配置,path匹配 URL,element指定要渲染的组件
Routes 的特点是:有且只有一个 Route 会显示 ------即当前 location.hash 对应匹配的那一个。
5.2 导航组件 Link
在 SPA 中不能直接使用 <a> 标签跳转,因为那会触发完整的页面刷新。react-router-dom 提供了 Link 组件来替代:
javascript
import { Link } from 'react-router-dom';
<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>
</ul>
</nav>
Link 组件专门为 SPA 路由跳转而设计,点击后不会刷新页面,只做前端路由切换。
5.3 路由懒加载
随着应用变大,所有页面组件一起打包下载会严重影响首页加载速度。路由懒加载 的思想是:非当前路由的页面不加载,只加载用户正在访问的那个页面。
javascript
import { lazy, Suspense } from 'react';
// 不再使用静态 import
// import Home from './pages/Home';
// import About from './pages/About';
// 改用动态 import 函数 + lazy
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const UserProfile = lazy(() => import('./pages/UserProfile'));
const NotFound = lazy(() => import('./pages/NotFound'));
const Products = lazy(() => import('./pages/Products'));
const ProductDetail = lazy(() => import('./pages/Products/Detail'));
const NewProduct = lazy(() => import('./pages/Products/New'));
lazy() 接收一个返回 import() 的函数,实现按需加载。配合 Suspense 组件,在懒加载组件还未就绪时显示一个 fallback:
xml
<Suspense fallback={<div>Loading...</div>}>
<Routes>
{/* 路由配置 */}
</Routes>
</Suspense>
5.4 动态路由
动态路由允许 URL 中包含可变参数,比如用户 ID、产品 ID:
ini
<Route path="/user/:id" element={<UserProfile />} />
在页面组件中,通过 useParams Hook 获取动态参数:
javascript
import { useParams } from 'react-router-dom';
function UserProfile() {
let { id } = useParams(); // 召之即来,获取 URL 中的动态参数
return <h2>User Profile {id}</h2>;
}
useParams 体现了 React Hooks 的设计思想------召之即来,在任何组件中按需获取路由参数,无需层层传递。
5.5 嵌套路由(多级路由)
嵌套路由用于表示页面中的层级关系,比如产品列表页面内嵌产品详情和新增产品:
xml
<Route path="/products" element={<Products />}>
{/* 二级路由 */}
<Route path=":productId" element={<ProductDetail />} />
<Route path="new" element={<NewProduct />} />
</Route>
父级组件需要通过 Outlet 组件指定子路由的渲染出口:
javascript
import { Outlet } from 'react-router-dom';
const Products = () => {
return (
<>
<h1>产品列表</h1>
<Outlet /> {/* 二级路由的出口,子组件在此渲染 */}
</>
);
};
5.6 404 Not Found 与路由重定向
使用通配符 * 匹配所有未定义的路径,作为 404 兜底页面:
ini
<Route path="*" element={<NotFound />} />
* 是贪婪匹配 ,会匹配所有路径,所以必须放在 Routes 的最末尾,确保前面的路由优先匹配。
在 404 页面中,可以使用 useNavigate 实现编程式导航:
javascript
import { useNavigate } from 'react-router-dom';
const NotFound = () => {
let navigate = useNavigate();
useEffect(() => {
setTimeout(() => {
navigate('/'); // 3 秒后跳转回首页
}, 3000);
}, []);
return <>404 Not Found</>;
};
另外,Navigate 组件可以实现路由重定向:
ini
<Route path="old-path" element={<Navigate replace to="/new-path" />} />
六、重要知识总结
| 知识点 | 核心要点 |
|---|---|
| 多页面 vs 单页面 | 传统多页面每次点击链接都会全量刷新页面("白一下");SPA 在一个页面内动态切换内容,体验更流畅 |
| Hash 路由原理 | # 后面的 hash 部分改变时 URL 会变但页面不刷新,通过监听 hashchange 事件来切换内容 |
| URL 结构 | protocol://host/path?queryString#hash,hash 是前端路由的关键 |
| 锚链接 | hash 的原始用途------在长页面中做"电梯导航",跳转到页面内指定位置 |
| HashRouter 实现 | routes 对象存储映射 → register 注册回调 → hashchange 事件驱动 load 方法 |
| this 指向问题 | 事件监听器中 this 默认指向触发事件的对象(如 window),需要用 bind 修正 |
| react-router-dom | React 官方路由库,核心组件:HashRouter、Routes、Route、Link、Navigate |
Link vs <a> |
<a> 标签会触发完整页面刷新,Link 组件专为 SPA 路由跳转设计,不刷新页面 |
| 路由懒加载 | lazy(() => import(...)) + Suspense,非当前路由页面不加载,提升首屏性能 |
| 动态路由 | path="/user/:id" 定义动态参数,useParams() Hook 在组件中获取参数值 |
| 嵌套路由 | 父路由包含子 <Route>,父组件中使用 <Outlet /> 作为子路由渲染出口 |
| 404 兜底 | path="*" 贪婪匹配所有未定义路径,必须放在 Routes 末尾 |
| 编程式导航 | useNavigate() Hook 返回 navigate 函数,用于代码中控制路由跳转 |
| 路由重定向 | <Navigate replace to="/new-path" /> 实现路径重定向 |
从最初认识 URL 中的 # 和 hashchange 事件,到手写 HashRouter,再到 React 中借助 react-router-dom 构建完整的路由体系------前端路由的演进始终围绕一个核心目标:在保证 URL 与资源一一对应的前提下,让页面切换更快、更流畅,给用户带来接近原生 App 的体验。