云计算百科
云计算领域专业知识百科平台

从页面跳转到手写 SPA:深入理解 Hash 路由与浏览器历史

从页面跳转到手写 SPA:深入理解 Hash 路由与浏览器历史

  • 前言
  • 1. 从多页面应用到单页应用
    • 1.1 一次传统页面跳转发生了什么
    • 1.2 SPA 为什么需要前端路由
  • 2. 理解 URL、Hash 与浏览历史
    • 2.1 Hash 是 URL 中的哪一部分
    • 2.2 锚点实验:先看见 Hash 如何变化
  • 3. 手写 HashRouter:逐块拆解核心代码
    • 3.1 页面骨架:路由入口与视图挂载点
    • 3.2 路由表:为什么用对象保存回调
    • 3.3 监听 Hash:bind 解决的不是语法,而是 this
    • 3.4 register:注册只建映射,不负责渲染
    • 3.5 load:读取、规范化、匹配、执行
    • 3.6 实例化与页面回调:闭包把视图连接起来
  • 4. 把碎片串起来:一次路由切换的完整执行链
    • 4.1 从点击页面二到视图更新
    • 4.2 首次加载为什么是特殊场景
  • 5. Hash 路由的能力边界与方案选择
    • 5.1 Hash 路由与 History 路由对比
    • 5.2 从手写路由器到框架路由器
  • 总结

前言

打开一个传统网站,点击“关于我们”,浏览器通常会请求一份新的 HTML,再重新解析文档、构建 DOM 并绘制页面。这个过程直观、可靠,却也意味着页面公共区域可能被反复加载,网络稍慢时还容易出现短暂白屏。移动互联网兴起之后,用户逐渐习惯原生 App 那种“只更新内容区域”的体验,单页应用(Single Page Application,SPA)也因此成为前端开发的重要形态。

SPA 只有一份主要 HTML,却仍然需要让不同 URL 对应不同内容。例如,访问 #/page1 显示页面一,访问 #/page2 显示页面二;点击浏览器的前进、后退按钮时,界面还应跟随历史记录恢复。解决“URL 变化与视图更新如何对应”这个问题的机制,就是前端路由。

本文从传统多页面跳转讲起,先用锚点实验理解 URL 的 Hash,再逐块拆解一个原生 JavaScript 实现的 HashRouter。重点不只是记住 hashchange、location.hash 和 bind 的写法,而是看清它们如何协作成一条完整的路由执行链,并进一步补齐首屏渲染、404 兜底、事件解绑和安全渲染等工程细节。

1. 从多页面应用到单页应用

1.1 一次传统页面跳转发生了什么

传统多页面应用(Multi-Page Application,MPA)通常让一个 URL 对应一份服务端资源。下面是最典型的导航结构:

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

用户点击“关于我们”后,浏览器会把当前地址切换为 /demo/about.html,向服务器请求新页面,然后解析 HTML、加载依赖资源、构建 DOM/CSSOM 并完成渲染。与此同时,浏览器通常还会向会话历史中加入一条记录,因此前进、后退按钮能够在两个页面之间导航。

示例中的首页与关于页分别拥有完整的 html、head 和 body,正是传统多页结构。关于页还给链接添加了 target="_blank",它会在新的浏览上下文中打开页面,而不是替换当前页。实际项目若明确需要新标签页,通常写成:

<a href="/demo/about.html" target="_blank" rel="noopener">
关于我们
</a>

这里的 rel="noopener" 用于切断新页面通过 window.opener 操作来源页面的能力,是更稳妥的安全习惯。若并不需要保留当前页面,则不应滥用 _blank。

传统页面导航的特点可以概括如下:

观察维度传统多页面导航
URL 与资源 一个 URL 通常对应一份独立 HTML 或服务端响应
页面更新 整个文档重新加载和渲染
公共布局 页头、导航等内容可能被重复返回与解析
浏览历史 浏览器原生维护,前进和后退行为直观
首次加载 通常只获取当前页面所需内容
页面切换体验 受网络和服务端响应影响,可能出现刷新感或白屏

需要特别区分两个常见的浏览器对象:navigator 主要描述浏览器与运行环境,例如语言、在线状态和 User Agent;当前 URL 的读取与跳转主要由 window.location 负责。前端路由真正频繁使用的是 location,而不是 navigator。

