React Router 快速预览
React Router 是 React 官方生态里用来实现“单页应用(SPA)路由” 的库;
React Router = 把浏览器 URL 映射成 React 组件树,并提供导航、参数、嵌套、重定向、懒加载等能力。
核心包(v6 之后)
| 包 | 作用 |
|---|---|
react-router | 核心逻辑 Hook & 组件(不带 DOM 依赖) |
react-router-dom | 浏览器端(Web)专用,依赖 DOM;我们一般装它 |
react-router-native | React-Native 移动端 |
| 安装: |
npm i react-router-dom
# 或
yarn add react-router-dom
最小可运行例子
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
function Home() { return <h2>首页</h2>; }
function About() { return <h2>关于</h2>; }
export default function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">Home</Link> | <Link to="/about">About</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</BrowserRouter>
);
}
BrowserRouter→ 监听浏览器地址栏,提供 HTML5 history。Link→ 声明式跳转(不会整页刷新)。Routes→ 路由匹配容器,只渲染第一个匹配的 Route(v6 不再写Switch)。Route→ path 写匹配规则,element写要渲染的组件。
常用组件 & Hook 速查
| 组件 / Hook | 作用 |
|---|---|
<Routes> | 包裹一组 <Route>,只渲染首个匹配 |
<Route path="..." element={...}> | 定义路径与组件映射 |
<Link to="..."> | 普通跳转 |
<NavLink to="..."> | 带“激活样式”的 Link |
<Navigate to="..." replace /> | 编程式重定向 |
useParams() | 获取动态参数 /user/:id |
useSearchParams() | 获取 ?key=val 查询串 |
useNavigate() | 命令式跳转 navigate('/home') |
useLocation() | 拿到当前 location 对象(pathname、search、state) |
useOutlet() | 在父路由里渲染子路由占位 |
Outlet 组件 | 同上,用于嵌套路由 |
嵌套路由(Outlet)
function Layout() {
return (
<div>
<header>通用头</header>
<Outlet /> {/* 子路由在这里渲染 */}
<footer>通用尾</footer>
</div>
);
}
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} /> // 默认子路由 / 对应
<Route path="about" element={<About />} />
<Route path="user/:id" element={<User />} />
</Route>
</Routes>
- 路径拼接:父 / + 子
about→ 实际/about index路由表示“父路径完全匹配时”显示的组件。
动态参数与获取
// 路由
<Route path="product/:pid" element={<Product />} />
// 组件
function Product() {
const { pid } = useParams(); // { pid: '123' }
return <h2>商品 {pid}</h2>;
}
查询字符串
function Search() {
const [searchParams, setSearchParams] = useSearchParams();
const keyword = searchParams.get('q'); // ?q=react
return (
<div>
当前关键词:{keyword}
<button onClick={() => setSearchParams({ q: 'router' })}>换词</button>
</div>
);
}
编程式导航
import { useNavigate } from 'react-router-dom';
function LoginBtn() {
const nav = useNavigate();
const login = () => {
// 登录成功后
nav('/dashboard', { replace: true }); // replace 不写会留下历史记录
};
return <button onClick={login}>登录</button>;
}
重定向 & 404
<Routes>
<Route path="/" element={<Home />} />
<Route path="old" element={<Navigate to="/new" replace />} />
<Route path="*" element={<NotFound />} /> // 通配符放最后
</Routes>
懒加载(Code Splitting)
import { lazy, Suspense } from 'react';
const Admin = lazy(() => import('./pages/Admin'));
<Route
path="admin"
element={
<Suspense fallback={<div>Loading...</div>}>
<Admin />
</Suspense>
}
/>
路由守卫(权限控制)
React Router 没有内置beforeEach,推荐组合组件实现:
function RequireAuth({ children }) {
const location = useLocation();
const authed = useAuth(); // 自定义 Hook 读登录态
return authed ? children : <Navigate to="/login" state={{ from: location }} replace />;
}
<Route
path="/dashboard"
element={
<RequireAuth>
<Dashboard />
</RequireAuth>
}
/>
数据加载(Loader)React Router v6.4+
// router.js
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
const router = createBrowserRouter([
{
path: '/user/:id',
element: <User />,
loader: async ({ params }) => {
return fetch(`/api/user/${params.id}`).then((r) => r.json());
},
},
]);
// 组件里
import { useLoaderData } from 'react-router-dom';
function User() {
const data = useLoaderData(); // 直接拿到 loader 返回值
return <div>{data.name}</div>;
}
root.render(<RouterProvider router={router} />);
React Router 深入理解
如果是想简单使用的话快速预览版看完就可以快速上手了尝试了,后续内容记录比较长有时间的同学可以继续写往下看~
什么是SPA
SPA 是 Single Page Application 的缩写,中文叫 单页应用。
通俗解释:整个网站只有一个 HTML 页面,浏览器把这张“白纸”下载下来后,所有的页面切换、数据渲染全靠 JavaScript 在本地完成,不再整页刷新。
对比传统多页(MPA)
| 场景 | 传统多页 MPA | 单页 SPA |
|---|---|---|
| 页面跳转 | 点链接 → 浏览器发请求 → 服务器返回全新 HTML|前端 JS 拦截跳转 → 局部改 DOM → 不刷新 | |
地址栏|每次换 .html | 换 URL 但只是历史记录(History API) | |
| 体感 | 白屏 → 闪屏 → 重载 | 快、丝滑、像原生 App |
| SEO | 服务器直接吐出完整 HTML,友好 | 早期空页面,需 SSR/预渲染补偿 |
| 技术栈 | 后端模板为主 | 前端框架为主(React/Vue/Angular) |
实现依赖的核心技术
- 前端路由(React Router、Vue Router):监听地址变化,映射到组件。
- AJAX / Fetch / Axios:无刷新拉接口数据。
- 模板引擎(JSX、Vue SFC):把数据拼成 DOM 片段。
- 浏览器 History API(
pushState/replaceState):让 URL 变,页面不刷新。 - 状态管理(Redux、Pinia、MobX):多组件共享数据。
极简 SPA 演示(原生 JS)
<!DOCTYPE html>
<html>
<body>
<nav>
<a href="/" onclick="go('/')">Home</a>
<a href="/about" onclick="go('/about')">About</a>
</nav>
<div id="app">Loading...</div>
<script>
function go(path) {
history.pushState(null, null, path); // 只改地址栏
render(path);
}
function render(path) {
const map = {
'/': '<h1>Home</h1>',
'/about': '<h1>About</h1>'
};
document.getElementById('app').innerHTML = map[path] || '<h1>404</h1>';
}
window.addEventListener('popstate', () => render(location.pathname));
render(location.pathname); // 首次
</script>
</body>
</html>
只有一个index.html,路由切换全靠 JS → 这就是最原始的 SPA。
优点
- 体验丝滑:无白屏,过渡动画容易做。
- 前后端彻底分离:后端只给 JSON,前端完全掌控 UI。
- 本地状态保持:切换页面不丢表单、不丢滚动条。
- 移动端友好:很容易套壳变成 Hybrid App 或 PWA。
缺点
- 首屏慢:要加载整个 JS 框架 + 路由 + 业务代码。
- SEO 难题:爬虫早期看不到内容 → 需要 SSR/预渲染。
- 浏览器前进/后退管理复杂:要自己维护历史栈。
- 内存泄漏风险:单页永不刷新,定时器、事件监听忘记清理就会堆积。
解决痛点的现代方案
| 问题 | 方案 |
|---|---|
| 首屏白屏大 | 代码分割 + 懒加载(React.lazy、Vite、Webpack) |
| SEO 不友好 | 服务端渲染 SSR(Next.js、Nuxt、Remix)或静态预渲染 SSG |
| 状态易乱 | Redux Toolkit、Pinia、Recoil 等集中式状态管理 |
什么是路由
路由就是“URL → 资源”的映射表,负责根据浏览器地址决定展示什么内容。
生活化比喻
把网站想成一座商场:
- URL = 门牌号(
/shop/shoes/nike) - 路由系统 = 楼层指示牌 + 自动扶梯,告诉你“去这个门牌号该进哪家店”
- 页面/组件 = 真正的店铺
两种语境
| 语境 | 路由含义 |
|---|---|
| 后端(传统) | 把 URL 映射到服务器文件或接口GET /api/user → 控制器 → 返回 JSON/HTML |
| 前端(SPA) | 把 URL 映射到本地组件或视图/user → 加载 User.vue / User.jsx 无需重新请求 HTML |
前端路由核心原理(SPA)
- 改 URL 不刷新页
history.pushState()或location.hash = '#xxx'
- 监听变化
window.onpopstate/hashchange
- 查表 → 渲染对应组件
- 路由表:
[{ path: '/home', component: Home }]
- 路由表:
- 浏览器历史栈依旧可用
- 前进/后退由 JS 接管,用户体验与多页无差别
极简原生实现(哈希路由的雏形)
<a href="#/home">Home</a>
<a href="#/about">About</a>
<div id="view"></div>
<script>
const routes = {
'/home': '<h1>Home</h1>',
'/about': '<h1>About</h1>'
};
function router() {
const path = location.hash.slice(1) || '/home';
document.getElementById('view').innerHTML = routes[path] || '<h1>404</h1>';
}
window.addEventListener('hashchange', router);
router(); // 首次
</script>
React Router 示例(HTML5 路由)
<Routes>
<Route path="/" element={<Home />} />
<Route path="/product/:id" element={<Product />} />
<Route path="*" element={<NotFound />} />
</Routes>
- 访问
/product/123→ 自动渲染<Product>并可通过useParams()拿到id = 123
路由常见能力
| 能力 | 说明 |
|---|---|
| 嵌套 | /user/profile → 先加载 UserLayout,再在内部 Outlet 里放 Profile |
| 参数 | 动态段 :id、查询串 ?tab=active |
| 重定向 | 访问 /old → 自动跳到 /new |
| 懒加载 | 进入路由才拉 JS 包,减少首屏体积 |
| 守卫 | 登录态校验,没登录跳登录页 |
| 命名路由/别名 | 生成路径时不用硬编码 |
后端路由 vs 前端路由速览
| 维度 | 后端路由 | 前端路由 |
|---|---|---|
| 地点 | 服务器 | 浏览器 |
| 刷新 | 整页重新加载 | 无刷新,局部替换 |
| 返回内容 | HTML/JSON | 组件片段 |
| SEO | 天然友好 | 需 SSR/预渲染 |
| 体验 | 传统 | 原生 App 级顺滑 |
React Router的理解
React-Router 是 React 的 官方路由库,专门负责在 单页应用(SPA) 里实现:“URL 变 → 组件自动切换,页面不刷新”
通俗理解:
React-Router 把 浏览器地址栏 变成 React 的 状态机,通过 声明式路由表 让 “URL ≡ 组件树”,并提供 导航、参数、嵌套、懒加载、鉴权 等全套能力。
核心思想
| 概念 | 说明 |
|---|---|
| 声明式 | 路由写进 JSX,跟 UI 一起描述:“什么地址渲染什么组件” |
| 动态 | 地址变化即重新匹配,组件自动卸载/挂载 |
| 可嵌套 | 路由可以像组件一样层层嵌套,天然对应 UI 布局 |
| 状态同步 | URL ↔ 组件状态 双向同步,刷新不丢 |
最小可运行代码
import { BrowserRouter, Routes, Route, Link, useParams } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<nav>
<Link to="/">Home</Link> | <Link to="/user/123">User</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/user/:id" element={<User />} />
</Routes>
</BrowserRouter>
);
}
function Home() { return <h1>Home</h1>; }
function User() {
const { id } = useParams(); // 拿到 123
return <h1>User {id}</h1>;
}
- 访问
/user/123→ 自动渲染<User id=123> - 按
F5刷新 → 服务器仍返回同一个index.html,JS 再按URL渲染对应组件
内部机制
- Router 上下文
- 顶层 BrowserRouter 创建 history 实例,监听 popstate
- 匹配算法
- 根据当前 location.pathname 遍历 Routes 子节点,找到最佳匹配 Route
- 渲染阶段
- 把匹配到的 element 作为 children 注入到 Outlet 位置
- 状态提升
- 路由信息通过 RouterContext 共享,任意组件可用 useLocation / useNavigate 等 Hook 读取或修改
常用 API 速查
| 组件 / Hook | 作用 |
|---|---|
<BrowserRouter> | HTML5 路由(推荐) |
<HashRouter> | Hash 路由(兼容老服务器) |
<Routes> | 路由匹配容器 |
<Route path="..." element={...}> | 路径→组件映射 |
<Link to="..."> | 声明式跳转 |
<NavLink> | 带激活样式的 Link |
<Navigate> | 重定向 |
useParams() | 动态参数 :id |
useSearchParams() | 查询串 ?key=val |
useNavigate() | 命令式 navigate('/home') |
useLocation() | 当前 location 对象 |
<Outlet> | 渲染子路由占位 |
进阶能力
| 能力 | 实现方式 |
|---|---|
| 嵌套路由 | 父 Route 里放 <Outlet>,子 Route 写在 children |
| 懒加载 | lazy(() => import('./Page')) + <Suspense> |
| 路由守卫 | 自定义组件包一层 {authed ? <Outlet /> : <Navigate to="/login" />} |
| 面包屑/菜单高亮 | 用 useMatches() 或 NavLink end |
| 数据加载 | v6.4+ loader + useLoaderData()(类似 Next.js) |
版本变化(v5 → v6 最痛三点)
<Switch>→<Routes>component={Home}→element={<Home />}- 不再支持
exact,默认就是精确匹配;通配用path="*"
易踩坑
- 把
Router包在React.StrictMode外面,否则热更新会重复创建history Link只能用在Router上下文里,别在组件外裸用- 用
index路由表示“父路径完全匹配时”显示的组件,不是/index - 嵌套路径不要写前导
/,否则会从根开始算
嵌套路由的使用
嵌套路由 = 把“路由”按组件层级一样层层嵌套,让 URL 段落 自动对应 布局层级,一行配置就能同时解决“页面框架 + 内容区切换”。
通俗理解:父路由提供骨架(Layout),子路由只替换骨架里的
<Outlet>局部内容。
场景举例
管理后台常见结构:
/setting/profile → SettingLayout 框架 + Profile 页
/setting/account → SettingLayout 框架 + Account 页
/setting/security → SettingLayout 框架 + Security 页
框架不动,右侧内容随 URL 变 → 这就是嵌套路由的典型用法。
核心 API
| API | 作用 |
|---|---|
<Outlet /> | 占位符:告诉 React-Router“子路由组件放这儿” |
| children 写法 | 在父 Route 内部继续写 Route |
| index 路由 | 父路径“完全匹配”时显示的默认子页 |
最小完整例子(v6)
import { Routes, Route, Outlet, Link, useParams } from 'react-router-dom';
/* ---------------- 骨架组件 ---------------- */
function SettingLayout() {
return (
<div style={{ display: 'flex' }}>
<aside>
<h3>设置菜单</h3>
<Link to="/setting/profile"> 个人信息 </Link>
<Link to="/setting/account"> 账号绑定 </Link>
</aside>
<main>
<Outlet /> {/* 子路由页面会插到这里 */}
</main>
</div>
);
}
/* ---------------- 子页面 ---------------- */
function Profile() { return <h2>Profile 内容</h2>; }
function Account() { return <h2>Account 内容</h2>; }
/* ---------------- 路由配置 ---------------- */
export default function App() {
return (
<Routes>
{/* 父路由:路径前缀 + 骨架组件 */}
<Route path="setting" element={<SettingLayout />}>
{/* 默认子页 */}
<Route index element={<Profile />} />
{/* 子路由:路径自动拼接成 /setting/xxx */}
<Route path="profile" element={<Profile />} />
<Route path="account" element={<Account />} />
</Route>
</Routes>
);
}
访问验证:
/setting→ 显示SettingLayout+ 默认Profile/setting/profile→ 框架不变,右侧Profile/setting/account→ 框架不变,右侧Account
路径规则速记
| 写法 | 实际 URL | 说明 |
|---|---|---|
父path="setting" | /setting | 不要前导 / |
子path="profile" | /setting/profile | 自动拼接 |
子path="/profile" | /profile | 带前导 / 会脱离嵌套! |
三级嵌套
<Route path="docs" element={<DocsLayout />}>
<Route index element={<DocsHome />} />
<Route path="guide" element={<Guide />}>
<Route index element={<GuideIntro />} />
<Route path="quick" element={<QuickStart />} />
<Route path="advanced" element={<Advanced />} />
</Route>
</Route>
URL → 组件
/docs/guide→DocsLayout+Guide骨架 + 默认GuideIntro/docs/guide/quick→ 前两层骨架不变 +QuickStart内容
动态参数 + 嵌套
<Route path="shop" element={<ShopLayout />}>
<Route path=":category" element={<Category />}>
<Route path=":id" element={<Product />} />
</Route>
</Route>
/shop/electronics/123在ShopLayout里渲染Category,在Category的<Outlet>里渲染Product各层分别用useParams()拿到:category/:id
相对链接写法
子组件里不写全路径,用相对地址更稳:
<Link to="quick">快速开始</Link>
- 当前父路径是
/docs/guide,点击后自动变成/docs/guide/quick
breadcrumbs(面包屑)思路
利用useMatches()拿到匹配数组,逐层展示:
const matches = useMatches(); // [{pathname, handle}, ...]
- 每层路由在
handle里放标题即可生成动态面包屑。
常见坑
- 忘了写
<Outlet>→ 子路由不显示也不报错 - 子路由加前导
/→ 跳出嵌套,父布局消失 - 用
exact思维 → v6 已废弃,路径前缀匹配自动完成 - 热更新后
Link白屏 → 检查是否包在BrowserRouter内部
组件传参方式
React Router 里 “路由组件” 拿数据有 4 条官方通道:
- 路径参数(动态段)
- 查询参数(?key=val)
- 状态参数(location.state,隐身传值)
- 哈希参数(#hash,很少用)
路径参数(params)
场景:/user/123、/shop/laptop/1001
// 1. 声明
<Route path="/user/:id" element={<User />} />
// 2. 跳转
<Link to="/user/123">张三</Link>
navigate('/user/123')
// 3. 接收
import { useParams } from 'react-router-dom';
function User() {
const { id } = useParams(); // { id: '123' }
}
特点
- 必填、可见、可收藏、可 SEO
- 只能传字符串,需自己转数字/布尔
查询参数(search / query)
场景:/search?q=react&page=2
// 1. 跳转(字符串)
<Link to="/search?q=react&page=2">第2页</Link>
// 2. 跳转(对象写法)
import { useSearchParams } from 'react-router-dom';
function Pagination() {
const [searchParams, setSearchParams] = useSearchParams();
// 读
const keyword = searchParams.get('q'); // react
const page = Number(searchParams.get('page') || 1);
// 写
const goPage = p => setSearchParams({ q: keyword, page: p });
}
特点
- 可缺省、可重复 key(
?tag=js&tag=node) - 刷新不丢,适合列表过滤、分页、tab 激活
状态参数(location.state)
场景:跳转到编辑页时把整行记录“隐身”带过去,URL 里看不到
// 1. 跳转时塞状态
navigate('/edit/123', { state: { row: { name: 'Tom', age: 18 } } })
// 或 Link
<Link to="/edit/123" state={{ row }}>编辑</Link>
// 2. 接收
import { useLocation } from 'react-router-dom';
function Edit() {
const location = useLocation();
const row = location.state?.row; // 整行数据
}
特点
- 不在地址栏出现,刷新后消失(SPA 内跳转无妨,外站打开就空)
- 适合一次性、敏感、大数据传递
哈希参数(hash)
场景:锚点定位或旧版 HashRouter
const location = useLocation();
console.log(location.hash); // '#section3'
- 很少主动传业务数据,只做页面内锚点。
实战综合例子
// 列表页:点击“编辑”带查询关键字 + 整行数据
function Row({ item }) {
const [searchParams] = useSearchParams();
const keyword = searchParams.get('q');
return (
<Link
to={`/edit/${item.id}`}
state={{ row: item, backKeyword: keyword }}
>
编辑
</Link>
);
}
// 编辑页:三种参数一起拿
function Edit() {
const { id } = useParams(); // 路径参数
const [searchParams] = useSearchParams(); // 查询参数
const location = useLocation(); // 状态参数
const row = location.state?.row;
const backKeyword = location.state?.backKeyword;
const save = () => {
api.update(id, row).then(() => {
navigate(`/list?q=${encodeURIComponent(backKeyword)}`);
});
};
}
对比速查表
| 方式 | 可见 | 刷新留存 | 适合数据类型 | 用法 |
|---|---|---|---|---|
| params | 地址栏 | ✔ | 短、必填标识 | useParams() |
| search | 地址栏 | ✔ | 过滤、分页、可选 | useSearchParams() |
| state | 不可见 | ✖ | 大对象、一次性 | location.state |
| hash | 地址栏 | ✔ | 锚点定位 | location.hash |
易踩坑
location.state刷新消失 → 编辑页要做“空值兜底”再拉接口searchParams.get()总返回字符串,转数字别忘了Number()- 路径参数区分大小写;search 不区分
- 对象写法
setSearchParams({ arr: [1,2] })会生成arr=1&arr=2,符合标准但后端需按数组接
路由的跳转方式
在 React-Router 里,“怎么跳” 分两大阵营:
- 声明式跳转 —— 写
JSX - 命令式跳转 —— 写
JS逻辑
声明式(模板里直接写)
| 组件 | 用法 | 场景 |
|---|---|---|
<Link> | <Link to="/user/123">详情</Link> | 普通锚点,可 SEO,默认替换 <a> |
<NavLink> | <NavLink to="/user" end>用户</NavLink> | 带“激活样式”的 Link,自动 .active |
<Navigate> | <Navigate to="/login" replace /> | 组件内部重定向(渲染即跳转) |
命令式(JS 里想跳就跳)
函数组件 ——useNavigateHook
import { useNavigate } from 'react-router-dom';
function LoginBtn() {
const nav = useNavigate();
const submit = async () => {
await api.login(form);
nav('/dashboard'); // 默认 push(可后退)
// nav('/dashboard', { replace: true }); // 不想让用户后退
};
return <button onClick={submit}>登录</button>;
}
类组件 —— 高阶组件注入(withRouter)
import { withRouter } from 'react-router-dom';
class LoginBtn extends React.Component {
submit = async () => {
await api.login(form);
this.props.navigate('/dashboard'); // 被注入
};
}
export default withRouter(LoginBtn);
路由外跳转(Redux-Saga、axios 拦截等)
import { createBrowserHistory } from 'history';
export const history = createBrowserHistory({ window });
// 任意文件
history.push('/login'); // 跳
history.replace('/404'); // 替换
history.back(); // 后退
history.go(-2); // 回退 2 步
注意:React-Router v6官方推荐 把navigate函数通过参数/上下文传进去,而不是直接import history实例;除非你在非React上下文里。
跳转目标写法(to 的 4 种形态)
// 1. 字符串
navigate('/user/123')
// 2. 对象(pathname + search + hash + state)
navigate({
pathname: '/user/123',
search: '?tab=profile',
hash: '#section3',
state: { from: 'table' } // 隐身传值
})
// 3. 函数式(基于当前 location)
navigate(loc => ({ ...loc, search: '?page=2' }))
// 4. 相对路径
navigate('../edit') // 当前段 + ../
navigate('detail') // 当前段 + detail
其他选项
| 选项 | 说明 |
|---|---|
replace: true | 替换当前记录,不可后退(登录后常用) |
state: object | 隐身传值,刷新消失 |
resetScroll: true | 跳转后滚动到顶部(默认行为,可关) |
至此从单页面(SPA)的介绍、路由的理解、嵌套使用路由、路由如何传参及跳转就彻底介绍完了,内容较多,感谢耐心阅读,欢迎留言指正~