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

各位观众老爷,晚上好!我是老码农,今天咱们来聊聊Vue Router里两个亲兄弟:createWebHistorycreateWebHashHistory,看看它们是怎么监视你浏览器地址栏的小秘密的。准备好了吗?发车咯!

开场白:路由,前端的导航员

想象一下,你打开一个网站,点击不同的链接,页面内容随之改变,但页面并没有完全刷新。这就是路由在默默工作。它就像一个导航员,指引着你从一个“页面”到另一个“页面”,而这些“页面”其实是组件在不同状态下的呈现。

Vue Router 就是 Vue.js 官方提供的路由管理器,它让构建单页应用(SPA)变得简单。而 createWebHistorycreateWebHashHistory 则是两种不同的历史模式,它们决定了你的 URL 看起来是什么样子,以及如何与服务器交互。

第一幕:历史模式的选择,一场关于 URL 的审美之争

在开始深入源码之前,我们先来明确一下 createWebHistorycreateWebHashHistory 的区别。

特性 createWebHistory (HTML5 History Mode) createWebHashHistory (Hash Mode)
URL 形式 /about /#/about
SEO 友好度 更好 较差
服务器配置 需要 不需要
兼容性 现代浏览器 较好

简单来说,createWebHistory 的 URL 更干净,更符合传统网站的形式,但需要服务器配置来正确处理所有路由。而 createWebHashHistory 的 URL 带有 #,不需要服务器配置,兼容性更好,但 SEO 不友好。

第二幕:createWebHistory,优雅的舞者

createWebHistory 基于 HTML5 History API,它提供了 pushStatereplaceStatepopstate 事件,让我们可以修改 URL,而无需重新加载页面。

让我们先来看看 createWebHistory 的大致结构(以下代码为简化版本,仅用于说明原理):

function createWebHistory(base) {
  if (base === void 0) { base = ''; } // 默认 base 为空

  // 确保 base 以 '/' 开头和结尾
  base = ensureStartingSlash(base);
  base = normalizeBase(base);

  const history = {
    location: getCurrentLocation(base),
    state: history.state,
    push: (to, data) => {
      const url = base + to;
      window.history.pushState(data, '', url);
      history.location = getCurrentLocation(base); // 更新 location
      history.state = data;
      // 触发路由变化
      triggerListeners(history.location, history.state);
    },
    replace: (to, data) => {
      const url = base + to;
      window.history.replaceState(data, '', url);
      history.location = getCurrentLocation(base); // 更新 location
      history.state = data;
      // 触发路由变化
      triggerListeners(history.location, history.state);
    },
    listen: (callback) => {
      listeners.push(callback);
      return () => {
        listeners = listeners.filter(l => l !== callback);
      };
    },
    go: (delta) => {
      window.history.go(delta);
    },
    // ... 其他方法
  };

  // 监听 popstate 事件
  window.addEventListener('popstate', () => {
    history.location = getCurrentLocation(base);
    history.state = history.state; //  history.state 不会因为popstate改变
    triggerListeners(history.location, history.state);
  });

  return history;
}

// 辅助函数,用于获取当前 location
function getCurrentLocation(base) {
  const raw = base ? window.location.pathname.slice(base.length) : window.location.pathname;
  return raw || '/';
}

// 辅助函数,用于触发监听器
let listeners = [];
function triggerListeners(location, state) {
  listeners.forEach(listener => listener(location, state));
}

function ensureStartingSlash(path) {
  return path.startsWith('/') ? path : '/' + path;
}

function normalizeBase(base) {
    if (!base.endsWith('/')) {
        base += '/'
    }
    return base;
}

让我们逐行解读这段代码:

  1. createWebHistory(base): 接收一个 base 参数,用于指定应用的基础路径。例如,如果你的应用部署在 https://example.com/app/,那么 base 就应该是 /app/

  2. ensureStartingSlash(base)normalizeBase(base): 确保 base 路径以 / 开头和结尾,这是为了方便后续的 URL 处理。

  3. history 对象: 这是 createWebHistory 返回的核心对象,它包含了以下关键方法:

    • location: 当前的 URL 路径。
    • state: 与当前历史记录项关联的状态数据。
    • push(to, data): 向历史记录中添加一个新的条目,to 是要跳转的路径,data 是可选的状态数据。它会调用 window.history.pushState 来修改 URL,并触发路由变化。
    • replace(to, data): 替换当前历史记录项,类似于 push,但不会在历史记录中创建新的条目。
    • listen(callback): 注册一个回调函数,当路由发生变化时会被调用。这个回调函数接收新的 locationstate 作为参数。
    • go(delta): 在历史记录中前进或后退,delta 是一个整数,表示要前进或后退的步数。
  4. window.addEventListener('popstate', ...): 监听 popstate 事件,当用户点击浏览器的前进/后退按钮时,或者调用 history.go() 方法时,会触发这个事件。在事件处理函数中,我们会更新 history.locationhistory.state,并触发路由变化。注意,popstate 事件只会在浏览器历史记录发生变化时触发,而不会在调用 pushStatereplaceState 时触发。

  5. getCurrentLocation(base): 根据当前的 window.location.pathnamebase 路径,计算出当前的 URL 路径。

  6. triggerListeners(location, state): 遍历所有注册的监听器,并调用它们,将新的 locationstate 传递给它们。

