从前端路由的起源到 React Router 实战

从前端路由的起源到 React Router 实战

一、传统的多页面时代

在互联网的早期,万物互联靠的是链接

ini 复制代码
<a href="http://127.0.0.1:5500/lj-ai/fe/history/demo/index.html">首页</a>
<a href="http://127.0.0.1:5500/lj-ai/fe/history/demo/about.html">关于我们</a>

点击一个链接,浏览器的工作流程是这样的:

  1. URL 作为浏览器访问代理,向服务器发起 HTTP 请求
  2. 服务器处于伺服状态 ,收到请求后返回 text/html 响应
  3. 浏览器拿到响应数据,重新渲染整个页面
  4. 浏览器历史记录中插入一条记录

这就是传统的多页面应用。每次点击链接,浏览器都会经历"白一下"的过程------尤其网速慢的时候,整个页面重新渲染的体验非常糟糕。

到了移动互联网时代,用户已经习惯了 App 那种流畅的体验,传统多页面的"全量刷新"模式显得越来越没有必要。

二、单页应用(SPA)的诞生

核心思想很简单:怎么把丰富的内容在一个网页里显示?

答案是通过 DOM 编程------根据不同的 URL,动态地将对应内容渲染到同一个容器中:

bash 复制代码
/ → index.html 的 content DOM 放到 #container
/about → about.html 的 content DOM 放到 #container

但这里有一个关键问题:怎么改变 URL 而不触发页面跳转?

三、Hash 路由------前端路由的基石

答案藏在 URL 的结构中:

ruby 复制代码
http(s)://www.baidu.com/u/123?a=1&b=2#/page1
└─protocol─┘ └───host───┘ └path┘ └─queryString─┘└─hash─┘

URL 中 # 开始的部分叫做 hash 。它的特点是:改变 hash 部分,URL 会变化,但页面不会刷新

hash 最初的设计目的是做锚链接------在传统的 PC 长页面中标记某个位置,像电梯一样直达:

css 复制代码
<a name="top"></a>
<a href="#bottom">去到底部</a>
<div style="height:100vh;background-color: yellow;"></div>
<a href="#top">回到顶部</a>
<div style="height:100vh;background-color: red;"></div>
<a name="bottom"></a>

前端开发者巧妙地利用了这一特性来做前端路由 :使用 #/#/about 这样的 hash 值,既能让 URL 改变(满足 URL 与资源的一一对应关系),又不会触发页面重新渲染。

当 hash 部分改变时,浏览器会触发 hashchange 事件,我们只需要监听这个事件,然后替换对应的 DOM 或组件即可:

javascript 复制代码
window.addEventListener('hashchange', function(event) {
    console.log('hash 改变了');
    console.log(event.newHash);
    console.log(event.oldHash);
})

四、手写一个 Hash 路由器

javascript 复制代码
class HashRouter {
    constructor() {
        // 前端路由集合------前后端分离后,前端也要有独立的路由
        this.routes = {};
        // 监听 hashchange 事件
        // 注意:回调中 this 默认指向 window,需要用 bind 修正 this 指向
        window.addEventListener('hashchange', this.load.bind(this));
    }
    // 注册路由:将 hash 路径与回调函数关联
    register(hash, callback) {
        this.routes[hash] = callback;
    }
    // 加载路由:根据当前 hash 找到对应的回调并执行
    load() {
        let hash = location.hash.slice(1); // 去掉开头的 #
        let handler;
        if (!hash) {
            // hash 为空时的处理
        } else {
            handler = this.routes[hash];
        }
        handler.call(this);
    }
}

// 使用
let router = new HashRouter();
let container = document.getElementById('container');
router.register('/page1', () => container.innerHTML = '页面一');
router.register('/page2', () => container.innerHTML = '页面二');
router.register('/page3', () => container.innerHTML = '页面三');

这个简易的 HashRouter 包含了前端路由的核心思想:

  • routes 对象存储路由映射表
  • register 方法注册路由
  • load 方法根据当前 hash 匹配并执行对应的回调
  • 通过 hashchange 事件驱动页面切换

SPA 极大地提升了用户体验------不再有页面白屏闪烁,内容的切换变得流畅自如。

五、React 集成前端路由

在 React 生态中,react-router-dom 是官方提供的路由解决方案。React 开发"全家桶"的典型组合是:

  • React ------ 组件开发、响应式 UI 界面
  • react-router-dom ------ 给应用添加前端路由
  • zustand / pinia ------ 状态管理

5.1 基本路由配置

javascript 复制代码
import { HashRouter as Router, Routes, Route } from 'react-router-dom';

<Router>
  <Routes>
    <Route path="/" element={<Home />} />
    <Route path="/about" element={<About />} />
  </Routes>
</Router>
  • HashRouter:底层基于 location.hashhashchange 事件,即我们前面学习的 hash 路由机制
  • Routes:路由配置的容器,内部包含多个 Route
  • Route:单条路由配置,path 匹配 URL,element 指定要渲染的组件

Routes 的特点是:有且只有一个 Route 会显示 ------即当前 location.hash 对应匹配的那一个。

在 SPA 中不能直接使用 <a> 标签跳转,因为那会触发完整的页面刷新。react-router-dom 提供了 Link 组件来替代:

javascript 复制代码
import { Link } from 'react-router-dom';

<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>

Link 组件专门为 SPA 路由跳转而设计,点击后不会刷新页面,只做前端路由切换。

5.3 路由懒加载

随着应用变大,所有页面组件一起打包下载会严重影响首页加载速度。路由懒加载 的思想是:非当前路由的页面不加载,只加载用户正在访问的那个页面

