前端路由(一):二十行代码实现 Hash 路由

背景

打开一个网页,浏览器地址栏里的 URL 发生变化,页面内容随之更新------这件事在今天的 Web 应用里已经习以为常。但在单页应用(SPA)成为主流之前,URL 变化几乎总是意味着一次完整的页面刷新。

本文从浏览器 URL 的结构出发,拆解传统多页导航的问题,再逐步说明 Hash 路由如何在「不刷新页面」和「URL 与资源一一对应」之间找到平衡,最后给出一份可运行的 HashRouter 实现。

传统多页导航的工作方式

先看一个最简单的多页面站点。两个 HTML 文件,通过 <a> 链接互相跳转:

xml 复制代码
<!-- demo/index.html -->
<nav>
  <ul>
    <li><a href="index.html">首页</a></li>
    <li><a href="about.html">关于我们</a></li>
  </ul>
</nav>
<main><h1>首页</h1></main>


<!-- demo/about.html -->
<nav>
  <ul>
    <li><a href="index.html">首页</a></li>
    <li><a href="about.html">关于我们</a></li>
  </ul>
</nav>
<main><h1>关于我们</h1></main>

用户点击链接时,浏览器执行以下步骤:

  1. 根据 href 中的 URL 向服务器发起 HTTP 请求
  2. 服务器返回对应资源的 text/html 响应
  3. 浏览器接收响应,销毁当前页面,重新渲染整个文档
  4. 浏览器历史记录中插入一条新记录

这个流程本身是合理的------URL 和资源是一一对应的关系,每个 URL 指向一个完整的 HTML 文档。但在移动网络环境下,每次跳转都经历「请求 → 等待 → 白屏 → 重新渲染」,体验上会明显感觉到页面闪烁。

单页应用的诉求

移动时代带来了一个变化:App 内的页面切换是即时的,不存在浏览器级别的整页刷新。用户对 Web 的预期也随之提高------内容切换应该更快,公共部分(导航栏、侧边栏)不应该反复销毁重建。

