从"白一下"到"丝滑切换"——前端路由的进化史

你有没有注意过:打开淘宝、刷B站、看知乎时,页面切换总是行云流水、一气呵成;但偶尔遇到一些老式网站,每点一个链接页面就要"白一下"重新加载。这背后的秘密,就是前端路由------从传统的多页面架构到现代单页应用的进化。本文通过两个亲手可运行的 demo 项目,带你彻底理解这段技术史。


一、回到底层:浏览器是怎么"看到"一个网页的?

在讲路由之前,我们先回到最底层的问题:当你在浏览器地址栏输入一个网址,按下回车,发生了什么?

这个过程的本质是一段接力赛

markdown 复制代码
1. URL 解析       ← 浏览器拆解你输入的地址(协议、主机、路径、参数......)
2. HTTP 请求      ← 浏览器向服务器发送请求:"请把 /index.html 给我"
3. 服务器响应     ← 服务器运行后端程序,生成 HTML 文本,返回给浏览器
4. 浏览器渲染     ← 浏览器拿到 HTML,构建 DOM 树,绘制像素到屏幕上
5. 历史记录插入   ← 浏览器在浏览历史中插入一条新记录(用于"前进/后退")

这一切的核心,是URL 和资源的对应关系------一个地址对应一个 HTML 页面。这种模式从 1990 年代万维网诞生起就已经确立,被称为 **"万物互联"**的基石:只要有一个链接,就能跳转到互联网上的任何一个角落。


二、demo 文件夹:传统多页面的"落日余晖"

来看 demo/ 文件夹里的两个 HTML 文件。它们是一个极简的传统多页面网站:

index.html(首页)

html 复制代码
<nav>
  <ul>
    <li><a href="http://127.0.0.1:5500/fe/history/demo/index.html">首页</a></li>
    <li><a href="http://127.0.0.1:5500/fe/history/demo/about.html">关于我们</a></li>
  </ul>
</nav>
<main><h1>首页</h1></main>

about.html(关于页)结构几乎一致,只是标题换成了"关于我们"。两个页面的导航栏一模一样------在传统模式中,每个页面都必须各自写一遍导航栏、页脚等公共部分。

当你点击"关于我们"的链接时,浏览器执行了全套操作:发起新的 HTTP 请求 → 服务器重新返回一个完整的 HTML → 整个页面重新渲染 。这被称为 MPA(Multi-Page Application,多页面应用)

传统模式的痛点

在 PC 互联网时代,多页面模式运转了几十年,没什么大问题。但到了移动互联网时代,这个"整页刷新"的设计成了性能杀手:

  • 白屏闪烁:网速稍慢,页面就会白一下再出现新内容。对手机用户而言,这种体验堪称灾难。
  • 重复传输:导航栏、底部栏、Logo、样式文件......这些公共资源每次都要重新传输和渲染,浪费了大量带宽。
  • 状态丢失:页面跳转后,之前的滚动位置、表单填写内容、播放进度等全部丢失。
  • 服务器压力:每次请求都要占用服务器计算资源去拼接完整的 HTML 页面。

这就引出了核心矛盾:在今天的设备上,有没有办法只替换变化的部分,而不重新渲染整个页面?


三、demo2/demo.html:Hash 的"前世"------锚点链接

在讨论前端路由之前,先看 demo2/demo.html,它展示了 hash(哈希)最原始的用途------锚点链接

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

这里的 #bottom#top 就是 hash------URL 中 # 号及之后的部分。它的原始作用是页内导航 :点击"去到底部",浏览器自动滚动到 <a name="bottom"> 所在的位置,就像坐电梯直达某一层。

关键特性 :当 hash 改变时,浏览器不会重新请求服务器,也不会刷新页面。页面还是那个页面,只是滚动位置变了。

再看看脚本部分:

js 复制代码
window.addEventListener('hashchange', function(event) {
  console.log('hash 改变了');
  console.log(event.newURL);  // 新的 URL(hash 部分变了)
  console.log(event.oldURL);  // 旧的 URL
})

浏览器提供了一个 hashchange 事件------只要 # 后面的部分发生了变化,这个事件就会被触发。正是这个机制,让开发者敏锐地意识到:如果把 hash 当作"页面标识"而不是"位置锚点",不就能在不刷新页面的情况下切换内容了吗?