关键点:pushStatereplaceStatepopstate

  • pushState(state, title, url): 向历史记录中添加一个新的条目。state 是与这个条目关联的数据,title 是页面的标题(现在大多数浏览器会忽略这个参数),url 是新的 URL。
  • replaceState(state, title, url): 替换当前历史记录条目。
  • popstate 事件: 当用户点击浏览器的前进/后退按钮,或者调用 history.go() 方法时,会触发这个事件。通过监听这个事件,我们可以知道用户在历史记录中进行了导航。

第三幕:createWebHashHistory,勤劳的搬运工

createWebHashHistory 使用 URL 的 hash 部分(# 及其后面的内容)来模拟路由。它不需要服务器配置,因为浏览器不会将 hash 部分发送到服务器。

以下是 createWebHashHistory 的简化版本:

function createWebHashHistory(base) {
  if (base === void 0) { base = ''; }

  base = ensureStartingSlash(base);
  base = normalizeBase(base);

  const history = {
    location: getHash(),
    state: {},
    push: (to, data) => {
      setHash(base + to);
      history.location = getHash();
      history.state = data;
      triggerListeners(history.location, history.state);
    },
    replace: (to, data) => {
      replaceHash(base + to);
      history.location = getHash();
      history.state = data;
      triggerListeners(history.location, history.state);
    },
    listen: (callback) => {
      listeners.push(callback);
      return () => {
        listeners = listeners.filter(l => l !== callback);
      };
    },
    go: (delta) => {
      window.history.go(delta); // hash 模式也需要 go,因为浏览器的前进后退需要能触发hashchange
    },
    // ... 其他方法
  };

  // 监听 hashchange 事件
  window.addEventListener('hashchange', () => {
    history.location = getHash();
    triggerListeners(history.location, history.state);
  });

  return history;

  function getHash() {
    let href = window.location.href;
    const index = href.indexOf('#');
    if (index < 0) return '/';

    href = href.slice(index + 1);
    return href.startsWith('/') ? href : '/' + href;
  }

  function setHash(path) {
    window.location.hash = path;
  }

  function replaceHash(path) {
    const href = window.location.href;
    const index = href.indexOf('#');
    window.location.replace(href.slice(0, index >= 0 ? index : href.length) + '#' + path);
  }

  // 辅助函数,用于触发监听器
  let listeners = [];
  function triggerListeners(location, state) {
    listeners.forEach(listener => listener(location, state));
  }

  function ensureStartingSlash(path) {
    return path.startsWith('/') ? path : '/' + path;
  }

  function normalizeBase(base) {
    if (!base.endsWith('/')) {
        base += '/'
    }
    return base;
  }
}

这段代码与 createWebHistory 类似,但有以下关键区别:

  1. getHash(): 从 window.location.href 中提取 hash 部分,作为当前的 URL 路径。

  2. setHash(path): 设置 window.location.hash,从而改变 URL 的 hash 部分。

  3. replaceHash(path): 替换当前的 hash 值,避免在历史记录中产生新的条目。

  4. window.addEventListener('hashchange', ...): 监听 hashchange 事件,当 URL 的 hash 部分发生变化时,会触发这个事件。在事件处理函数中,我们会更新 history.location,并触发路由变化。

关键点:hashchange 事件

  • hashchange 事件: 当 URL 的 hash 部分发生变化时,会触发这个事件。通过监听这个事件,我们可以知道用户在 hash 路由中进行了导航。

第四幕:监听 URL 变化的秘密武器

无论是 createWebHistory 还是 createWebHashHistory,它们的核心都在于监听 URL 的变化,并通知 Vue Router 进行相应的处理。

  • createWebHistory: 通过监听 popstate 事件来感知 URL 的变化。
  • createWebHashHistory: 通过监听 hashchange 事件来感知 URL 的变化。

当 URL 发生变化时,它们会:

  1. 更新 history.locationhistory.state
  2. 调用 triggerListeners 函数,通知所有注册的监听器。

Vue Router 会在这些监听器中更新组件的渲染,从而实现页面的切换。

第五幕:总结与思考

createWebHistorycreateWebHashHistory 是 Vue Router 中两种不同的历史模式,它们分别基于 HTML5 History API 和 URL 的 hash 部分来实现路由功能。

  • createWebHistory 的 URL 更干净,但需要服务器配置。
  • createWebHashHistory 不需要服务器配置,兼容性更好,但 URL 带有 #,SEO 不友好。

它们都通过监听特定的事件(popstatehashchange)来感知 URL 的变化,并通知 Vue Router 进行相应的处理。

理解了它们的实现原理,你就能更好地选择合适的历史模式,并更好地调试和优化你的 Vue Router 应用。

彩蛋:一些小技巧

  • base 选项: 在配置 Vue Router 时,可以使用 base 选项来指定应用的基础路径。这在部署到子目录时非常有用。
  • scrollBehavior 选项: 可以使用 scrollBehavior 选项来控制页面滚动行为。例如,可以在路由切换时滚动到页面顶部。
  • 路由守卫: 可以使用路由守卫(beforeEachbeforeResolveafterEach)来控制路由的访问权限,或者在路由切换前后执行一些操作。

好了,今天的分享就到这里。希望大家有所收获!如果有什么问题,欢迎在评论区留言。下次再见!

发表回复

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