React学习笔记(三)

178 阅读4分钟

React Router 快速预览

React Router 是 React 官方生态里用来实现“单页应用(SPA)路由” 的库;

React Router = 把浏览器 URL 映射成 React 组件树,并提供导航、参数、嵌套、重定向、懒加载等能力。

核心包(v6 之后)

作用
react-router核心逻辑 Hook & 组件(不带 DOM 依赖)
react-router-dom浏览器端(Web)专用,依赖 DOM;我们一般装它
react-router-nativeReact-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)

  1. 改 URL 不刷新页
    • history.pushState()location.hash = '#xxx'
  2. 监听变化
    • window.onpopstate/hashchange
  3. 查表 → 渲染对应组件
    • 路由表:[{ path: '/home', component: Home }]
  4. 浏览器历史栈依旧可用
    • 前进/后退由 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渲染对应组件

内部机制

  1. Router 上下文
    • 顶层 BrowserRouter 创建 history 实例,监听 popstate
  2. 匹配算法
    • 根据当前 location.pathname 遍历 Routes 子节点,找到最佳匹配 Route
  3. 渲染阶段
    • 把匹配到的 element 作为 children 注入到 Outlet 位置
  4. 状态提升
    • 路由信息通过 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 最痛三点)

  1. <Switch><Routes>
  2. component={Home}element={<Home />}
  3. 不再支持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/guideDocsLayout+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/123ShopLayout里渲染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 条官方通道:

  1. 路径参数(动态段)
  2. 查询参数(?key=val)
  3. 状态参数(location.state,隐身传值)
  4. 哈希参数(#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 里,“怎么跳” 分两大阵营:

  1. 声明式跳转 —— 写JSX
  2. 命令式跳转 —— 写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)的介绍路由的理解嵌套使用路由路由如何传参及跳转就彻底介绍完了,内容较多,感谢耐心阅读,欢迎留言指正~