一个天才般的技术思路就此诞生。


四、demo2/index.html:Hash 路由的"开天辟地"

demo2/index.html,它用不到 60 行的 JavaScript,手动实现了一个完整的前端路由系统。这代表了从多页面向单页面应用的关键一跃。

页面结构:极简中的革命性

html 复制代码
<nav>
  <ul>
    <li><a href="#/page1">页面一</a></li>
    <li><a href="#/page2">页面二</a></li>
    <li><a href="#/page3">页面三</a></li>
  </ul>
</nav>
<div id="container"></div>

注意这里链接的变化------不再是传统的 href="index.html",而是 href="#/page1"。点击这些链接时:

  • URL 变了(从 /#/page1 变为 /#/page2
  • 页面不会刷新
  • 导航栏、页脚等公共部分保持不动
  • 只有 <div id="container"> 里的内容会被替换

这就是 SPA(Single Page Application,单页应用) 的核心思想:整个网站只有"一个页面",通过动态切换 DOM 内容来模拟页面跳转。

HashRouter 类:一个手工打造的路由引擎

js 复制代码
class HashRouter {
  constructor() {
    this.routers = {};  // 路由表:hash → 回调函数
    window.addEventListener('hashchange', this.load.bind(this));
  }

  register(hash, callback) {
    this.routers[hash] = callback;  // 注册一个路由规则
  }

  load() {
    let hash = location.hash.slice(1);  // 获取 # 后面的部分(去掉 #)
    let handler = this.routers[hash];   // 从路由表中找到对应的处理函数
    handler.call(this);                  // 执行处理函数
  }
}

这个四十多行的类,浓缩了整个前端路由最原始也是最核心的逻辑。让我们逐层解剖:

第一层:路由表 this.routers

this.routers 是一个普通的 JavaScript 对象,本质是 key → value 映射

json 复制代码
{ "/page1": callback1, "/page2": callback2, "/page3": callback3 }

这和传统后端的路由表在概念上是完全一致的------只不过传统后端路由返回的是整个 HTML 页面 ,而前端路由返回的是一个回调函数,执行后更新某个 DOM 区域。

第二层:hashchange 事件监听

js 复制代码
window.addEventListener('hashchange', this.load.bind(this));

这里有三层技术细节值得深挖:

  1. hashchange 事件:浏览器在 hash 变化时自动触发。这让我们不用自己写轮询去检查 URL 有没有变------把主动权交给了浏览器。

  2. this.load.bind(this) :这是一个经典的 JavaScript this 绑定问题。当 hashchange 事件触发时,事件处理函数中的 this 默认指向触发事件的对象------window。但我们希望 this 指向 HashRouter 实例,才能访问 this.routersbind(this) 创建了一个新函数,其中 this 被永久锁定为当前实例,无论将来谁调用这个函数。

  3. 与之相关的 callapplybind 三个方法,是 JavaScript 函数式编程中"手动指定 this"的三种武器。callapply 是临时借用的------立即执行并改变 this;bind 是永久绑定的------返回一个绑定了 this 的新函数但不立即执行。

第三层:load() 方法------路由的分发核心

js 复制代码
load() {
  let hash = location.hash.slice(1);  // 例如 "/page1"
  let handler = this.routers[hash];   // 从路由表查找
  handler.call(this);                  // 执行,确保回调函数中 this 正确
}

location.hash 是浏览器提供的 API,返回当前 URL 的 hash 部分(包括 #)。slice(1) 去掉第一个字符 #,得到 /page1

然后从 this.routers 对象中查找对应的处理函数------这本质上是一个 O(1) 的哈希查找,效率极高。

最后 handler.call(this) 确保处理函数中如果使用了 this,它指向的是 HashRouter 实例而非全局对象。

第四层:注册与启动

js 复制代码
let router = new HashRouter();
let container = document.getElementById('container');

router.register('/page1', () => container.innerHTML = '页面一');
router.register('/page2', () => container.innerHTML = '页面二');
router.register('/page3', () => container.innerHTML = '页面三');

注册阶段将三个路径和处理函数一一绑定。这里处理函数只是简单地修改了 #container 的文本内容,但在真实项目中,处理函数可以做任何复杂操作------发起 AJAX 请求获取数据、渲染一整个组件树、更新页面标题......

为什么这被称为"开天辟地"?

这个几十行的 demo 解决了传统多页面的所有核心痛点:

传统 MPA SPA + Hash 路由
每次切换都整页刷新,白屏闪烁 只替换变化区域,无白屏
公共部分(导航栏等)每次重新渲染 公共部分保持不变
页面状态丢失 状态始终保持在内存中
服务器每次返回完整 HTML 服务器只返回数据(JSON),渲染由前端完成
浏览器下载大量重复资源 只传输变化的数据

URL 和资源的对应关系被完整保留------每个 hash 值对应一个"虚拟页面"的内容------但实现方式从"重新请求整页"变成了"局部替换 DOM"。


五、从前端路由到现代框架:后面的故事

demo 中的 HashRouter 是前端路由的"石器时代"实现。但它蕴含的核心思想------路由表映射、事件驱动、DOM 动态替换------至今仍是 React Router、Vue Router 等现代路由库的基础。

后续的发展脉络大致如下:

  1. History API(HTML5)pushStatereplaceState 让开发者可以在不刷新页面的情况下改变 URL 的路径部分(而不只是 hash),实现了 example.com/page1 这样的"干净 URL"。

  2. React 组件化 :路由不再只是"替换 innerHTML",而是挂载和卸载组件。路由匹配到一个路径,就渲染对应的 React 组件,未匹配的页面组件完全不存在于 DOM 树中。

  3. 路由懒加载:非当前页面的代码根本不需要下载,只有用户真正访问时才动态加载------首屏速度大幅提升。

  4. 嵌套路由与动态路由/products/:id 可以匹配任意产品 ID,路由参数被注入组件,一条规则覆盖无数页面。


六、总结:理解底层,方能驾驭框架

demo/ 的传统 <a href="xxx.html"> 链接,到 demo2/demo.html 的锚点 hash,再到 demo2/index.html 的手工 HashRouter------这三个 demo 串起了一条完整的技术进化链。

核心 takeaways

  • 多页面(MPA) 是 web 的原生模式,URL 和 HTML 文件一一对应,简单但每次切换都要整页刷新。
  • Hash 的原始用途 是页内锚点导航,改变 hash 不会触发页面刷新,这一特性被开发者创造性利用。
  • Hash 路由 通过 hashchange 事件监听 + 路由表映射 + DOM 动态替换,在不刷新页面的情况下实现了"页面切换"的效果------这就是 SPA 的基石。
  • bind/call/apply 是 JavaScript 中手动指定 this 三种方式,理解它们对阅读任何前端框架源码都至关重要。
  • 今天的 React Router、Vue Router 在概念上和这个小 demo 并无本质区别------只是用更健壮的方式,把"路由映射 + DOM 替换"这个模式工程化了。

当你下次在 React 项目里写下 <Route path="/about" component={About} /> 时,不妨想想那个不到 60 行的 HashRouter------它背后的核心逻辑,和你写的这行代码是相通的。理解底层,方能驾驭框架。


相关推荐
玉宇夕落1 小时前
React 多种路由学习:HashRouter 源码级剖析
前端
他们叫我秃子1 小时前
前端开发转 Go 全栈(三):从函数到 error,Go 连“失败”都要明确返回
前端·后端·go
Canace1 小时前
GPT-5.6 到底怎么选?一文搞懂 Sol、Terra、Luna 和 Ultra
前端·人工智能·chatgpt
用户059540174461 小时前
LangChain Memory 踩坑实录:10种异常场景差点搞崩生产,我们写了一套自动化测试保命
前端·css
名字还没想好☜1 小时前
React useImperativeHandle 实战:让父组件安全地调用子组件的方法
前端·javascript·react.js·react·forwardref
CodeSheep1 小时前
FFmpeg 9.0正式发布:代号“Lei”,以纪念中国开发者雷霄骅
前端·后端·程序员
IT_陈寒1 小时前
小心!Java里的这个空指针问题绝对坑过你
前端·人工智能·后端
Canace1 小时前
AI 都能操作浏览器了,却读不了微信公众号文章
前端·人工智能·产品
铁皮饭盒1 小时前
阿里ASR语音转文字,用JS模拟微信
前端·javascript·后端