14、ReactJS - 路由(Router):从痛点到企业级实战,一篇讲透前端路由的前世今生

文章目录
- [14、ReactJS - 路由(Router):从痛点到企业级实战,一篇讲透前端路由的前世今生](#14、ReactJS - 路由(Router):从痛点到企业级实战,一篇讲透前端路由的前世今生)
-
- 一、痛点场景:没有路由的前端到底有多痛
- [二、解决方案:React Router 如何破局](#二、解决方案:React Router 如何破局)
- [三、React Router 是什么](#三、React Router 是什么)
-
- [3.1 专业解释](#3.1 专业解释)
- [3.2 大白话解释](#3.2 大白话解释)
- [3.3 生活案例](#3.3 生活案例)
- [四、为什么选择 React Router](#四、为什么选择 React Router)
- [五、演进之路:从 v1 到 v7 的十年变迁](#五、演进之路:从 v1 到 v7 的十年变迁)
-
- [5.1 v1 - v3 时代(2014 - 2017):集中式配置](#5.1 v1 - v3 时代(2014 - 2017):集中式配置)
- [5.2 v4 - v5 时代(2017 - 2021):组件化路由](#5.2 v4 - v5 时代(2017 - 2021):组件化路由)
- [5.3 v6 时代(2021 - 2024):现代路由的定型](#5.3 v6 时代(2021 - 2024):现代路由的定型)
- [5.4 v6.4+ 时代(2022 至今):Data Router 的引入](#5.4 v6.4+ 时代(2022 至今):Data Router 的引入)
- [5.5 v7 时代(2025+):全栈框架的融合](#5.5 v7 时代(2025+):全栈框架的融合)
- 六、怎么用:从入门到企业级实战
-
- [6.1 安装与基础配置](#6.1 安装与基础配置)
- [6.2 核心组件与 Hooks](#6.2 核心组件与 Hooks)
- [6.3 嵌套路由与布局](#6.3 嵌套路由与布局)
- [6.4 动态路由与参数传递](#6.4 动态路由与参数传递)
- [6.5 编程式导航](#6.5 编程式导航)
- [6.6 企业级实战:权限路由守卫](#6.6 企业级实战:权限路由守卫)
- [6.7 企业级实战:路由懒加载与代码分割](#6.7 企业级实战:路由懒加载与代码分割)
- [6.8 企业级实战:Data Router 数据加载](#6.8 企业级实战:Data Router 数据加载)
- 七、竞品对比:四大路由方案优劣势分析
- 八、常用场景盘点
- 九、面试官高频面试题
-
- [面试题一:BrowserRouter 和 HashRouter 有什么区别?](#面试题一:BrowserRouter 和 HashRouter 有什么区别?)
- [面试题二:React Router v5 和 v6 有哪些主要区别?](#面试题二:React Router v5 和 v6 有哪些主要区别?)
- 面试题三:如何实现路由守卫(权限控制)?
- 面试题四:路由参数有哪些传递方式?如何获取?
- [面试题五:history 模式下刷新页面为什么会 404?怎么解决?](#面试题五:history 模式下刷新页面为什么会 404?怎么解决?)
- [面试题六:React Router 的嵌套路由是如何工作的?](#面试题六:React Router 的嵌套路由是如何工作的?)
- 面试题七:如何实现路由懒加载?
- [面试题八:`<Link>` 和 `<a>` 标签有什么区别?](#面试题八:
<Link>和<a>标签有什么区别?)
- 十、总结
一、痛点场景:没有路由的前端到底有多痛
做前端开发的同学,大概率都经历过这样的场景:公司要做一个管理后台,里面有用户管理、订单管理、数据统计等十几个页面。最朴素的做法是什么?每个页面写一个独立的 HTML 文件,index.html、users.html、orders.html,页面之间用 <a href> 跳转。
听上去没问题,对吧?但真正做起来,痛点一个接一个:
痛点一:整页刷新,体验割裂。 从用户列表页点进用户详情页,浏览器整个白屏一下,再重新加载所有 CSS、JS、图片。一个后台系统动辄十几个页面,每次跳转都像在翻一本厚重的书,用户体验极其糟糕。
痛点二:状态丢失,数据重复请求。 你在列表页筛选了条件、翻到了第三页,点进详情再点返回,之前的筛选条件全没了,又得重新操作一遍。因为每次跳转都是全新的页面,内存中的状态荡然无存。
痛点三:公共组件重复渲染。 每个页面都有相同的顶部导航栏、侧边菜单,却要在每个 HTML 里重复写一遍。改一个导航链接,得去十几个文件里逐个修改,维护成本极高。
痛点四:前后端耦合严重。 每个页面对应后端一个路由,前端改个页面路径还得跟后端打招呼,部署也得跟着改 Nginx 配置。

这些痛点的根源是什么?就是传统多页应用(MPA)的页面跳转模型。浏览器天然就是"一个 URL 对应一个文档"的模式,而现代 Web 应用需要的是"一个应用内多个视图,切换时不刷新页面"。这,就是前端路由要解决的核心问题。
二、解决方案:React Router 如何破局
React Router 的核心思路一句话就能说清:把 URL 和组件之间建立映射关系,URL 变化时不刷新页面,而是动态渲染对应的组件。
具体来说,它做了这几件关键的事:
- 拦截浏览器导航行为:通过 History API 或 Hash 变化监听,在 URL 改变时阻止浏览器默认的整页刷新行为。
- 匹配路由规则:根据当前 URL 去匹配预先定义好的路由配置,找到对应的组件。
- 动态渲染组件 :把匹配到的组件渲染到页面上的指定位置(通常是
<Outlet />占位符),其他部分(导航栏、侧边栏)保持不动。 - 管理路由状态:提供路由参数、查询参数、导航状态等数据,让组件能感知当前路由上下文。
这样一来,页面切换变成了组件的卸载和挂载,没有网络请求、没有白屏、状态可以保留在内存中,用户体验瞬间提升了一个档次。这就是单页应用(SPA) 的核心魅力,而 React Router 就是 React 生态中实现 SPA 路由的事实标准。
三、React Router 是什么
3.1 专业解释
React Router 是一个基于 React 思想的声明式路由库,它通过组件化的方式将 URL 与 React 组件进行映射,实现客户端路由(Client-Side Routing)。它提供了 <BrowserRouter>、<Routes>、<Route>、<Link>、<Navigate>、<Outlet /> 等组件,以及 useParams、useNavigate、useLocation、useSearchParams 等 Hooks,让开发者能够以声明式、组件化的方式管理应用的导航逻辑。
从架构上看,React Router 分为三层:
- react-router:核心库,提供路由匹配和导航的基础能力,与平台无关。
- react-router-dom:在核心库基础上封装了 DOM 环境下的实现,是 Web 开发中实际使用的包。
- react-router-native:面向 React Native 的路由实现。
3.2 大白话解释
说人话,React Router 就是一个**"URL 翻译官"**。你告诉它:"当地址是 /users 的时候,显示用户列表组件;当地址是 /users/123 的时候,显示 ID 为 123 的用户详情组件。"它就忠实地执行这个映射关系。用户在浏览器地址栏输入什么、点了什么链接,它就把对应的组件"搬"到页面上,而且整个过程不刷新页面,就像在手机 App 里切换页面一样流畅。
你可以把它想象成商场里的导览员。商场(你的应用)有很多楼层和店铺(组件),顾客(用户)说"我要去三楼的服装店"(输入 URL),导览员(React Router)不把整个商场拆掉重建,而是直接把顾客领到对应的店铺(渲染对应组件),商场的大门、电梯、走廊(导航栏、布局)始终都在。
3.3 生活案例
拿大家最熟悉的微信来类比。微信底部有四个 Tab:微信、通讯录、发现、我。点不同的 Tab,顶部标题栏和底部导航栏都不变,只有中间内容区域在切换。这就是典型的前端路由行为。
如果没有前端路由,微信每次切换 Tab 都要整个 App 重启一遍,聊天记录全没了,那还能用吗?React Router 干的就是这件事------让 Web 应用拥有原生 App 一样的页面切换体验。

四、为什么选择 React Router
了解了是什么之后,你可能会问:前端路由方案那么多,为什么偏偏是 React Router?它的优势体现在哪里?
第一,生态地位无可撼动。 React Router 是 React 官方推荐的路由方案,从 2014 年诞生至今已经迭代了十多年,GitHub 星标超过 5 万,npm 周下载量超过千万次。几乎所有 React 教程、开源项目、企业项目都在用它,遇到问题搜一下就有答案,这是其他方案无法比拟的生态优势。
第二,声明式 API 符合 React 哲学。 React 的核心思想是"UI 是状态的函数",React Router 把路由也变成了声明式的组件写法。你不需要命令式地监听 URL 变化、手动操作 DOM,只需要写 <Route path="/users" element={<Users />} /> 这样的配置,路由关系一目了然。
第三,嵌套路由能力强大。 企业级应用几乎都有复杂的布局嵌套:外层是整体布局,内层是页面内容,页面内可能还有 Tab 切换。React Router 的嵌套路由配合 <Outlet />,能优雅地处理任意层级的布局组合,这是它相比其他方案的核心竞争力之一。
第四,功能全面且持续进化。 从基础的路由匹配、参数传递,到权限守卫、懒加载、数据加载(loader/action)、表单处理,React Router 覆盖了企业级应用路由的所有场景。尤其是 v6.4 之后引入的 Data Router 和 v7 融合 Remix 后的全栈能力,让它不再只是一个"路由库",而是一个完整的应用框架。
第五,与 React 特性深度集成。 React Router 全面拥抱 Hooks,提供了 useNavigate、useParams、useLocation、useSearchParams、useLoaderData 等一系列 Hooks,与函数组件和 React 18 的并发特性无缝配合。
五、演进之路:从 v1 到 v7 的十年变迁
React Router 不是一天建成的,它的 API 经历了几次重大重构。了解这段历史,能帮你看懂为什么现在的 API 是这样设计的,也能在维护老项目时不至于一脸懵。

5.1 v1 - v3 时代(2014 - 2017):集中式配置
早期的 React Router 采用集中式路由配置,所有路由规则写在一个地方,通过 <Router>、<Route>、<IndexRoute> 嵌套定义。这就是你在很多老教程里看到的写法:
javascript
import { Router, Route, IndexRoute, browserHistory } from 'react-router';
ReactDOM.render((
<Router history={browserHistory}>
<Route path="/" component={App}>
<IndexRoute component={Home} />
<Route path="about" component={About} />
<Route path="contact" component={Contact} />
</Route>
</Router>
), document.getElementById('app'));
这个时期的特点是:路由配置集中、嵌套路由原生支持、但 API 偏命令式,与 React 的组件化思想融合得不够深入。
5.2 v4 - v5 时代(2017 - 2021):组件化路由
v4 是一次颠覆性的重构,核心理念变成了"一切皆组件 "。路由规则不再集中配置,而是分散在各个组件中,用 <Switch> 包裹多个 <Route>,匹配到第一个就渲染。同时包名也拆分成了 react-router-dom(Web 端)和 react-router-native(移动端)。
javascript
import { BrowserRouter, Switch, Route, Link } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">首页</Link>
<Link to="/about">关于</Link>
</nav>
<Switch>
<Route exact path="/" component={Home} />
<Route path="/about" component={About} />
</Switch>
</BrowserRouter>
);
}
v5 在 v4 基础上做了优化,引入了 useParams、useLocation 等 Hooks,但核心 API 没变。这个时期的问题是:嵌套路由变得别扭(需要手动写 path 拼接)、<Switch> 的匹配顺序依赖书写顺序、exact 属性满天飞,容易出错。
5.3 v6 时代(2021 - 2024):现代路由的定型
v6 又是一次重大重构,吸收了 Reach Router 的优秀设计(排名匹配、嵌套路由回归),同时修复了 v5 的诸多痛点:
<Switch>改为<Routes>,路由匹配基于排名算法而非书写顺序,不再需要exact。component/render属性统一为element,直接传 JSX。- 嵌套路由回归,子路由的
path支持相对路径。 - 新增
<Outlet />作为子路由渲染占位符。 - 全面 Hooks 化,
useNavigate替代useHistory。 - 移除了对
path-to-regexp的依赖,包体积更小。
javascript
import { BrowserRouter, Routes, Route, Outlet, Link } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route path="users/:id" element={<UserDetail />} />
</Route>
</Routes>
</BrowserRouter>
);
}
5.4 v6.4+ 时代(2022 至今):Data Router 的引入
v6.4 引入了基于 Remix 理念的 Data Router,通过 createBrowserRouter 创建路由,支持 loader(数据加载)、action(表单处理)、errorElement(错误边界)等能力,把数据获取和路由绑定在一起,避免了传统的 useEffect 拉数据模式。
javascript
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
const router = createBrowserRouter([
{
path: '/users/:id',
element: <UserDetail />,
loader: async ({ params }) => {
const res = await fetch(`/api/users/${params.id}`);
return res.json();
},
errorElement: <ErrorPage />,
},
]);
function App() {
return <RouterProvider router={router} />;
}
5.5 v7 时代(2025+):全栈框架的融合
v7 是 React Router 的又一个里程碑,它正式融合了 Remix 框架的全栈能力,从一个纯客户端路由库演进为支持服务端渲染(SSR)、服务端函数、渐进增强的全栈框架。v7 保持了对 v6 API 的高度兼容,同时提供了 react-router 统一包名,支持 SSR、流式渲染、服务端 loaders/actions 等能力。
这十年的演进,本质上是在回答一个问题:路由到底应该只是 URL 到组件的映射,还是应该承担更多应用架构的职责? React Router 的答案是后者,它正在从一个路由库成长为一个完整的应用框架。
六、怎么用:从入门到企业级实战
理论讲了这么多,接下来进入实战环节。我会从最基础的安装配置讲起,逐步深入到企业项目中的权限控制、懒加载、数据加载等高级用法。所有代码都基于当前主流的 React Router v6。
6.1 安装与基础配置
首先安装依赖。Web 项目安装 react-router-dom,它会自动引入核心库 react-router:
bash
npm install react-router-dom
或者使用 yarn / pnpm:
bash
yarn add react-router-dom
pnpm add react-router-dom
安装完成后,在应用入口用 <BrowserRouter> 包裹整个应用,然后用 <Routes> 和 <Route> 定义路由规则:
jsx
// src/main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Layout from './layouts/Layout';
import Home from './pages/Home';
import About from './pages/About';
import NotFound from './pages/NotFound';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="about" element={<About />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</BrowserRouter>
);
几个关键点:
<BrowserRouter>使用 HTML5 History API,URL 干净(如/about),但需要服务端配置支持,否则刷新会 404。<Route path="*">是通配路由,匹配所有未定义的路径,通常用于 404 页面。<Route index>是索引路由,当父路径匹配但没有子路径时渲染,比如访问/时渲染<Home />。
6.2 核心组件与 Hooks
React Router v6 提供的核心 API 可以分为组件和 Hooks 两类:
常用组件:
| 组件 | 作用 |
|---|---|
<BrowserRouter> |
基于 History API 的路由容器,URL 干净 |
<HashRouter> |
基于 URL hash 的路由容器,兼容性好,URL 带 # |
<Routes> |
路由匹配容器,替代 v5 的 <Switch> |
<Route> |
单条路由规则,定义 path 与 element 的映射 |
<Link> |
声明式导航链接,替代 <a>,不刷新页面 |
<NavLink> |
带激活状态的导航链接,适合菜单高亮 |
<Navigate> |
声明式重定向组件 |
<Outlet /> |
子路由渲染占位符,嵌套路由的核心 |
常用 Hooks:
| Hook | 作用 |
|---|---|
useNavigate() |
获取导航函数,实现编程式跳转 |
useParams() |
获取动态路由参数,如 /users/:id 中的 id |
useLocation() |
获取当前 location 对象,包含 pathname、search、state 等 |
useSearchParams() |
获取和设置 URL 查询参数 |
useRouteError() |
获取路由加载/渲染时的错误信息 |
useLoaderData() |
获取 Data Router 中 loader 返回的数据 |
6.3 嵌套路由与布局
嵌套路由是企业项目中最常用的特性之一。典型的后台系统布局是:外层有侧边栏和顶部导航,内层是具体页面内容。用嵌套路由实现非常优雅:
jsx
// src/layouts/Layout.jsx
import { Outlet, NavLink } from 'react-router-dom';
export default function Layout() {
return (
<div className="app-layout">
<aside className="sidebar">
<NavLink to="/" end className={({ isActive }) => isActive ? 'active' : ''}>
首页
</NavLink>
<NavLink to="/users" className={({ isActive }) => isActive ? 'active' : ''}>
用户管理
</NavLink>
<NavLink to="/orders" className={({ isActive }) => isActive ? 'active' : ''}>
订单管理
</NavLink>
</aside>
<main className="content">
{/* 子路由渲染到这里 */}
<Outlet />
</main>
</div>
);
}
路由配置中,子路由写在父路由的内部:
jsx
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="users" element={<Users />} />
<Route path="orders" element={<Orders />} />
</Route>
</Routes>
这样,无论切换到哪个子页面,侧边栏都保持不变,只有 <Outlet /> 的内容在切换。这就是嵌套路由的威力------布局与内容分离,公共部分只渲染一次。

6.4 动态路由与参数传递
企业项目中大量使用动态路由,比如用户详情页 /users/123、商品详情页 /products/456。用 :paramName 语法定义动态段:
jsx
<Route path="/users/:userId" element={<UserDetail />} />
在组件中通过 useParams() 获取参数:
jsx
// src/pages/UserDetail.jsx
import { useParams, Link } from 'react-router-dom';
import { useEffect, useState } from 'react';
export default function UserDetail() {
const { userId } = useParams();
const [user, setUser] = useState(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}, [userId]);
if (!user) return <div>加载中...</div>;
return (
<div>
<h1>{user.name}</h1>
<p>邮箱:{user.email}</p>
<Link to="/users">返回列表</Link>
</div>
);
}
查询参数(URL 中 ?key=value 的部分)通过 useSearchParams() 操作:
jsx
import { useSearchParams } from 'react-router-dom';
export default function UserList() {
const [searchParams, setSearchParams] = useSearchParams();
const page = searchParams.get('page') || 1;
const keyword = searchParams.get('keyword') || '';
const handleSearch = (e) => {
e.preventDefault();
setSearchParams({ keyword: e.target.keyword.value, page: 1 });
};
return (
<div>
<form onSubmit={handleSearch}>
<input name="keyword" defaultValue={keyword} placeholder="搜索用户" />
<button type="submit">搜索</button>
</form>
<p>当前第 {page} 页,关键词:{keyword}</p>
</div>
);
}
6.5 编程式导航
除了用 <Link> 做声明式导航,很多场景需要在代码逻辑中跳转,比如登录成功后跳转到首页、表单提交后跳转到列表页。用 useNavigate():
jsx
import { useNavigate } from 'react-router-dom';
export default function LoginPage() {
const navigate = useNavigate();
const handleLogin = async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(Object.fromEntries(formData)),
});
if (res.ok) {
// 跳转到首页,replace 为 true 时不保留历史记录
navigate('/', { replace: true });
}
};
return (
<form onSubmit={handleLogin}>
<input name="username" placeholder="用户名" />
<input name="password" type="password" placeholder="密码" />
<button type="submit">登录</button>
</form>
);
}
navigate 还支持前进后退:
javascript
navigate(-1); // 后退一步
navigate(1); // 前进一步
6.6 企业级实战:权限路由守卫
企业项目几乎都需要权限控制:未登录用户不能访问后台页面,普通用户不能访问管理员页面。React Router v6 没有内置的路由守卫 API,但通过高阶组件(HOC)的方式可以优雅实现。
第一步:创建权限守卫组件
jsx
// src/components/ProtectedRoute.jsx
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '../hooks/useAuth';
export default function ProtectedRoute({ children, requiredRole }) {
const { isAuthenticated, user } = useAuth();
const location = useLocation();
// 未登录,重定向到登录页,并记录来源地址
if (!isAuthenticated) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
// 已登录但角色不满足,显示无权限页面
if (requiredRole && user.role !== requiredRole) {
return <Navigate to="/403" replace />;
}
return children;
}
第二步:在路由配置中使用
jsx
<Routes>
{/* 公开页面 */}
<Route path="/login" element={<Login />} />
<Route path="/403" element={<Forbidden />} />
{/* 需要登录的页面 */}
<Route path="/" element={
<ProtectedRoute>
<Layout />
</ProtectedRoute>
}>
<Route index element={<Home />} />
<Route path="users" element={<Users />} />
{/* 需要管理员角色 */}
<Route path="admin" element={
<ProtectedRoute requiredRole="admin">
<AdminDashboard />
</ProtectedRoute>
} />
</Route>
<Route path="*" element={<NotFound />} />
</Routes>
第三步:登录页跳回来源地址
jsx
// src/pages/Login.jsx
import { useNavigate, useLocation } from 'react-router-dom';
export default function Login() {
const navigate = useNavigate();
const location = useLocation();
const from = location.state?.from?.pathname || '/';
const handleLogin = async () => {
// ... 登录逻辑
navigate(from, { replace: true });
};
return <button onClick={handleLogin}>登录</button>;
}
这样就实现了完整的权限控制链路:未登录跳转登录页、登录后跳回原页面、角色不足显示 403。

6.7 企业级实战:路由懒加载与代码分割
大型应用如果把所有页面代码打包到一个 JS 文件里,首屏加载会非常慢。React Router 配合 React 的 React.lazy 和 Suspense 可以轻松实现路由级别的代码分割,每个页面按需加载:
jsx
// src/router/index.jsx
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import Layout from '../layouts/Layout';
import Loading from '../components/Loading';
// 懒加载页面组件
const Home = lazy(() => import('../pages/Home'));
const Users = lazy(() => import('../pages/Users'));
const UserDetail = lazy(() => import('../pages/UserDetail'));
const Orders = lazy(() => import('../pages/Orders'));
const AdminDashboard = lazy(() => import('../pages/AdminDashboard'));
export default function AppRouter() {
return (
<BrowserRouter>
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route path="users" element={<Users />} />
<Route path="users/:id" element={<UserDetail />} />
<Route path="orders" element={<Orders />} />
<Route path="admin" element={<AdminDashboard />} />
</Route>
</Routes>
</Suspense>
</BrowserRouter>
);
}
这样,用户访问 /users 时才会加载 Users 页面的 JS 代码,首屏只加载 Layout 和 Home 的代码,大大提升首屏速度。配合 Vite 或 Webpack 的 chunk 分割,还能进一步优化。
6.8 企业级实战:Data Router 数据加载
传统模式下,组件渲染后通过 useEffect 拉数据,会经历"渲染空状态 -> 请求数据 -> 重新渲染"的过程,体验不够好。v6.4 引入的 Data Router 把数据加载前置到路由层,组件渲染时数据已经准备好了。
jsx
// src/router/index.jsx
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import Layout from '../layouts/Layout';
import Home from '../pages/Home';
import UserList from '../pages/UserList';
import UserDetail from '../pages/UserDetail';
import ErrorPage from '../pages/ErrorPage';
const router = createBrowserRouter([
{
path: '/',
element: <Layout />,
errorElement: <ErrorPage />,
children: [
{ index: true, element: <Home /> },
{
path: 'users',
element: <UserList />,
// 进入路由前自动加载数据
loader: async () => {
const res = await fetch('/api/users');
if (!res.ok) throw new Response('加载失败', { status: 500 });
return res.json();
},
},
{
path: 'users/:id',
element: <UserDetail />,
loader: async ({ params }) => {
const res = await fetch(`/api/users/${params.id}`);
if (!res.ok) throw new Response('用户不存在', { status: 404 });
return res.json();
},
},
],
},
]);
export default function App() {
return <RouterProvider router={router} />;
}
在组件中通过 useLoaderData() 直接获取数据:
jsx
// src/pages/UserList.jsx
import { useLoaderData, Link } from 'react-router-dom';
export default function UserList() {
const users = useLoaderData();
return (
<div>
<h1>用户列表</h1>
<table>
<thead>
<tr>
<th>ID</th>
<th>姓名</th>
<th>邮箱</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{users.map(user => (
<tr key={user.id}>
<td>{user.id}</td>
<td>{user.name}</td>
<td>{user.email}</td>
<td>
<Link to={`/users/${user.id}`}>查看详情</Link>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
Data Router 还支持 action 处理表单提交、shouldRevalidate 控制数据重新加载时机,这些能力在企业项目中能大幅简化数据管理逻辑。
七、竞品对比:四大路由方案优劣势分析
前端路由领域不止 React Router 一家,了解各方案的优劣势,才能在项目选型时做出最优决策。

| 对比维度 | React Router v6/v7 | Next.js App Router | TanStack Router | Reach Router |
|---|---|---|---|---|
| 定位 | 通用客户端路由库 | 全栈框架内置路由 | 类型优先的现代路由库 | 简洁可访问的路由库 |
| 路由定义方式 | 组件式 <Route> 或对象式配置 |
文件系统约定(app 目录结构) | 对象式配置 + 类型推断 | 组件式 <Router> |
| 嵌套路由 | 原生支持,<Outlet /> 占位 |
原生支持,layout.tsx 约定 | 原生支持 | 原生支持 |
| 类型安全 | 运行时匹配,参数需手动声明类型 | 基于文件系统自动推断 | 全链路类型安全,搜索参数也有类型 | 较弱 |
| SSR/SSG | v7 支持 SSR,v6 需自行配置 | 开箱即用,SSR/SSG/ISR/PPR | 支持(TanStack Start) | 不支持 |
| 数据加载 | v6.4+ loader/action,v7 服务端 loader | Server Components、fetch 缓存 | loader + 类型化数据 | 不支持 |
| 包体积 | 约 10KB(gzip) | 框架级,体积较大 | 约 8KB(gzip) | 约 6KB(gzip) |
| 生态成熟度 | 极高,十年积累,社区庞大 | 极高,Vercel 背书,增长迅猛 | 中等,TanStack 生态加持 | 已停止维护,并入 React Router |
| 学习曲线 | 平缓,API 直观 | 较陡,约定大于配置,概念多 | 中等,类型驱动需要适应 | 平缓 |
| 适用场景 | 通用 SPA、企业后台、需要灵活控制架构 | 内容站、电商、SEO 敏感型应用、全栈项目 | 注重类型安全的中大型 TS 项目 | 老项目维护,不建议新项目使用 |
| 主要优势 | 灵活通用、生态成熟、嵌套路由强大、v7 全栈化 | SSR/SSG 开箱即用、Vercel 部署零配置、性能优化内置 | 端到端类型安全、搜索参数类型推断、现代 API 设计 | API 简洁、可访问性好、排名匹配 |
| 主要劣势 | v6 之前版本碎片化严重、SSR 需额外配置(v7 前) | 与 Vercel 深度绑定、自建部署复杂、框架约束多 | 生态较新、社区资源相对少、学习成本 | 已停止维护、功能有限、不支持数据加载 |
选型建议:
- 如果你做的是企业内部管理后台、通用 SPA,React Router 是最稳妥的选择,生态成熟、招人容易、灵活度高。
- 如果你的项目强依赖 SEO (官网、博客、电商),或者需要服务端渲染,Next.js 是更优解,它的路由是框架内置能力,不需要单独引入 React Router。
- 如果你的团队极度重视 TypeScript 类型安全,且愿意尝试新事物,TanStack Router 提供了 React Router 目前还不具备的全链路类型推断能力。
- Reach Router 已经正式并入 React Router,新项目不要再使用。
八、常用场景盘点
结合企业项目经验,React Router 在以下场景中发挥着关键作用:
场景一:企业管理后台。 这是 React Router 最经典的应用场景。嵌套路由处理侧边栏布局、动态路由处理详情页、权限守卫控制访问范围、懒加载优化首屏速度,几乎所有特性都能用上。
场景二:电商商品详情与购物车。 商品列表到详情的动态路由、搜索筛选条件通过查询参数持久化(刷新页面条件不丢失)、购物车状态通过路由 state 传递,都是常见用法。
场景三:内容平台与博客。 文章列表、文章详情、分类归档、标签页,通过嵌套路由和动态路由组织内容结构,配合 Data Router 的 loader 预加载文章数据。
场景四:多步骤表单与向导。 注册流程、订单结账等多步骤场景,用路由管理每一步的状态,用户可以通过浏览器前进后退在步骤间切换,刷新也能回到当前步骤。
场景五:登录认证与权限体系。 登录页与主应用分离、登录后跳回来源页、基于角色的路由级权限控制、Token 过期自动跳转登录,这些都是权限守卫的典型应用。
场景六:Tab 切换与面板管理。 页面内的 Tab 切换也可以用路由管理,把 Tab 状态同步到 URL 查询参数,实现可分享、可刷新的 Tab 状态。
九、面试官高频面试题
路由是前端面试的必考点,以下是企业面试中出现频率最高的题目及参考答案,建议收藏。

面试题一:BrowserRouter 和 HashRouter 有什么区别?
参考答案:
两者都是 React Router 的顶层路由容器,核心区别在于 URL 形式和底层实现:
- BrowserRouter(history 模式) :使用 HTML5 History API(
pushState、replaceState、popstate事件),URL 干净美观(如/users/123),但需要服务端配置支持------因为刷新时浏览器会向服务器请求完整路径,如果服务器没有对应资源就会返回 404。解决方案是在 Nginx 等服务器上配置所有路由都 fallback 到index.html,由前端路由接管。 - HashRouter(hash 模式) :使用 URL 的 hash 部分(
#后面的内容),如/#/users/123。hash 变化不会触发服务器请求,所以刷新不会 404,不需要服务端配置。但 URL 中带#,不够美观,且对 SEO 不友好。
选型原则:正式项目优先用 BrowserRouter,配合服务端配置;静态页面、演示项目、无法配置服务端时用 HashRouter。
面试题二:React Router v5 和 v6 有哪些主要区别?
参考答案:
<Switch>改为<Routes>,匹配算法从"按顺序匹配第一个"改为"排名匹配算法",自动选择最优匹配,不再需要exact。component和render属性统一为element,直接传入 JSX 元素。- 嵌套路由回归,子路由 path 支持相对路径,通过
<Outlet />渲染子路由。 useHistory改为useNavigate,API 更简洁。- 移除了
withRouter高阶组件,统一使用 Hooks。 - 移除了对
path-to-regexp的依赖,路径语法更简单,包体积更小。 - 新增
useSearchParams方便操作查询参数。 - v6.4 引入 Data Router(
createBrowserRouter),支持 loader/action/errorElement。
面试题三:如何实现路由守卫(权限控制)?
参考答案:
React Router v6 没有内置守卫 API,通过高阶组件模式实现。创建一个 ProtectedRoute 组件,内部判断登录状态和角色权限,不满足条件时用 <Navigate> 重定向到登录页或 403 页。在路由配置中用 ProtectedRoute 包裹需要保护的组件即可。具体实现见本文 6.6 节。
面试题四:路由参数有哪些传递方式?如何获取?
参考答案:
- 动态路由参数 :在 path 中用
:paramName定义,如/users/:id,通过useParams()获取。 - 查询参数 :URL 中
?key=value部分,通过useSearchParams()获取和设置。 - state 参数 :通过
<Link state={...}>或navigate(to, { state: {...} })传递,通过useLocation().state获取,数据不显示在 URL 中,适合传递敏感或大量数据。 - 路径通配符 :
path="*"匹配任意路径,通过useParams().['*']获取匹配到的剩余路径。
面试题五:history 模式下刷新页面为什么会 404?怎么解决?
参考答案:
原因 :history 模式使用 pushState 修改 URL,但这个操作只改变浏览器地址栏,不会向服务器发送请求。当用户刷新页面时,浏览器会向服务器发起对当前完整 URL 的 HTTP 请求,而服务器上并没有对应的物理文件或路由(因为这是前端路由),所以返回 404。
解决方案 :在服务端配置 URL 重写规则,将所有非静态资源的请求都重定向到 index.html,让前端路由接管页面渲染。以 Nginx 为例:
nginx
location / {
try_files $uri $uri/ /index.html;
}
Apache、Node.js 服务器(Express、Koa)也有对应的配置方式。本地开发时 Vite、Webpack DevServer 已经内置了这个配置,所以本地刷新不会 404,只有部署到生产环境才需要手动配置。
面试题六:React Router 的嵌套路由是如何工作的?
参考答案:
嵌套路由的核心是 <Outlet /> 组件。父路由的 element 中放置 <Outlet /> 作为子路由的渲染占位符。当 URL 匹配到子路由时,React Router 会将子路由的 element 渲染到父组件的 <Outlet /> 位置,父组件的其他部分(导航栏、侧边栏)保持不变。子路由的 path 可以写相对路径(不需要写完整的父路径前缀),React Router 会自动拼接。这种设计实现了布局与内容的分离,是企业级应用中处理复杂布局的标准方案。
面试题七:如何实现路由懒加载?
参考答案:
配合 React 的 React.lazy 和 Suspense 实现。用 lazy(() => import('./pages/xxx')) 动态导入页面组件,用 <Suspense fallback={<Loading />}> 包裹 <Routes>,在组件加载过程中显示 loading 状态。这样每个路由对应的代码会被分割成独立的 chunk,访问时才加载,优化首屏性能。具体实现见本文 6.7 节。
面试题八:<Link> 和 <a> 标签有什么区别?
参考答案:
<a> 标签是浏览器原生的超链接,点击会触发整页刷新,向服务器请求新页面。<Link> 是 React Router 提供的导航组件,底层也是渲染成 <a> 标签,但会拦截点击事件,通过 History API 修改 URL 并触发前端路由匹配,不刷新页面。因此在 SPA 内部导航必须用 <Link>,只有跳转到外部链接时才用原生 <a>。
十、总结
React Router 是 React 生态中不可或缺的基础设施。从最初的集中式配置,到组件化路由,再到如今的 Data Router 和全栈框架化,它的每一次演进都在回应前端应用复杂度提升带来的新需求。
对于开发者来说,掌握 React Router 不仅仅是学会几个 API,更是理解前端路由的本质------URL 与视图的映射、状态与导航的协同、布局与内容的分离。这些思想在任何前端框架中都是通用的。
希望这篇文章能帮你从"会用"进阶到"理解",在企业项目中写出更优雅、更健壮的路由代码。如果觉得有帮助,欢迎点赞收藏,也欢迎在评论区交流你的路由实践经验。
本文为原创文章,如需转载,请联系作者获得授权,并注明出处。