1.2 SPA 为什么需要前端路由

SPA 的核心思路是:只加载一次应用外壳,后续切换页面时保留外壳,仅替换指定挂载点中的内容。例如,导航栏始终保留,只把页面内容渲染进 #container:

<header>固定导航区域</header>

<!– 路由匹配成功后,只更新这个挂载点 –>
<div id="container"></div>

这能减少整页刷新,保留应用的内存状态,也让切换动画和交互体验更接近桌面或移动 App。不过,单纯调用 DOM API 替换内容还不够:如果页面一、页面二始终共用同一个 URL,用户就无法收藏具体页面,刷新后也不知道该恢复哪个视图,浏览器的前进与后退更无从谈起。

前端路由的本质,是在不重新加载整份文档的前提下,建立 URL 状态与页面视图之间的映射关系,并在 URL 发生变化时驱动视图更新。

于是,一个最小路由系统至少要完成三件事:

  • 改变 URL:点击导航后,地址栏必须出现可区分的路径状态。
  • 监听变化:URL 状态改变时,JavaScript 能收到通知。
  • 匹配并渲染:找到该路径对应的处理函数,把正确内容放进挂载点。

Hash 路由恰好提供了这三项能力,而且无需服务端为每个前端路径配置回退规则,因此很适合用来理解前端路由的基本模型。

2. 理解 URL、Hash 与浏览历史

2.1 Hash 是 URL 中的哪一部分

观察下面这个地址:

https://www.example.com/u/123?a=1&b=2#/page1

它可以拆成以下几部分:

URL 部分示例值作用
协议 protocol https: 规定通信方案
主机 host www.example.com 标识服务器及端口
路径 pathname /u/123 标识服务端资源路径
查询参数 search ?a=1&b=2 携带键值形式的请求参数
片段 hash #/page1 标识文档内部位置或客户端状态

Hash 从 # 开始,正式名称是 URL Fragment(片段标识符)。它最初用于定位长页面中的某个元素,例如访问 article.html#chapter-3 时直接滚动到第三章。它有两个非常适合前端路由的特征:

  • 修改片段会改变地址栏和浏览历史,但通常不会触发当前 HTML 文档的重新请求。
  • JavaScript 可以通过 location.hash 读取片段,并通过 hashchange 事件感知它的变化。

还要注意,Hash 通常不会随 HTTP 请求发送给服务器。也就是说,请求 https://example.com/index.html#/page1 时,服务器主要看到的是 /index.html;#/page1 留在浏览器端处理。这正是 Hash 路由不依赖服务端识别前端路径的原因。

// 地址为 https://example.com/index.html#/page1 时:
console.log(location.pathname); // "/index.html"
console.log(location.hash); // "#/page1"

2.2 锚点实验:先看见 Hash 如何变化

在实现路由器之前,可以先用一个长页面观察 Hash 的原始用途。

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hash 锚点实验</title>
</head>
<body>
<!– URL 变成 #top 时,浏览器会定位到这个元素 –>
<div id="top"></div>
<a href="#bottom">回到底部</a>

<div style="height: 200vh; background: yellow;"></div>

<a href="#top">回到顶部</a>
<div style="height: 300vh; background: red;"></div>

<div id="bottom"></div>

<script>
window.addEventListener('hashchange', function (event) {
console.log('Hash 改变了');
console.log('新地址:', event.newURL);
console.log('旧地址:', event.oldURL);
});
</script>
</body>
</html>

