React Router

2 阅读13分钟

一、你每天都在用路由,只是你不知道而已

打开淘宝刷商品、在 B 站看视频、用 GitHub 浏览代码仓库——每一次点击链接、每一次页面跳转,背后都有一个"路由系统"在默默工作。路由(Routing),说白了就是"根据不同的地址,展示不同的内容"。

在传统的 Web 开发中,这个活儿是后端干的。比如你访问 https://example.com/products/123,浏览器会向服务器发送一个 HTTP 请求,服务器收到后去数据库查商品编号为 123 的信息,然后把拼好的 HTML 页面返回给浏览器。这个过程被称为"服务端路由"或"多页应用(MPA,Multi-Page Application)"。

听起来很合理,对吧?但它有个致命的体验问题:每次跳转都会出现短暂的白屏。因为浏览器需要销毁当前页面、重新请求、重新解析、重新渲染。在网络不好的时候,用户就只能盯着白屏发呆。这在用户体验至上的今天,是不可接受的。

于是,单页应用(SPA,Single Page Application) 应运而生。SPA 的核心理念是:整个应用只加载一个 HTML 页面,之后所有的"页面切换"都由 JavaScript 在前端完成——不向服务器发请求,不刷新页面,只是把页面上的内容换掉。这样一来,切换就变得"丝滑"了,没有白屏,没有等待。

而实现这种"丝滑切换"的关键技术,就是前端路由。


二、前端路由的两种实现方式:Hash 模式 vs History 模式

前端路由的核心问题可以简化为:如何在不刷新页面的前提下,改变浏览器地址栏的 URL,同时让页面内容跟着变?

聪明的工程师们找到了两个突破口。

2.1 Hash 模式:利用 URL 的"锚点"

URL 中的 # 符号(也叫 hash 或锚点)有一个独特的性质:改变 # 后面的内容,浏览器不会向服务器发送请求,也不会刷新页面。这是 HTML 设计之初就有的特性——# 本来是用来做页面内定位的(比如跳转到某个标题),但前端开发者发现了它的新用途。

当你访问 https://example.com/#/products/123 时,浏览器只把 # 前面的部分(https://example.com/)发给服务器,# 后面的内容(/products/123)完全由前端处理。当 hash 发生变化时,浏览器会触发 hashchange 事件,前端代码监听这个事件,根据不同的 hash 值渲染不同的页面内容。

在我们分析的 react-router-demo 项目中,使用的正是 Hash 模式:

import { HashRouter as Router } from 'react-router-dom';

项目中所有页面的 URL 都会带上 #,比如 #/about、#/user/123、#/products/456。优点是兼容性好,不需要服务器端做任何配置;缺点就是 URL 里有个不怎么美观的 #。

2.2 History 模式:利用 HTML5 History API

HTML5 引入了 history.pushState() 和 history.replaceState() 两个 API,允许开发者在不刷新页面的情况下修改浏览器地址栏的 URL。更棒的是,这种方式不需要 #,URL 看起来跟普通的后端路由完全一样,比如 /about、/products/123。

不过 History 模式有一个"坑":当你直接访问 /products/123 或者刷新页面时,浏览器会把这个路径当作一个真实的 HTTP 请求发到服务器。如果服务器没有对应的处理逻辑,就会返回 404。因此使用 History 模式需要在服务器端做 fallback 配置——把所有前端路由的路径都指向 index.html。

React Router 中把这两种模式封装为两个组件:HashRouter 和 BrowserRouter,使用时只需替换最外层的包装组件即可,非常方便。


三、走进 React Router:核心三件套

React Router 是 React 生态中最流行的路由库,安装方式非常简单:

npm install react-router-dom

安装完成后,我们从最核心的三个组件开始认识它——Router、Routes 和 Route。你可以把它们理解为一个路由系统的三个层级:

  • Router:整个路由系统的"大管家",负责监听 URL 变化并提供路由上下文。在 demo 项目中,<HashRouter> 包在最外层,意味着它接管了所有的路由逻辑。
  • Routes:一组路由规则的"容器",负责匹配当前的 URL 并决定渲染哪个页面组件。可以把它想象成一个"规则匹配器"——它会从上到下扫描所有 Route,找到第一个匹配的来渲染。
  • Route:单条路由规则,指定"当 URL 是这个路径时,渲染那个组件"。

三者的配合可以简化成这种结构:

<Router>
  <Navigation />
  <Routes>
    <Route path='/' element={<Home />} />
    <Route path='/about' element={<About />} />
    <Route path='/user/:id' element={<UserProfile />} />
    <Route path='*' element={<NotFound />} />
  </Routes>
</Router>

这里面有几个值得注意的细节:

  1. path='/' 是根路径,也就是首页。用户在地址栏输入应用地址后最先看到的就是它。
  2. path='/about' 是"关于"页面,非常直观,URL 和路径一一对应。
  3. path='/user/:id' 中的 :id 是一个动态参数——冒号开头的路径片段可以匹配任意值,比如 /user/123 和 /user/abc 都会命中这条规则。
  4. path='*' 中的 * 是一个通配符,匹配所有未被前面规则命中的路径,通常用来做 404 页面。

