深入理解 Vue Router 源码中 `createWebHistory` 和 `createWebHashHistory` 的实现细节,以及它们如何监听 URL 变化。

大家好,欢迎来到今天的“Vue Router 源码探秘”讲座。今天咱们不讲高深莫测的理论,直接扒开 Vue Router 的底裤,看看 createWebHistorycreateWebHashHistory 这两个家伙到底是怎么监听 URL 变化的。

咱们先来简单回顾一下,Vue Router 提供了几种不同的 history 模式,它们决定了你的应用 URL 的外观和行为。 其中最常见的两种就是今天的主角:

  • Web History (HTML5 History API): 使用 createWebHistory 创建,URL 看起来像正常的网站,例如 /about/users/123。它利用了浏览器提供的 history.pushStatehistory.replaceState 方法,以及 popstate 事件。

  • Hash History: 使用 createWebHashHistory 创建,URL 中会带有一个 # 符号,例如 /#/about/#/users/123。 它的工作原理是监听 URL 中 # 后面的内容变化,并且通过 window.location.hash 来读取和设置 hash 值。

那么,它们是如何具体实现的呢? 让我们开始深入源码!

一、createWebHistory 的实现细节

createWebHistory 的核心思想是利用 HTML5 History API 提供的能力,优雅地操作浏览器的历史记录,并且监听 popstate 事件来感知用户的前进后退操作。

  1. createWebHistory 函数

    首先,我们来看一下 createWebHistory 函数本身。 它的作用是创建一个 history 对象,这个对象会提供一些方法来操作 URL,并且监听 URL 的变化。

    // 简化后的 createWebHistory 函数 (源码位置: packages/router/src/history/html5.ts)
    function createWebHistory(base = '') {
      // 处理 base URL
      base = normalizeBase(base);
    
      // history 栈的初始状态
      const history = {
        location: START, // 初始 URL
        state: null,     // 初始 state
        base,
        push(to, data) {
          // ... pushState 的逻辑
        },
        replace(to, data) {
          // ... replaceState 的逻辑
        },
        listen(callback) {
          // ... 监听 popstate 事件
        },
        destroy() {
          // ... 移除监听器
        },
      };
    
      return history;
    }
    
    // normalizeBase 函数,用于处理 base URL 的逻辑
    function normalizeBase(base) {
        if (!base) {
            return '';
        }
        if (base.startsWith('/')) {
            return base;
        }
        if (base.startsWith('.')) {
            // 处理相对路径
            return (new URL(base, document.location.origin)).pathname;
        }
        // 如果不是 '/' 或 '.' 开头,则认为是绝对路径
        return '/' + base;
    }

    这个 createWebHistory 函数返回一个对象,这个对象包含一些方法,比如 pushreplacelisten。 这些方法会被 Vue Router 内部调用,用来更新 URL 和监听 URL 的变化。 normalizeBase 函数是为了确保 base URL 的格式正确。

  2. pushreplace 方法

    push 方法用于向历史记录中添加一个新的条目,replace 方法用于替换当前的条目。 它们都利用了 history.pushStatehistory.replaceState 方法。

    // 简化后的 push 和 replace 方法
    push(to, data) {
      const url = resolveURL(to, history.base); // 解析 URL
      history.state = data;
      history.location = url;
      window.history.pushState(data, '', url);
    },
    replace(to, data) {
      const url = resolveURL(to, history.base); // 解析 URL
      history.state = data;
      history.location = url;
      window.history.replaceState(data, '', url);
    },
    
    // resolveURL 函数,用于解析 URL
    function resolveURL(to, base) {
        if (to.startsWith('/')) {
            return base + to;
        }
        if (to.startsWith('.')) {
            return (new URL(to, document.location.origin + base)).pathname;
        }
        // 如果不是 '/' 或 '.' 开头,则认为是绝对路径
        return to;
    }

    这两个方法都接收一个 to 参数,表示要跳转到的 URL。 它们还会接收一个可选的 data 参数,这个参数可以用来存储一些状态信息,这些信息会和历史记录一起保存。 resolveURL 函数用来将传入的 to 参数解析成完整的 URL。

  3. listen 方法

    listen 方法用于监听 popstate 事件。 当用户点击浏览器的前进或后退按钮时,会触发 popstate 事件。

    // 简化后的 listen 方法
    listen(callback) {
      const popStateHandler = () => {
        history.location = window.location.pathname; // 获取当前 URL
        callback(history.location, history.state);    // 调用回调函数
      };
    
      window.addEventListener('popstate', popStateHandler);
    
      return () => {
        window.removeEventListener('popstate', popStateHandler); // 返回一个函数,用于移除监听器
      };
    },

    listen 方法接收一个回调函数,当 popstate 事件触发时,会调用这个回调函数,并且传入当前 URL 和 state。 它返回一个函数,调用这个函数可以移除监听器。

    注意: popstate 事件只会在用户通过浏览器的前进后退按钮或者调用 history.back()history.forward()history.go() 方法时触发。 通过 history.pushState() 或者 history.replaceState() 方法改变 URL 不会触发 popstate 事件。 这也是为什么 Vue Router 需要手动更新内部状态的原因。

  4. 总结

    createWebHistory 的实现原理可以总结为以下几点:

    • 使用 history.pushStatehistory.replaceState 方法来操作 URL。
    • 监听 popstate 事件来感知用户的导航操作。
    • 手动更新内部状态,因为 pushStatereplaceState 方法不会触发 popstate 事件。

