路由中hash模式和History模式的实现原理

前言

在单页应用(SPA)席卷前端领域的今天,网页早已不再是"点击链接→等待刷新"的传统模式。当我们在浏览器地址栏输入URL时,页面竟能魔术般地切换内容而不刷新------这背后究竟藏着怎样的技术奥秘?

前端路由正是实现这种"魔法"的核心技术。它像一位聪明的"URL翻译官",在浏览器地址变化与页面组件展示之间搭建桥梁。无论是电商平台的商品分类切换,还是管理系统的菜单导航,都离不开路由机制的支撑。本文将带您揭开前端路由的神秘面纱,通过hash模式与history模式两大实现方案的对比,结合手写代码案例,彻底理解这项构建现代Web应用不可或缺的技术。

什么是路由?

用来描述服务器上资源的路径

前端路由是指?

  • 单页应用中

  • 构建 浏览器 url 地址 和 组件之间的映射关系

前端路由的本质

  • 路由是浏览器URL地址与前端组件之间的映射关系,充当"媒婆"的角色,实现URL变化时展示对应的页面组件。

前端路由实现的要求

  1. 要监听 url是否 变更了
  2. url变更时,浏览器不能刷新(单页应用,当浏览器刷新时,整个应用都会重新加载)

hash 模式

在浏览器眼里,url 携带了#,#后面的内容都会被认为是 hash 值,浏览器中 hash 值的变更不会带来页面变更

原理:通过对 url 设置监听,当 hash 值改变时,触发 hashchange 事件,使用 location.hash 获取 url 中#后面的 hash 值,根据 hash 值,找到数组对象中对应的组件,将组件渲染到页面中

手搓hash

我们首先需要监听'haschange'事件,其中的事件参数包含了我们需要监听的url地址,但是官方也为url地址栏专门打造了对象location,location.hash

js 复制代码
 window.addEventListener("hashchange", (e) => {
        console.log(e);
        renderView(window.location.hash);
      });

通过监听事件对象里面的url

js 复制代码
    function renderView(url) {
        const index = routes.findIndex((item) => {
          return "#" + item.path === url;
        });
        let routerView = document.getElementById("root");
        routerView.innerHTML = routes[index].component();
      }

这里相当于发布订阅模式中的订阅,当点击url发生变更的时候就相当于执行发布中的renderView函数,函数将,通过在routes数组里面找是否有对应的url地址,有就返回数组中对应路径的下标,然后再将数组里面对应的那个组件代码片段取出来出来展示。展示在这里div标签,通过设置这个div标签的innerHTML

js 复制代码
    window.addEventListener("DOMContentLoaded", () => {
        renderView(window.location.hash);
      });

增加一个"DOMContentLoaded"事件监听在页面初次刷新时进行组件的初始化

js 复制代码
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <ul>
      <li><a href="#/home">首页</a> <a href="#/about">关于</a></li>
    </ul>
    <!-- 当url变更后展示对应的代码片段 -->
    <div id="root"></div>
    <script>
      const routes = [
        {
          path: "/home",
          component: () => {
            return "<h1>首页e</h1>";
          },
        },
        {
          path: "/about",
          component: () => {
            return "<h1>关于e</h1>";
          },
        },
      ];
      window.addEventListener("hashchange", (e) => {
        console.log(e);
        renderView(window.location.hash);
      });
      function renderView(url) {
        const index = routes.findIndex((item) => {
          return "#" + item.path === url;
        });
        let routerView = document.getElementById("root");
        routerView.innerHTML = routes[index].component();
      }

      window.addEventListener("DOMContentLoaded", () => {
        renderView(window.location.hash);
      });
    </script>
  </body>
</html>

history 模式

浏览器提供了一个 history 对象,用来管理浏览器的历史记录,并且 history 对象中有 pushState、replaceState、popState 三个方法,通过调用这三个方法

pushState:向历史记录栈中添加一条记录

replaceState:替换当前历史记录栈中的记录

popState:向历史记录栈中取出最后一条记录

pushState 修改 url 不触发页面刷新

我们可以改变 url,并触发 popState 事件,从而实现路由跳转。即监听popState事件可以关联到浏览器的前进后退事件