四、导航组件 Link:为什么不能用 <a> 标签?

初学前端路由时,很多人会有一个疑问:为什么不用 <a href="/about"> 来做页面跳转?

答案很简单:<a> 标签会导致页面刷新,这就违背了 SPA 的初衷。点击一个 <a> 链接,浏览器会认为你要去一个新的页面,于是发起 HTTP 请求、销毁当前页面、加载新页面——白屏又回来了。

React Router 提供了专门的导航组件 Link,它在底层拦截了点击事件,使用 History API 或 hash 更新 URL,不刷新页面,只替换内容。在我们的 demo 项目中,导航栏就是这样实现的:

import { Link } from 'react-router-dom';

function Navigation() {
  return (
    <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>
  );
}

从用户视角看,点击这些链接跟普通链接没什么区别——URL 变了,内容也变了。但技术上,这一切都是在前端完成的,没有触发任何 HTTP 请求。除此之外,Link 还支持 NavLink 变体,可以在"当前活跃的链接"上自动添加样式类名,非常方便制作导航高亮效果。


五、动态路由与 useParams:一个页面模板,千种内容

设想一个电商应用:如果系统里有 10 万个商品,难道要写 10 万条 Route 吗?显然不可能。

动态路由解决了这个问题。一条带参数的 Route 可以匹配无数个具体路径,然后通过 useParams Hook 把 URL 中的参数提取出来,在组件内部使用。就像函数一样——定义一个带参数的"模板页面",不同的参数值产生不同的内容。

Demo 项目中的用户资料页展示了这个模式:

// App.jsx 中的路由配置
<Route path='/user/:id' element={<UserProfile />} />

// UserProfile 组件
import { useParams } from 'react-router-dom';

function UserProfile() {
  let { id } = useParams();
  return <h2>User Profile: {id}</h2>;
}

当用户访问 /user/123 时,useParams() 返回 { id: '123' };访问 /user/abc 时,返回 { id: 'abc' }。拿到这个参数后,组件可以去请求对应的用户数据、展示不同用户的信息——一个组件,服务所有用户。

商品详情页也是同样的逻辑:

const { productId } = useParams();
// productId 就是从 /products/123 中提取的 "123"

从软件工程的角度看,动态路由将"路由的复用性"发挥到了极致,本质上是参数化 UI 思想在前端路由中的体现——用可变的参数驱动组件渲染出不可变的功能。


六、嵌套路由与 Outlet:页面的"俄罗斯套娃"

真实的应用界面往往是有层级结构的。以电商后台为例:左侧是菜单栏,右侧是内容区;点击"商品管理",右侧显示商品列表;再点击某个商品,右侧变成商品详情。在这个交互中,左侧菜单始终不变,只有内容区在动态切换。

React Router 通过**嵌套路由(Nested Routes)**和 <Outlet /> 优雅地解决了这种"布局复用"问题。

Demo 项目中的商品模块就是一个典型案例:

// App.jsx —— 一级路由 + 嵌套的二级路由
<Route path='/products' element={<Products />}>
  <Route path=':productId' element={<ProductDetail />} />
  <Route path='new' element={<NewProduct />} />
</Route>

/products 是一个一级路由,它渲染 <Products /> 组件。而在 Products 组件内部,放了一个 <Outlet />:

import { Outlet } from 'react-router-dom';

const Products = () => {
  return (
    <>
      <h1>产品列表</h1>
      <Outlet />
    </>
  );
};

<Outlet /> 是一个"插槽"——当 URL 匹配到二级路由时,对应的子组件就会被渲染到这个位置:

  • /products/new → 在 Outlet 位置显示 <NewProduct />
  • /products/123 → 在 Outlet 位置显示 <ProductDetail />

这样做的好处显而易见:<h1>产品列表</h1> 在父组件中只写了一处,所有子页面自动共享这个标题。扩展到更复杂的场景,侧边栏、顶栏、面包屑导航等公共 UI 都可以通过嵌套路由的 Outlet 机制来统一管理,避免了在每个子页面中重复编写布局代码。

嵌套路由可以无限层级地深入——你可以有 /products/:productId/reviews/:reviewId 这样的三级甚至四级嵌套,只要每一层在对应位置放好 <Outlet />,React Router 就能正确渲染。这就像俄罗斯套娃,一层套一层,结构清晰又高度复用。


七、路由重定向:让用户"不迷路"

在实际应用中,经常会遇到路由变更的场景:某个旧版页面被废弃了,需要引导用户去新的地址;或者用户访问了一个需要登录的页面,没登录就跳转到登录页。

React Router 提供了 Navigate 组件来处理"重定向":

<Route path='/old-path' element={<Navigate replace to="/new-path" />} />

当用户访问 /old-path 时,React Router 会自动把 URL 替换为 /new-path 并渲染新页面的内容。replace 属性的作用是"替换"当前的历史记录条目——意味着用户点击浏览器的"后退"按钮时,不会回到 /old-path,而是回到更早的页面。这在重定向场景下是更合理的体验,毕竟用户并不关心那个已经被废弃的旧地址。