javascript 复制代码
import { lazy, Suspense } from 'react';

// 不再使用静态 import
// import Home from './pages/Home';
// import About from './pages/About';

// 改用动态 import 函数 + lazy
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'));

lazy() 接收一个返回 import() 的函数,实现按需加载。配合 Suspense 组件,在懒加载组件还未就绪时显示一个 fallback:

xml 复制代码
<Suspense fallback={<div>Loading...</div>}>
  <Routes>
    {/* 路由配置 */}
  </Routes>
</Suspense>

5.4 动态路由

动态路由允许 URL 中包含可变参数,比如用户 ID、产品 ID:

ini 复制代码
<Route path="/user/:id" element={<UserProfile />} />

在页面组件中,通过 useParams Hook 获取动态参数:

javascript 复制代码
import { useParams } from 'react-router-dom';

function UserProfile() {
    let { id } = useParams();  // 召之即来,获取 URL 中的动态参数
    return <h2>User Profile {id}</h2>;
}

useParams 体现了 React Hooks 的设计思想------召之即来,在任何组件中按需获取路由参数,无需层层传递。

5.5 嵌套路由(多级路由)

嵌套路由用于表示页面中的层级关系,比如产品列表页面内嵌产品详情和新增产品:

xml 复制代码
<Route path="/products" element={<Products />}>
  {/* 二级路由 */}
  <Route path=":productId" element={<ProductDetail />} />
  <Route path="new" element={<NewProduct />} />
</Route>

父级组件需要通过 Outlet 组件指定子路由的渲染出口:

javascript 复制代码
import { Outlet } from 'react-router-dom';

const Products = () => {
    return (
        <>
            <h1>产品列表</h1>
            <Outlet />  {/* 二级路由的出口,子组件在此渲染 */}
        </>
    );
};

5.6 404 Not Found 与路由重定向

使用通配符 * 匹配所有未定义的路径,作为 404 兜底页面:

ini 复制代码
<Route path="*" element={<NotFound />} />

*贪婪匹配 ,会匹配所有路径,所以必须放在 Routes 的最末尾,确保前面的路由优先匹配。

在 404 页面中,可以使用 useNavigate 实现编程式导航:

javascript 复制代码
import { useNavigate } from 'react-router-dom';

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

另外,Navigate 组件可以实现路由重定向:

ini 复制代码
<Route path="old-path" element={<Navigate replace to="/new-path" />} />

六、重要知识总结

知识点 核心要点
多页面 vs 单页面 传统多页面每次点击链接都会全量刷新页面("白一下");SPA 在一个页面内动态切换内容,体验更流畅
Hash 路由原理 # 后面的 hash 部分改变时 URL 会变但页面不刷新,通过监听 hashchange 事件来切换内容
URL 结构 protocol://host/path?queryString#hash,hash 是前端路由的关键
锚链接 hash 的原始用途------在长页面中做"电梯导航",跳转到页面内指定位置
HashRouter 实现 routes 对象存储映射 → register 注册回调 → hashchange 事件驱动 load 方法
this 指向问题 事件监听器中 this 默认指向触发事件的对象(如 window),需要用 bind 修正
react-router-dom React 官方路由库,核心组件:HashRouterRoutesRouteLinkNavigate
Link vs <a> <a> 标签会触发完整页面刷新,Link 组件专为 SPA 路由跳转设计,不刷新页面
路由懒加载 lazy(() => import(...)) + Suspense,非当前路由页面不加载,提升首屏性能
动态路由 path="/user/:id" 定义动态参数,useParams() Hook 在组件中获取参数值
嵌套路由 父路由包含子 <Route>,父组件中使用 <Outlet /> 作为子路由渲染出口
404 兜底 path="*" 贪婪匹配所有未定义路径,必须放在 Routes 末尾
编程式导航 useNavigate() Hook 返回 navigate 函数,用于代码中控制路由跳转
路由重定向 <Navigate replace to="/new-path" /> 实现路径重定向

从最初认识 URL 中的 #hashchange 事件,到手写 HashRouter,再到 React 中借助 react-router-dom 构建完整的路由体系------前端路由的演进始终围绕一个核心目标:在保证 URL 与资源一一对应的前提下,让页面切换更快、更流畅,给用户带来接近原生 App 的体验

相关推荐
刘发财10 分钟前
前端2秒生成500页矢量PDF,rust真的强到没朋友
前端·javascript·rust
郑州光合科技余经理43 分钟前
国际版外卖系统:税率字段怎么和订单主流程解耦
android·java·开发语言·前端·后端·php·ai编程
梦想平凡1 小时前
百游棋牌源代码开发搭建教程(十):隔离部署、备份恢复与双端验收
java·前端·javascript·数据库·源代码管理
yume_sibai2 小时前
06-Rust Web 开发实战(Axum 框架 + 数据库 + JWT 认证 + 中间件 + 部署)
前端·数据库·rust
计算机魔术师4 小时前
特朗普上台打给黄仁勋:AI末日论是骗局,我们绝不让它发生
前端
troy1284 小时前
Python 基础语法(八):Web 后端开发、数据分析与可视化、网络爬虫、人工智能 / 大模型应用
前端·python·数据分析
计算机魔术师4 小时前
CEO说要慢下来,黑客说别做梦了——同一篇论文,两种命运
前端
kyriewen5 小时前
我花3天抓了一个幽灵bug,凶手藏在第4层
前端·javascript·程序员
IT_陈寒5 小时前
Java里用Stream.parallel()翻车实录,这性能还不如单线程
前端·人工智能·后端
wangchunyu1145 小时前
JavaScript 零基础入门教程
前端