点击“回到底部”时,浏览器按以下顺序工作:

  • href="#bottom" 把当前 URL 的 Hash 改为 #bottom。
  • 浏览器在文档中寻找 id="bottom" 的元素并滚动定位。
  • 因为 Hash 确实发生了变化,window 派发 hashchange 事件。
  • 事件对象 event 提供完整的 oldURL 与 newURL,监听器可以比较切换前后的地址。
  • 旧式 HTML 也允许通过 <a name="top"></a> 定义锚点,它仍能解释早期网页的工作方式,但新代码更推荐直接给目标元素设置 id。另外,如果当前已经是 #bottom,再次点击同一个链接时 Hash 没有变化,通常也就不会产生新的 hashchange。

    锚点导航与 Hash 路由使用的是同一种 URL 变化机制,区别只在于变化发生之后做什么:锚点导航让浏览器滚动到元素;Hash 路由则把 #/page1 当成业务路径,查找处理函数并替换 DOM。

    3. 手写 HashRouter:逐块拆解核心代码

    3.1 页面骨架:路由入口与视图挂载点

    先看页面主体:

    <header>
    <nav>
    <ul>
    <!– 点击链接只改变 Hash,不重新请求整份 HTML –>
    <li><a href="#/page1">页面一</a></li>
    <li><a href="#/page2">页面二</a></li>
    <li><a href="#/page3">页面三</a></li>
    </ul>
    </nav>
    </header>

    <!– 所有“页面”最终都渲染到同一个挂载点 –>
    <div id="container"></div>

    href="#/page1" 中真正的路由路径是 /page1。在 # 后保留开头的 / 不是浏览器的强制要求,而是一种约定:它让前端路径看起来更像普通 URL 路径,也方便路由表统一管理。

    #container 是整个示例的视图挂载点。传统多页应用切换的是整份 HTML,当前 SPA 切换的只是这个元素的子节点。导航、文档结构和 JavaScript 运行环境都继续保留。

    脚本放在 body 末尾也有实际意义:浏览器执行 document.getElementById('container') 时,挂载点已经解析完成,所以不必额外等待 DOMContentLoaded。如果脚本被移动到 head 且没有 defer,查询结果就可能是 null。

    3.2 路由表:为什么用对象保存回调

    核心类从构造函数开始:

    class HashRouter {
    constructor() {
    // 每个路由器实例都有一张独立的路由表
    // 键是路径,例如 /page1;值是路径对应的渲染函数
    this.routers = {};
    }
    }

    class 是创建同类对象的语法结构;调用 new HashRouter() 时,JavaScript 会创建实例,把实例绑定为构造函数中的 this,再执行 constructor。因此,this.routers 属于当前实例,而不是所有实例共享的全局变量。

    路由表的结构可以直观地理解为:

    {
    '/page1': function () { /* 渲染页面一 */ },
    '/page2': function () { /* 渲染页面二 */ },
    '/page3': function () { /* 渲染页面三 */ }
    }

    这里保存的是函数引用,注册时并不会执行。只有当前 Hash 与键匹配时,路由器才取出并调用相应函数。于是,路径识别和具体页面渲染实现被分开:HashRouter 只负责“找谁”,每个回调负责“画什么”。

    用普通对象做路由表足以解释原理,但工程代码更适合使用 Map 或 Object.create(null)。普通对象继承自 Object.prototype,某些特殊键名可能与原型属性冲突;Map 则天然表达“路径到处理函数”的映射,并提供明确的 set、get 和 has API。

    3.3 监听 Hash:bind 解决的不是语法,而是 this

    构造函数中最关键的一段,是注册 hashchange 监听器:

    class HashRouter {
    constructor() {
    this.routers = {};

    window.addEventListener(
    'hashchange',
    // bind 不会立即执行 load,而是返回 this 已固定的新函数
    this.load.bind(this)
    );
    }
    }

    为什么不能直接写 window.addEventListener('hashchange', this.load)?因为 this.load 只是取出了方法的函数值,等 Hash 改变后,它会作为事件监听器被 window 调用。对于这种普通监听函数,函数内部的 this 通常指向注册事件的当前目标 window,不再是 HashRouter 实例。接下来访问 this.routers,得到的就不是实例中的路由表。

    bind 正是为了解决这个调用上下文丢失问题:

    const boundLoad = this.load.bind(this);

    它会创建一个新函数,并把新函数以后执行时的 this 永久固定为当前路由器实例。需要纠正一个常见误解:经过 this.load.bind(this) 后,load() 内的 this 指向 HashRouter 实例,而不是 window。可以从 load() 中输出实例进行验证。

    bind、call 和 apply 都能控制 this,但使用时机不同:

    方法是否立即调用原函数参数形式这里是否适用
    fn.bind(thisArg) 否,返回新函数 后续参数可预置 适合,事件系统需要一个待调用的函数
    fn.call(thisArg, a, b) 参数逐个传入 不适合直接注册,会立刻执行
    fn.apply(thisArg, [a, b]) 参数以数组传入 不适合直接注册,会立刻执行

    也不能写 this.load(),因为这会在构造阶段马上调用方法,并把它的返回值 undefined 交给 addEventListener。事件系统需要的是函数,而不是函数调用结果。

    这个写法还有一个工程细节:每次执行 bind 都会生成新的函数对象。如果以后需要 removeEventListener,就必须保留同一个引用。因此增强版会把绑定结果保存到实例属性中。

    3.4 register:注册只建映射,不负责渲染

    register 方法非常短,却体现了路由器最重要的抽象:

    register(hash, callback) {
    // 例如:this.routers['/page1'] = renderPage1
    this.routers[hash] = callback;
    }

    调用 router.register('/page1', callback) 后,路由表中就多了一项 /page1 → callback。此时页面没有变化,因为注册阶段只负责构建配置。把“注册”和“执行”分开后,新增页面无需修改 load() 的判断逻辑,只需继续添加映射即可。

    如果采用大量 if…else,代码会随页面数量线性膨胀:

    // 不利于扩展的写法
    if (hash === '/page1') {
    renderPage1();
    } else if (hash === '/page2') {
    renderPage2();
    } else if (hash === '/page3') {
    renderPage3();
    }

    路由表把条件分支转化为一次属性查找,这也是各种路由库都会维护“路由配置表”的根本原因。真实框架还会在此基础上加入动态参数、嵌套路由、懒加载、导航守卫等能力。

    3.5 load:读取、规范化、匹配、执行

    load() 是路由匹配的核心,可以按四个动作理解:

    load() {
    // 1. location.hash 会得到 "#/page1"
    // slice(1) 去掉开头的 #,得到路由表使用的 "/page1"
    const hash = location.hash.slice(1);

    let handler;

    // 2. 空 Hash 被规范为根路径 /
    if (!hash) {
    handler = this.routers['/'];
    } else {
    // 3. 用当前路径查询对应的处理函数
    handler = this.routers[hash];
    }

    // 4. 只有匹配成功才执行,避免调用 undefined 报错
    if (handler) {
    handler();
    }
    }

    假设当前地址是 http://127.0.0.1:5500/demo2/index.html#/page2,每一步的数据变化如下:

    执行阶段表达式或动作结果
    读取地址 location.hash "#/page2"
    去掉井号 .slice(1) "/page2"
    查询路由表 this.routers['/page2'] 页面二的回调函数
    执行处理器 handler() #container 显示“页面二”

    slice(1) 表示从索引 1 一直截取到字符串结尾。因为索引 0 正好是 #,所以它比手工替换更直接。如果地址没有 Hash,location.hash 是空字符串,slice(1) 仍然得到空字符串,随后进入根路径分支。

    if (handler) 是最小的防御性判断:访问没有注册的 #/unknown 时,属性查找得到 undefined,路由器不会盲目调用它。不过,这也意味着旧视图会继续留在屏幕上,用户不知道导航失败。后文会用通配处理器显示 404。

    还有两个细节必须看清:

    • 源码虽然尝试查找根路径 /,但并没有注册 / 的回调,所以无 Hash 首次打开时依然没有内容。
    • hashchange 只在 Hash 发生变化后触发。直接打开已经带有 #/page2 的地址时,仅仅注册监听器不会自动执行 load(),首屏也可能为空。

    这两个问题都需要通过注册根路由并主动执行一次首屏渲染来解决。

    3.6 实例化与页面回调:闭包把视图连接起来

    最后一块代码负责创建路由器、找到挂载点并注册三个页面:

    const router = new HashRouter();
    const container = document.getElementById('container');

    router.register('/page1', function () {
    // 当前内容是固定字符串,因此示例中不存在外部输入注入
    container.innerHTML = '<h1>页面一</h1>';
    });

    router.register('/page2', function () {
    container.innerHTML = '<h1>页面二</h1>';
    });

    router.register('/page3', function () {
    container.innerHTML = '<h1>页面三</h1>';
    });

    三个回调都使用了外层作用域中的 container。即使注册代码执行完毕,回调以后被 handler() 调用时仍能访问这个变量,这就是闭包:函数会记住其创建位置的词法作用域。于是,路由处理器无需把挂载点设成全局变量,也能在未来的事件回调中更新它。

    innerHTML 会把字符串解析成 DOM,并替换容器原有的全部子节点。它适合展示最小示例,但要了解它的边界:

    • 替换节点会丢失旧子树中的 DOM 状态与事件监听器。
    • 把用户输入或接口返回值直接拼进 innerHTML,可能产生跨站脚本攻击(XSS)。
    • 页面复杂后,手工拼接字符串不利于组件复用和精确更新。

    这里写入的是开发者定义的固定字符串,所以风险可控。处理不可信文本时,应优先使用 textContent;复杂应用则通常交给 Vue、React 等框架的渲染系统管理。

    完整代码展示:

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>SPA</title>
    </head>
    <body>
    <header>
    <nav>
    <ul>
    <li><a href="#/page1">页面一</a ></li>
    <li><a href="#/page2">页面二</a ></li>
    <li><a href="#/page3">页面三</a ></li>
    </ul>
    </nav>
    </header>
    <div id="container">

    </div>
    <script>
    class HashRouter{
    constructor(){
    // this 实例
    this.routers = {}; // 前后端分离的,前端也要独立的路由
    // 传统的多页开发,前端没有独立的路由 前端路由集合
    window.addEventListener('hashchange',
    // this 指向事件发生的对象 window
    // apply call 手动指定this 区别是参数
    // bind 返回一个新函数
    this.load.bind(this)
    )
    }
    register(hash,callback){
    this.routers[hash] = callback;
    }
    load() {
    // 对象的属性或方法
    console.log(this);// this -> window
    let hash = location.hash.slice(1);// 切掉#
    // console.log(hash);
    let handler;
    if(!hash){
    handler = this.routers['/'];
    }else{
    handler = this.routers[hash];
    }
    if(handler){
    handler();
    }
    }
    }

    // 挂载点 只需要动态修改它
    // spa 极大的提升了用户体验
    // 前端路由
    let router=new HashRouter();
    let container = document.getElementById('container');
    router.register('/page1',function(){
    container.innerHTML = '<h1>页面一</h1>';
    })
    router.register('/page2',function(){
    container.innerHTML = '<h1>页面二</h1>';
    })
    router.register('/page3',function(){
    container.innerHTML = '<h1>页面三</h1>';
    })
    </script>
    </body>
    </html>

    4. 把碎片串起来:一次路由切换的完整执行链

    4.1 从点击页面二到视图更新

    单独看每个 API 并不复杂,真正重要的是理解它们的协作顺序。假设页面已完成初始化,用户点击“页面二”,完整链路如下:

  • 浏览器解析 <a href="#/page2">,把地址栏的 Hash 更新为 #/page2。
  • 文档主体没有重新请求,当前 JavaScript 运行环境也没有被销毁。
  • 浏览器向 window 派发 hashchange 事件,并调用构造函数中注册的绑定函数。
  • bind 保证 load() 内部的 this 仍是当前 HashRouter 实例。
  • load() 读取 location.hash,通过 slice(1) 得到 /page2。
  • 路由器从 this.routers 中取出 /page2 对应的回调。
  • 回调通过闭包访问 container,把其内容替换成 <h1>页面二</h1>。
  • 用户看到页面局部更新;使用浏览器后退时,历史记录恢复旧 Hash,再次触发同一套匹配与渲染流程。
  • 这条链路可以浓缩成一张阶段表:

    层次负责者核心任务
    交互入口 <a href="#/page2"> 声明目标 URL 状态
    状态变化 浏览器地址栏与历史记录 更新 Hash,保留同一文档
    变化通知 hashchange 通知 JavaScript 重新匹配
    路径解析 location.hash.slice(1) 把 URL 片段转成路由路径
    路由匹配 this.routers[hash] 找到路径对应的处理器
    视图渲染 路由回调与 DOM API 更新 #container 内容

    这也解释了为什么浏览器后退不需要单独写一个“后退按钮处理器”:后退操作改变历史记录中的 Hash,hashchange 监听器仍会接住变化,再根据恢复后的 URL 渲染视图。URL 是状态来源,视图是该状态的结果,这是前端路由比“点击后直接改 DOM”更可靠的地方。

    4.2 首次加载为什么是特殊场景

    事件驱动代码很容易忽略首屏。用户第一次打开 index.html#/page3 时,页面开始执行脚本之前,URL 中已经是 #/page3。监听器只能捕获未来发生的变化,不能补发过去的事件,因此必须在所有路由注册完成后主动调用一次 load():

    // 先注册,后加载;顺序不能反
    router.register('/page3', renderPage3);

    // 根据页面启动时已经存在的 Hash 完成首屏匹配
    router.load();

    如果先 load() 再注册,路由表还是空的,同样无法匹配。因此一个完整的路由器通常会提供 start():先安装监听器,再主动渲染当前地址,把“监听未来变化”和“处理当前状态”统一起来。

    5. Hash 路由的能力边界与方案选择

    5.1 Hash 路由与 History 路由对比

    现代 SPA 还常用 History API 实现“干净路径”,例如把 #/page1 写成 /page1。核心 API 是 history.pushState()、history.replaceState() 和 popstate 事件。两种模式解决的是同一个问题,但部署要求不同:

    对比项Hash 路由History 路由
    URL 形式 example.com/#/page1 example.com/page1
    核心监听 hashchange popstate
    主动改地址 修改 location.hash 或点击 Hash 链接 history.pushState() / replaceState()
    是否整页刷新 路由切换时不会 正确使用 API 时不会
    服务端配置 通常无需识别 Hash 后的路径 必须把前端路径回退到 SPA 入口 HTML
    直接刷新子路由 服务器仍收到入口文档路径,通常可用 配置不当时服务器可能返回 404
    URL 美观度 带 # 更接近普通站点路径
    适用场景 静态托管、教学、小型后台、部署受限场景 对 URL 规范和 SEO 配合要求更高的现代应用

    History 路由最容易踩的坑发生在“直接刷新”。在应用内部从 / 切到 /page1 时,pushState 只修改浏览器地址,不会请求服务器;但用户刷新 /page1 时,浏览器会真的向服务器请求这个路径。如果服务器没有把它回退到 index.html,就会返回 404。Hash 不会发送给服务器,因此天然绕开了这个部署问题。

    还要注意:调用 history.pushState() 本身不会触发 popstate,应用通常要在调用后主动渲染;popstate 主要用于响应浏览器前进和后退。这个设计与 Hash 路由的首屏问题类似——注册事件监听不等于当前状态已经被处理。

    5.2 从手写路由器到框架路由器

    当前 HashRouter 只实现了静态路径精确匹配,但它已经具备路由系统的骨架:路由表、URL 监听、路径解析、处理器分发和视图渲染。框架路由器是在这个骨架上继续解决复杂业务问题:

    • /users/:id 这样的动态参数需要路径模式解析,而不只是字符串完全相等。
    • 父布局与子页面需要嵌套路由和多个渲染出口。
    • 大型应用需要按路由拆包,通过动态 import() 延迟加载页面代码。
    • 登录校验、权限判断和未保存表单提示需要导航守卫。
    • 页面切换后还要处理滚动位置、标题、过渡动画和异步竞态。

    因此,生产项目通常不必重新发明完整路由库,但手写这个最小实现非常有价值。它揭示了 Vue Router、React Router 等工具背后的稳定模型:URL 是可观察状态,路由表把状态映射为处理逻辑,渲染层再把匹配结果变成界面。掌握这条主线后,再学习框架提供的声明式配置就不会停留在 API 记忆层面。

    总结

    Hash 最初用于文档内部定位,却因为“改变 URL、保留浏览历史、无需重新请求当前文档”这几个特征,成为实现前端路由的天然工具。一个最小 Hash 路由器只需要完成三步:监听 hashchange,从 location.hash 解析路径,再从路由表取出处理器更新挂载点。代码虽短,背后却串联了事件模型、this 绑定、类实例、函数引用、闭包、DOM 更新和浏览器会话历史等多项 JavaScript 基础。

    真正可靠的实现还要照顾事件之外的状态:首次进入页面应主动匹配当前 URL,空路径需要默认页,未知路径需要 404,绑定函数需要保留引用以便解绑,不可信数据不应直接进入 innerHTML。Hash 路由与 History 路由的核心模型相同,主要差异在 URL 形式和服务端回退要求。理解这些底层机制后,无论使用原生 JavaScript 还是框架路由器,都能更准确地判断路由为什么更新、为什么失效,以及应该从 URL、监听、匹配还是渲染哪一层开始排查。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 从页面跳转到手写 SPA:深入理解 Hash 路由与浏览器历史
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!