单页应用(Single Page Application,SPA)的思路是:

  • 整个应用只加载一个 HTML 文档
  • 导航栏、页脚等公共区域保持不变
  • 点击链接时,只替换内容区域(例如一个 <div id="container">
  • 不再向服务器请求完整的 HTML 页面

这就引出了一个问题:URL 还要不要变?

如果 URL 不变,用户无法收藏特定页面,也无法使用浏览器的前进/后退。SPA 需要一种机制,既能改变 URL,又不会触发整页刷新。

URL 的 Hash 部分

一个完整的 URL 可以拆成以下结构:

bash 复制代码
http(s)://www.example.com/path/to/page?q=search#/section
└──┬──┘  └─────┬──────┘ └─────┬─────┘ └──┬──┘ └───┬───┘
 protocol     host          path      query   hash/fragment

Hash 是 URL 中 # 之后的部分。它的原始用途是锚链接------标记长页面中的某个位置,点击后浏览器直接滚动到对应区域,不触发页面刷新:

css 复制代码
<a href="#bottom">去到底部</a>
<div style="height: 200vh;"></div>
<a name="bottom"></a>

Hash 有两个关键特性:

  1. 修改 hash 不会导致浏览器向服务器发起请求# 之后的内容不会发送到服务端。
  2. 修改 hash 会更新浏览器地址栏,并且会在浏览器历史记录中插入一条新记录。

这两个特性恰好满足了 SPA 前端路由的需求------URL 变了,页面没刷新,历史记录也有了。

Hash 路由的设计

前端路由要解决的核心问题是:把不同的 URL(更准确地说,不同的 hash)映射到不同的内容渲染逻辑上。

拆开来看,需要三样东西:

  1. 路由表:一个存储「hash 路径 → 渲染函数」映射的数据结构
  2. 监听机制:当 hash 变化时,能够感知并响应
  3. 渲染入口:一个固定的 DOM 挂载点,用于替换内容

浏览器提供了 hashchange 事件来支持第二点。每当 URL 的 hash 部分发生变化(无论是用户点击锚链接、手动修改地址栏、还是 JavaScript 修改 location.hash),hashchange 事件都会触发:

javascript 复制代码
window.addEventListener('hashchange', function(event) {
  console.log('hash 改变了');
  console.log(event.newURL);  // 变化后的完整 URL
  console.log(event.oldURL);  // 变化前的完整 URL
});

基于这个事件,可以构建一个简单的 HashRouter 类。

实现

以下实现来自 demo2/index.html:

javascript 复制代码
class HashRouter {
  constructor() {
    // 路由表:存储 hash 路径到回调函数的映射
    this.routers = {};

    // 监听 hashchange 事件
    // 注意:事件回调中的 this 默认指向 window
    // 使用 bind 将 this 绑定到 HashRouter 实例
    window.addEventListener('hashchange', this.load.bind(this));
  }

  // 注册路由:将 hash 路径与回调函数关联
  register(hash, callback) {
    this.routers[hash] = callback;
  }

  // 加载路由:根据当前 hash 执行对应的回调
  load() {
    let hash = location.hash.slice(1);  // 去掉开头的 #
    let handler;

    if (!hash) {
      // hash 为空时不做处理(可以在这里设置默认路由)
    } else {
      handler = this.routers[hash];
    }

    handler.call(this);  // 执行回调,手动指定 this
  }
}

使用方式:

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

// 注册三条路由,每条路由对应不同的内容渲染
router.register('/page1', () => (container.innerHTML = '页面一'));
router.register('/page2', () => (container.innerHTML = '页面二'));
router.register('/page3', () => (container.innerHTML = '页面三'));

页面中的导航链接使用 hash 形式的 href:

xml 复制代码
<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>

几处值得注意的实现细节:

  • location.hash 的返回值包含开头的 #,注册和匹配时统一用 .slice(1) 去掉,保持路由键名干净
  • addEventListener 的回调中,this 默认指向触发事件的 DOM 元素(这里就是 window)。代码中使用 .bind(this) 返回一个新函数,将 this 固定为 HashRouter 实例,确保 load 方法内部能正确访问 this.routers
  • register 方法将回调存入 this.routers 对象,load 方法通过 this.routers[hash] 完成 O(1) 查找------路由表本质上是一个键值对

回看:从锚链接到前端路由

readme.md 中概括了这条线索:

  • 锚链接是 hash 的原始用途------在长页面中定位,用户体验类似「坐电梯直达」
  • hash 作为 URL 的一部分,改变它不会触发页面刷新,但会更新浏览器地址栏和历史记录
  • 前端路由抓住了这个特性:用 #/page1#/about 这样的 hash 值模拟多页面的 URL 结构,通过 hashchange 事件触发 DOM 或组件替换

这是一个典型的「复用已有基础设施解决新问题」的模式。浏览器没有专门为 SPA 设计路由 API,但 hash + hashchange 的组合刚好能拼出前端路由需要的全部行为。

Hash 路由的局限与后续方向

Hash 路由解决了 SPA 最紧迫的问题,但也有明显的局限:

  • URL 中始终带着 #,不够干净
  • hash 部分不会发送到服务端,SEO 不友好
  • 与服务端渲染(SSR)配合困难

HTML5 引入了 History API(pushState / replaceState + popstate 事件),可以在不刷新页面的情况下修改完整的 URL 路径,不再需要 #。这是 Hash 路由之后的下一个演进方向,原理与本文讨论的 Hash 路由一脉相承------都是在「不刷新页面」和「URL 可变」之间建立映射。

小结

前端路由要做的事情可以归结为一句话:URL 改变时,用一段 JavaScript 逻辑替代浏览器的默认导航行为,在不刷新页面的前提下完成内容替换。

Hash 路由是这条思路上最直接的实现:用 # 之后的片段做路径标识,用 hashchange 事件做触发器,用一个键值对做路由表。实现代码不过二十行,但它在传统多页和现代 SPA 之间划出了一条清晰的分界线。

相关推荐
勾勾圈圈蛋蛋1 小时前
黑马Vue_day11:Vue3的watch,ref和reactive,vue2->vue3,defineOptions、Model
前端
AlloyTeamZy1 小时前
我发现,一个人做小游戏最难的,根本不是写代码
前端·人工智能·程序员
触底反弹1 小时前
🚀 20 行代码手写前端路由 + React Router 核心 API 一篇搞定!
前端·javascript·react.js
灏仟亿前端技术团队2 小时前
企业级 Agent 中台建设方案
前端
程序员黑豆2 小时前
鸿蒙应用开发:一次开发多端适配与自适应布局实战
前端·harmonyos
布兰妮甜2 小时前
Vue 状态管理选型:Pinia 完整实战,对比 Vuex,模块化持久化
前端·javascript·vue.js·pinia·vuex
神王宝宝 王者小学2 小时前
面向领域驱动架构的查询实现方式
前端·python·架构
小林ixn2 小时前
React Router 从入门到实战:一篇搞定路由配置、懒加载与嵌套路由
前端·react.js·前端框架
labixiong2 小时前
async/await 到底是不是 Generator 的语法糖?手写执行器,Babel 编译产物里藏着答案
前端·javascript·babel