window.history.pushState(null, "", item.getAttribute("href"));

注意:.getAttribute(), 这个叫读取标签身上的属性。任何标签都可以用getAttribute去读取它身上的任何属性

window.history.pushState(null, "", item.getAttribute("href"));

renderView(location.pathname)

但我们pushState改变了url之后通过渲染函数,渲染对应url的组件,怎么知道当前url路径,可以通过事件参数也可以通过location.pathname来得到对应的url

pushState()会进入浏览器的缓存栈中但是不会被前进和后退按钮事件触发,hash模式和多页应用在跳转了页面之后,跳转到的路径是会被浏览器的缓存站给缓存起来,拥有历史记录。但用pushstate跳转的url虽然有历史记录,但无法被浏览器的前进后退事件触发,后退无法回到pushState添加的url地址,而是回到pushState之前的url地址。那么我们回退的时候还想要监听回退事件,我们就可以监听history对象身上的popstate事件,单点击回退按钮,会触发,设计一个渲染函数,帮我们渲染对应url的组件

window.addEventListener('popstate', () => { // 监听浏览器的前进后退事件 renderView(location.pathname) }) 也就是触发这里的函数调用

html 复制代码
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <ul>
    <li><a href="/home">首页</a></li>
    <li><a href="/about">关于</a></li>
  </ul>

  <!-- 当 url 变更后,展示对应的代码片段 -->
  <div id="root"></div>

  <script>
    const routes = [
      {
        path: '/home',
        component: (val) => {
          return `<h1>首页页面${val}</h1>`
        }
      },
      {
        path: '/about',
        component: () => {
          return '<h1>关于页面</h1>'
        }
      }
    ]

    let routerView = document.getElementById('root')


    window.addEventListener('DOMContentLoaded', () => {
      onLoad()
    })

    window.addEventListener('popstate', () => {  // 监听浏览器的前进后退事件
      renderView(location.pathname)
    })


    function onLoad() {  // 渲染对应的组件
      let linkList = document.querySelectorAll('a[href]')
      linkList.forEach(el => {
        el.addEventListener('click', function(e) {
          e.preventDefault()  // 阻止默认行为
          history.pushState(null, '', el.getAttribute('href'))  // 进入浏览器的缓存栈,但是不受前进后退事件的影响

          // console.log(location);
          
          renderView(location.pathname)
        })
      })
      
    }


    function renderView(url) {
      const index = routes.findIndex(item => {
        return item.path === url
      })
      routerView.innerHTML = routes[index].component()
    }
  </script>
</body>
</html>

总结

从#号后的哈希值到干净的路径地址,从手动监听hashchange到利用history API优雅控制浏览历史,我们见证了前端路由技术的演进历程。这两种模式如同双生花:

  • hash模式以兼容性见长,像一位可靠的老匠人,用#符号在URL中划出专属领地
  • history模式则追求极致体验,通过pushState/replaceState实现无刷新导航,宛若现代建筑中的智能电梯
相关推荐
Code季风9 分钟前
Gin 框架中的模板引擎使用指南
服务器·前端·gin
狼性书生2 小时前
uniapp实现的圆形滚盘组件模板
前端·uni-app·vue·组件
芥子沫7 小时前
VSCode添加Python、Java注释技巧、模板
开发语言·前端·javascript
cos7 小时前
FE Bits 前端周周谈 Vol.2|V8 提速 JSON.stringify 2x,Vite 周下载首超 Webpack
前端·javascript·css
wfsm8 小时前
pdf预览Vue-PDF-Embed
前端
wangbing11258 小时前
界面规范的其他框架实现-列表-layui实现
前端·javascript·layui
Hurry69 小时前
web应用服务器tomcat
java·前端·tomcat
烛阴9 小时前
Sin -- 重复的、流动的波浪
前端·webgl
北'辰11 小时前
DeepSeek智能考试系统智能体
前端·后端·架构·开源·github·deepseek
前端历劫之路12 小时前
🔥 1.30 分!我的 JS 库 Mettle.js 杀入全球性能榜,紧追 Vue
前端·javascript·vue.js