二、createWebHashHistory 的实现细节

createWebHashHistory 的实现相对简单,因为它只需要操作 URL 中的 hash 部分。

  1. createWebHashHistory 函数

    // 简化后的 createWebHashHistory 函数 (源码位置: packages/router/src/history/hash.ts)
    function createWebHashHistory(base = '') {
      // 处理 base URL
      base = normalizeBase(base);
    
      const history = {
        location: getHash(), // 初始 URL
        state: null,     // 初始 state
        base,
        push(to, data) {
          // ... 设置 hash 的逻辑
        },
        replace(to, data) {
          // ... 替换 hash 的逻辑
        },
        listen(callback) {
          // ... 监听 hashchange 事件
        },
        destroy() {
          // ... 移除监听器
        },
      };
    
      return history;
    }
    
    // normalizeBase 函数,用于处理 base URL 的逻辑
    function normalizeBase(base) {
        if (!base) {
            return '';
        }
        if (base.startsWith('/')) {
            return base;
        }
        if (base.startsWith('.')) {
            // 处理相对路径
            return (new URL(base, document.location.origin)).pathname;
        }
        // 如果不是 '/' 或 '.' 开头,则认为是绝对路径
        return '/' + base;
    }
    
    // getHash 函数,用于获取 hash 值
    function getHash() {
        let href = window.location.href;
        const index = href.indexOf('#');
        if (index < 0) return '';
    
        href = href.slice(index + 1);
        return href;
    }

    createWebHashHistory 函数返回一个 history 对象,这个对象包含一些方法,比如 pushreplacelisten。 这些方法会被 Vue Router 内部调用,用来更新 URL 和监听 URL 的变化。 getHash 函数用于获取当前 URL 中的 hash 值。

  2. pushreplace 方法

    pushreplace 方法用于设置 URL 中的 hash 值。

    // 简化后的 push 和 replace 方法
    push(to, data) {
      setHash(to);
      history.location = to;
      history.state = data;
    },
    replace(to, data) {
      setHash(to);
      history.location = to;
      history.state = data;
    },
    
    // setHash 函数,用于设置 hash 值
    function setHash(to) {
      window.location.hash = to;
    }

    这两个方法都接收一个 to 参数,表示要设置的 hash 值。 它们会直接修改 window.location.hash 属性。

  3. listen 方法

    listen 方法用于监听 hashchange 事件。 当 URL 中的 hash 值发生变化时,会触发 hashchange 事件。

    // 简化后的 listen 方法
    listen(callback) {
      const hashChangeHandler = () => {
        history.location = getHash(); // 获取当前 hash 值
        callback(history.location, history.state);    // 调用回调函数
      };
    
      window.addEventListener('hashchange', hashChangeHandler);
    
      return () => {
        window.removeEventListener('hashchange', hashChangeHandler); // 返回一个函数,用于移除监听器
      };
    },

    listen 方法接收一个回调函数,当 hashchange 事件触发时,会调用这个回调函数,并且传入当前 hash 值和 state。 它返回一个函数,调用这个函数可以移除监听器。

  4. 总结

    createWebHashHistory 的实现原理可以总结为以下几点:

    • 使用 window.location.hash 属性来操作 URL 中的 hash 值。
    • 监听 hashchange 事件来感知 URL 的变化。

三、两种 History 模式的比较