除了声明式的 Navigate,React Router 还提供了编程式的 useNavigate Hook。Demo 项目中的 404 页面就用到了它:

import { useNavigate } from 'react-router-dom';

const NotFound = () => {
  let navigate = useNavigate();
  useEffect(() => {
    setTimeout(() => {
      navigate('/');  // 3 秒后自动跳回首页
    }, 3000);
  }, []);
  return <>Not Found</>;
};

useNavigate 让你可以在任何逻辑中触发页面跳转——比如表单提交成功后跳转到成功页、检测到登录过期后跳转到登录页、或者像这个例子中一样,在 404 页面上给用户几秒钟时间查看提示信息,然后自动回到首页。这种"编程式导航"赋予了开发者更大的灵活性,不再局限于用户点击链接这一种触发方式。


八、路由懒加载:让首屏"飞"起来

随着应用规模的增长,JavaScript 打包文件会越来越庞大。如果用户访问首页时需要下载整个应用的代码(包括那些用户可能根本不会访问的页面),首页加载速度就会变得很慢。

路由懒加载正是为了解决这个问题。核心思路很朴素:用户当前访问哪个页面,就只加载哪个页面的代码,其他的以后再说。

React 提供了 React.lazy() 和 Suspense 来支持组件的动态导入:

import { lazy, Suspense } from 'react';

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'));

// 使用时包裹 Suspense
<Suspense fallback={<div>Loading...</div>}>
  <Routes>
    <Route path='/' element={<Home />} />
    {/* ...其他路由 */}
  </Routes>
</Suspense>

lazy(() => import('./pages/About')) 告诉构建工具(如 Vite 或 Webpack):"这个模块不要打进主包里,单独拆出来"。当用户导航到 /about 时,React Router 发现需要渲染 About 组件,才会去加载对应的代码包。而在代码下载期间,Suspense 的 fallback 属性会显示一个加载指示器(比如 "Loading..." 或一个骨架屏)。

这样做的好处是实实在在的:假设你的完整应用打包后有 2MB,首屏可能只需要其中的 200KB。在移动网络或者弱网环境下,这个差距可能就是"秒开"和"等了 5 秒还在转圈"的区别。对于用户体验和 SEO 来说,首屏加载速度都是至关重要的指标。


九、404 页面与兜底机制:优雅地处理"找不到"

无论路由设计得多完善,用户总会碰到"找不到"的情况——输入了错误的 URL、点击了失效的链接、或者收藏的书签对应的页面已经被删除。一个好的前端应用应该有"兜底"机制。

在 React Router 中,path='*' 就是兜底方案:

<Route path='*' element={<NotFound />} />

它使用通配符 * 匹配所有未被前面路由规则捕获的路径。这条规则必须放在 <Routes> 的最后面,因为 React Router 是按顺序匹配的——如果放在前面,所有路径都会被它"吃掉"。

Demo 项目中的 404 页面不仅展示了"找不到"的信息,还用 useNavigate 实现了 3 秒后自动跳回首页的功能,这是一个很好的用户体验细节——与其让用户在 404 页面上不知所措,不如主动引导他们回到有效页面。


十、总结:前端路由的技术全景

回顾一下,我们通过一个完整的 Demo 项目,梳理了前端路由从"为什么需要"到"怎么实现"的完整知识链:

  1. 为什么需要前端路由? 告别多页应用的白屏刷新,实现 SPA 的丝滑切换。
  2. 两种实现模式:Hash 模式(# + hashchange 事件)兼容性好,History 模式(HTML5 History API)URL 更美观。
  3. 核心 API 三件套:Router 提供路由上下文,Routes 做规则匹配,Route 定义具体的路径-页面映射关系。
  4. 导航组件 Link:替代 <a> 标签,实现无刷新的页面跳转。
  5. 动态路由与 useParams:用一条路由规则匹配无限个具体路径,通过参数驱动内容变化。
  6. 嵌套路由与 Outlet:实现布局复用,像俄罗斯套娃一样组织页面层级。
  7. 重定向与编程式导航:Navigate 组件处理声明式跳转,useNavigate Hook 支持逻辑驱动的跳转。
  8. 路由懒加载:按需加载页面代码,大幅提升首屏性能。
  9. 404 兜底:path='*' 通配符捕获未匹配路径,优雅处理"找不到"的情况。

前端路由不仅仅是一种技术实现,更是一种架构思维的体现——将 URL 视为应用状态的一部分,用声明式的方式描述"什么地址展示什么内容"。理解了前端路由,你就理解了 SPA 应用跳转的灵魂。

对于正在学习前端开发的大学生来说,React Router 是进入"现代 Web 应用开发"世界的一把钥匙。掌握它之后,你会发现那些曾经觉得"神秘"的页面切换效果——比如浏览器地址栏变化但页面不刷新、多级菜单与面包屑导航的联动、甚至移动端 App 中"页面栈"的切换动画——背后的原理都是相通的。

从"刷新白屏"到"丝滑切换",前端路由让 Web 应用的用户体验上了一个台阶。而 React Router,正是这条进化之路上最重要的同行者之一。