从前端路由的起源到 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 的体验

相关推荐
明月_清风1 小时前
🚀 AI Agent 完全入门指南:从 LLM 到生产落地,新手必懂的 34 个核心概念
前端·后端·ai编程
鸽鸽1 小时前
Vue 3 API 完全指南:从 Options 到 Composition 的进阶之路
前端·vue.js
名字还没想好☜1 小时前
React 用 Portal + Context 实现全局 Toast:一次调用、自动消失与队列管理
前端·javascript·react.js·react·next.js
HjhIron1 小时前
React Router 通关指南:从入门到鉴权,一个项目全搞定
前端
kyson_1 小时前
如何做一款自适应的数据大屏:`vw/vh`、`rem`、`scale` 应该怎么选?
前端
雪隐1 小时前
WPF + MVVM 实战系列02-告别 INPC 手写时代,做个体面的现代 WPF 人
前端·后端·c#
肉肉不吃 肉1 小时前
无渲染组件和组合式函数
前端·javascript·vue.js
未来智慧谷1 小时前
从 Atlas 关停复盘:AI 应用的三种载体形态怎么选(独立应用 / 浏览器扩展 / 桌面宿主)
前端·人工智能·ai·架构·浏览器
sugar__salt1 小时前
前端路由技术详解:从传统多页到 React SPA 的完整进化之路
前端·javascript·react.js·前端框架
Canace2 小时前
Harness Engineering 到底在做什么
前端·人工智能·ai编程