为了更清晰地理解 createWebHistorycreateWebHashHistory 的区别,我们用一个表格来对比一下它们:

特性 createWebHistory (HTML5 History API) createWebHashHistory (Hash History)
URL 外观 /about, /users/123 /#/about, /#/users/123
依赖 浏览器支持 HTML5 History API 浏览器基本支持
SEO 友好性 更好 较差
服务器配置 需要服务器配置,确保所有路由都指向 index.html 不需要服务器配置
监听事件 popstate hashchange
实现复杂度 较高 较低
初次加载 URL 正常显示 URL 中包含 #

四、URL 变化的监听机制深度剖析

现在,我们来深入探讨一下这两种 history 模式是如何监听 URL 变化的。

1. createWebHistorypopstate 事件监听

createWebHistory 依赖浏览器的 popstate 事件。这个事件在以下情况下会被触发:

  • 用户点击浏览器的前进或后退按钮。
  • JavaScript 代码调用 history.back()history.forward()history.go() 方法。

popstate 事件触发时,createWebHistory 会:

  1. 读取 window.location.pathname 获取当前的 URL。
  2. 调用注册的回调函数,通知 Vue Router URL 已经改变。

重要提示: history.pushState()history.replaceState() 方法虽然可以改变 URL,但它们不会触发 popstate 事件! 这就是为什么 Vue Router 在调用这两个方法之后,需要手动更新内部状态的原因。

为什么 pushStatereplaceState 不触发 popstate

这是 HTML5 History API 的设计决定的。 如果每次调用 pushStatereplaceState 都触发 popstate 事件,那么会导致无限循环,因为 popstate 事件的处理函数又会调用 pushStatereplaceState

2. createWebHashHistoryhashchange 事件监听

createWebHashHistory 依赖浏览器的 hashchange 事件。这个事件在以下情况下会被触发:

  • URL 中的 hash 值发生变化。
  • JavaScript 代码直接修改 window.location.hash 属性。

hashchange 事件触发时,createWebHashHistory 会:

  1. 读取 window.location.hash 获取当前的 hash 值。
  2. 调用注册的回调函数,通知 Vue Router URL 已经改变。

总结:

  • createWebHistory 通过监听 popstate 事件来感知浏览器的前进后退操作,并且手动更新内部状态。
  • createWebHashHistory 通过监听 hashchange 事件来感知 URL 中 hash 值的变化。

五、 实际应用场景分析

让我们通过一些实际场景来分析这两种 History 模式的选择:

  • 场景 1: 需要良好的 SEO

    如果你的应用需要被搜索引擎抓取,那么 createWebHistory 是更好的选择。 因为搜索引擎可以更容易地理解和索引没有 # 符号的 URL。

  • 场景 2: 兼容旧浏览器

    如果你的应用需要兼容一些比较老的浏览器,而这些浏览器可能不支持 HTML5 History API,那么 createWebHashHistory 是一个更安全的选择。 因为所有浏览器都支持 hashchange 事件。

  • 场景 3: 简单的静态网站

    如果你的应用是一个简单的静态网站,不需要服务器端的特殊配置,那么 createWebHashHistory 可以让你快速部署。

  • 场景 4: 需要控制应用的 base URL

    两种模式都支持设置 base URL,但是 createWebHistory 更加灵活,因为它可以处理更复杂的 base URL 结构。

六、 源码调试技巧

如果你想更深入地了解 Vue Router 的源码,可以尝试以下调试技巧:

  1. 下载 Vue Router 的源码: 从 GitHub 上下载 Vue Router 的源码。
  2. 使用 console.log 语句: 在关键的代码位置插入 console.log 语句,打印一些变量的值,以便了解代码的执行流程。
  3. 使用断点调试: 在浏览器或者 IDE 中设置断点,然后运行你的 Vue 应用,当代码执行到断点时,你可以查看当前的变量值和调用栈。
  4. 阅读测试用例: Vue Router 的测试用例可以帮助你理解代码的功能和用法。

七、 总结

今天我们深入探讨了 Vue Router 源码中 createWebHistorycreateWebHashHistory 的实现细节,以及它们是如何监听 URL 变化的。希望通过今天的讲解,你能够对 Vue Router 的内部机制有更深入的了解。

记住,理解源码不是为了死记硬背,而是为了更好地理解框架的设计思想,从而能够更灵活地使用框架,并且在遇到问题时能够更快地找到解决方案。

今天的讲座就到这里,谢谢大家!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注