早期的 Web 开发,那会儿还没有 "前端路由" 这个概念。整个 Web 世界里,路由的控制权完全掌握在后端手里。
路由的前世:后端路由时代
传统后端路由的工作流
想象一下这样的场景:用户打开浏览器访问example.com,服务器收到请求后,会根据 URL 路径找到对应的路由处理函数,从数据库取数据,然后用模板引擎把数据套进 HTML 里,最后把完整的 HTML 页面返回给浏览器。如果用户点击了页面上的 "关于我们" 链接,浏览器会再次发送example.com/about的请求,服务器重复上述过程,返回一个全新的 HTML 页面。
客户端请求 → 服务器接收 → 路由匹配 → 读取数据 → 渲染HTML → 返回页面
这种模式下,每个 URL 对应一个完整的 HTML 页面,切换页面就意味着要重新向服务器发请求、重新加载整个页面。
后端路由的局限
这种开发方式足够简单直接,新手也能快速上手。但问题也很明显:
-
前后端强耦合:后端开发者不仅要写业务逻辑、操作数据库,还要负责 HTML 页面的渲染
-
体验不佳:每次切换页面都要白屏等待,完全刷新整个页面
-
开发效率低:前后端开发者需要频繁沟通页面细节,MVC 模式里的 View 层成了前后端交叉地带
-
资源浪费:明明只需要更新页面的一部分,却要传输整个 HTML 文档
前端路由的崛起:SPA 时代的必然
随着 Ajax 技术的成熟和前端框架的发展,"前后端分离" 逐渐成为主流。后端不再以返回完整页面为目的,而是提供 API 接口;前端则通过 JavaScript 动态更新页面内容,这就是单页应用(SPA)。
为什么需要前端路由?
SPA 虽然解决了页面刷新问题,但带来了新的挑战:如何在不刷新页面的情况下,让 URL 与页面内容对应起来?用户刷新页面时,如何确保能回到当前看到的内容?
前端路由就是为了解决这些问题而生的:
-
保持 URL 与页面内容同步
-
支持浏览器的前进 / 后退功能
-
实现页面间的无刷新跳转
-
让 SPA 应用也能被搜索引擎正确索引
从 MVC 到 MVVM:前端开发模式的演进
SPA 的普及也推动了前端开发模式从 MVC 向 MVVM 的转变。在传统 MVC 中,Controller 既要处理业务逻辑,又要手动操作 DOM 更新视图,随着应用复杂度提升,代码会变得臃肿混乱。
而 MVVM(Model-View-ViewModel)模式则实现了数据与视图的解耦:
-
Model:负责管理数据,可能是从后端 API 获取的用户信息、商品列表,也可能是本地存储的配置
-
View:就是用户看到的页面,所有按钮、输入框等元素都属于 View,只负责展示数据
-
ViewModel:作为连接 Model 和 View 的桥梁,一边监听 Model 的变化,数据更新时自动通知 View 重新渲染;另一边接收 View 的操作(如点击按钮、输入文字),再去修改 Model 的数据
在 React 中,useState 定义的状态就是 Model,JSX 结构就是 View,React 内部的状态管理机制承担了 ViewModel 的角色。当输入框输入文字时,onChange 事件触发状态更新(修改 Model),ViewModel 检测到变化后,会让 View 重新渲染显示新内容。这种数据驱动的方式,让开发者不用手动操作 DOM,只需关注数据变化。
正是这种模式的成熟,让前端能够独立掌控页面渲染逻辑,也为前端路由的实现奠定了基础 —— 路由切换本质上就是改变 View 层展示的组件,而这一切都可以通过 ViewModel 对数据的管理来实现。
React Router:React 生态的路由方案
React 作为构建 SPA 的主流框架,本身并没有内置路由功能,而 React Router 则成为了 React 生态中事实上的路由标准。
核心概念与基本用法
首先我们需要安装 React Router:
npm i react-router-dom
最基础的路由配置示例:
import { BrowserRouter as Router, Route, Link, Routes } from 'react-router-dom';
function Home() {
return <h1>首页</h1>;
}
function About() {
return <h1>关于我们</h1>;
}
function App() {
return (
<Router>
{/* 导航链接 */}
<nav>
<Link to="/">首页</Link> |
<Link to="/about">关于我们</Link>
</nav>
{/* 路由匹配 */}
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</Router>
);
}
这里有几个核心组件需要理解:
-
BrowserRouter:使用 HTML5 的 history API 实现路由(pushState、replaceState)
-
Link:生成导航链接,类似
<a>标签但不会导致页面刷新 -
Route:定义路径与组件的映射关系
-
Routes:管理路由匹配,只会渲染第一个匹配的路由
动态路由与参数获取
实际开发中,我们经常需要处理带参数的路由,获取路由参数常用到 useParams 和 useSearchParams,不过两者有明显区别。
useParams 用于获取路由路径中的动态参数,比如/user/123中的123:
import { useParams } from 'react-router-dom';
<Route path="/user/:id" element={<UserProfile />} />
function UserProfile() {
const { id } = useParams();
return <h1>用户ID:{id}</h1>;
}
而 useSearchParams 用于获取 URL 中查询字符串的参数,也就是?后面的部分,比如/search?keyword=react&page=1中的keyword和page:
import { useSearchParams } from 'react-router-dom';
function SearchPage() {
const [searchParams, setSearchParams] = useSearchParams();
// 获取参数
const keyword = searchParams.get('keyword');
const page = searchParams.get('page');
// 更新参数
const handlePageChange = (newPage) => {
// 会更新URL为/search?keyword=react&page=newPage
setSearchParams({ keyword, page: newPage });
};
return (
<div>
<p>搜索关键词:{keyword}</p>
<p>当前页码:{page}</p>
<button onClick={() => handlePageChange(2)}>去第二页</button>
</div>
);
}
简单来说,useParams 获取的是路由路径中定义的动态片段,参数是 URL 路径的一部分;useSearchParams 获取的是查询字符串参数,参数以键值对形式跟在?后面,且 useSearchParams 还能修改参数并更新 URL,这是它和 useParams 的一个重要区别。
嵌套路由
复杂应用通常会有嵌套结构,比如二级路由、三级路由等等,这里用二级路由来做一个示例:
先定义一个 Layout 组件作为父容器,专门放公共布局和路由出口:
function DashboardLayout() {
return (
<div className="dashboard-container">
<aside>后台导航</aside>
<main>
{/* 子路由会渲染到这里 */}
<Outlet />
</main>
</div>
);
}
然后在路由配置里通过嵌套关系关联起来:
// 路由配置
<Route path="/dashboard" element={<DashboardLayout />}>
<Route path="profile" element={<Profile />} />
<Route path="settings" element={<Settings />} />
<Route index element={<DashboardHome />} />
</Route>
这里的 <Outlet /> 就相当于路由出口,父路由匹配时会先渲染 Layout 组件,再把匹配到的子路由组件放到 Outlet 的位置。这种方式的好处是:
- 公共布局(比如侧边栏、导航栏)只写一次,不用在每个子组件里重复
- 路由结构更直观,从配置就能看出嵌套关系
- 方便统一处理权限、面包屑等全局逻辑
React Router 的高级用法
编程式导航
除了使用Link组件进行声明式导航,我们还可以通过编程方式控制路由跳转:
import { useNavigate } from 'react-router-dom';
function Login() {
const navigate = useNavigate();
const handleLogin = () => {
// 登录逻辑...
// 登录成功后跳转到首页
navigate('/');
// 或者替换当前历史记录(无法回退到登录页)
// navigate('/', { replace: true });
};
return <button onClick={handleLogin}>登录</button>;
}
路由守卫与权限控制
某些页面需要登录后才能访问,我们可以创建一个私有路由组件:
function PrivateRoute({ element }) {
const isAuthenticated = checkUserLoginStatus(); // 检查用户是否登录
// 如果未登录,重定向到登录页
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return element;
}
// 使用私有路由
<Route
path="/dashboard"
element={<PrivateRoute element={<Dashboard />} />}
/>
路由懒加载
为了优化大型应用的加载速度,我们可以使用 React 的lazy和Suspense结合路由实现按需加载:
import { lazy, Suspense } from 'react';
// 懒加载组件
const About = lazy(() => import('./About'));
// 路由配置
const router = createBrowserRouter([
{
path: '/about',
element: <Suspense fallback={<div>加载中...</div>}> <About /> </Suspense>
}
])
前端路由的底层实现原理
理解路由的工作原理,能帮助我们更好地使用 React Router。前端路由主要有两种实现方式,它们的核心区别在于如何在不刷新页面的情况下改变 URL,并让页面内容与 URL 保持同步。
1. Hash 模式:URL 中的隐藏开关
Hash 模式是前端路由最原始的实现方式,它利用了浏览器的一个特性:URL 中的哈希值(# 后面的部分)发生变化时,不会向服务器发送请求,也不会刷新整个页面。
工作流程:
用户点击链接 → URL 哈希值变化 → 触发 hashchange 事件 → JS 监听事件并更新页面内容
关键代码:
// 监听哈希值变化
window.addEventListener('hashchange', () => {
// 获取当前哈希值,例如 #/products/123
const hash = window.location.hash;
// 提取路径部分(去掉 #)
const path = hash.slice(1); // 得到 /products/123
// 根据路径渲染对应组件
renderComponentByPath(path);
});
// 初始化时也需要渲染一次
renderComponentByPath(window.location.hash.slice(1));
特点:
- 兼容性好:支持所有现代浏览器,甚至包括 IE8
- 无需服务器配置:服务器只看到
http://example.com,哈希值完全由前端处理 - URL 不美观:带有
#符号,不符合传统 URL 习惯
2. History 模式:利用 HTML5 的历史管理
History 模式是 HTML5 引入的新特性,通过 history.pushState() 和 history.replaceState() 方法,我们可以直接操作浏览器的历史记录,实现 URL 变化但不刷新页面。
工作流程:
用户点击链接 → JS 调用 pushState 修改 URL → 页面内容更新 → 浏览器历史记录变化
关键代码:
// 修改 URL 但不刷新页面
window.history.pushState(null, null, '/products/123');
// 监听浏览器前进/后退按钮
window.addEventListener('popstate', () => {
// 获取当前路径
const path = window.location.pathname; // /products/123
// 根据路径渲染对应组件
renderComponentByPath(path);
});
// 处理页面加载时的初始路径
renderComponentByPath(window.location.pathname);
特点:
- URL 更美观:没有
#,例如http://example.com/products/123 - 需要服务器配合:用户直接访问
http://example.com/products/123时,服务器需要返回同一个 HTML 文件 - 兼容性稍差:不支持 IE9 及以下浏览器
两种模式的核心对比
| 维度 | Hash 模式 | History 模式 |
|---|---|---|
| URL 格式 | http://example.com/#/path | http://example.com/path |
| 触发方式 | 监听 hashchange 事件 | 调用 pushState/replaceState |
| 服务器需求 | 无需特殊配置 | 需要配置所有路由返回同一个 HTML |
| 浏览器支持 | 全兼容(IE8+) | IE10+ |
| 历史记录管理 | 自动记录哈希变化 | 需要手动调用 API 管理 |
React Router 的最佳实践
路由配置集中管理
对于大型应用,建议将路由配置集中管理:
// 1. 用 createBrowserRouter 定义路由表
const router = createBrowserRouter([
{
path: '/',
element: <Home />
},
{
path: '/about',
element: <About />
},
{
path: '/dashboard',
element: <DashboardLayout />,
children: [
{ path: 'profile', element: <Profile /> }
]
}
]);
// 2. 用 RouterProvider 加载
function App() {
return <RouterProvider router={router} />;
}
处理 404 页面
定义一个匹配所有路径的路由作为 404 页面:
<Routes>
{/* 其他路由... */}
<Route path="*" element={<NotFound />} />
</Routes>
服务器配置
使用 history 模式时,需要服务器配合处理刷新 404 的问题(以 Nginx 为例):
location / {
try_files $uri $uri/ /index.html;
}
总结
从后端路由到前端路由,反映了 Web 开发的演进历程:后端路由时代,前后端未分离,服务器直接返回完整页面;前端路由时代,前后端分离,前端掌控页面渲染与跳转,MVVM 模式让数据驱动视图成为可能。
React Router 作为前端路由的优秀实现,让我们能够轻松构建复杂的单页应用。路由的本质就是URL 与资源的映射关系,无论是后端还是前端,理解这一点,就能在技术迭代中保持清晰的认知。