详细阐述 Vue Router 源码中 `createWebHistory` 和 `createWebHashHistory` 的实现细节,以及它们如何监听 URL 变化。

各位观众老爷,晚上好!今天咱们来聊聊 Vue Router 里的两位老朋友:createWebHistorycreateWebHashHistory。这两位,一个用正经的 URL,一个用“不正经”的 URL(带 #),但都能让你的 Vue 应用实现页面跳转,背后到底藏着什么猫腻呢? 咱们今天就来扒一扒它们的底裤(源码),看看它们是怎么监听 URL 变化的。

开场白:历史模式与哈希模式,两个世界的选择

首先,简单回顾一下这两种模式的区别:

  • Web History 模式 (createWebHistory):使用标准的 URL,看起来更“优雅”,例如 https://example.com/about。需要服务器端的配置来支持,否则刷新页面可能会出现 404 错误。
  • Web Hash 模式 (createWebHashHistory):URL 中带有一个 # 符号,例如 https://example.com/#/about。不需要服务器端配置,因为 # 后面的内容不会发送到服务器。

选择哪种模式取决于你的需求和服务器环境。如果你的服务器支持 History API,那么 History 模式是更好的选择。否则,Hash 模式是一个安全的选择。

第一幕:createWebHistory 的身世之谜

createWebHistory 返回一个 history 对象,这个对象包含了一系列的方法和属性,用于操作浏览器的历史记录。我们先来看看它的主要实现部分:

// 简化后的 createWebHistory 实现
function createWebHistory(base) {
  base = normalizeBase(base); // 规范化 base URL

  const history = {
    location: getCurrentLocation(base), // 获取当前 URL
    state: historyState, // 获取历史状态

    push: (to, data) => {
      const targetLocation = resolve(to); // 解析 to 地址
      pushWithRedirect(targetLocation, data); // 执行 push 操作
    },
    replace: (to, data) => {
       const targetLocation = resolve(to);
      replaceWithRedirect(targetLocation, data); // 执行 replace 操作
    },
    go: (delta) => {
      window.history.go(delta); // 调用浏览器的 go 方法
    },
    back: () => history.go(-1), // 返回上一页
    forward: () => history.go(1), // 前进到下一页

    listen: (callback) => {
      const removeListener = popStateHandler.listen(callback); // 监听 popstate 事件
      return removeListener;
    },
    destroy: () => {
       popStateHandler.destroy();
    },
  };

  //初始化页面状态
  if (historyState === START) {
        replace(history.location)
  }

  return history;
}

// 规范化 base URL
function normalizeBase(base) {
  if (!base) {
    return '/';
  }
  if (base.startsWith('.')) {
    base = new URL(base, document.baseURI).pathname;
  }
  if (!base.endsWith('/')) {
    base += '/';
  }
  return base;
}

// 获取当前 URL
function getCurrentLocation(base) {
  const raw = window.location.pathname;
  // Remove base first
  if (base !== '/' && raw.startsWith(base)) {
    return raw.slice(base.length);
  }
  return raw;
}

// 解析 to 地址
function resolve(to) {
  if (typeof to === 'string') {
    return to;
  }
  if (typeof to === 'object' && to != null) {
    const { path, query, hash } = to;
    let target = path;

    if (query) {
      target += '?' + new URLSearchParams(query).toString();
    }

    if (hash) {
      target += '#' + hash;
    }
    return target;
  }

  // 不支持的类型直接返回空
  return '';
}

//push 操作,如果需要重定向,则处理重定向
function pushWithRedirect(to, data){
     try {
      // This will fail on file:// because the security settings
      window.history.pushState(data, '', base + to);
      history.location = to;
      history.state = data;
      popStateHandler.onPopState({ state: data, path: to });
    } catch (error) {
      //如果出错,调用replace
      replaceWithRedirect(to, data)
    }
}

//replace 操作,如果需要重定向,则处理重定向
function replaceWithRedirect(to, data){
      try {
      window.history.replaceState(data, '', base + to);
      history.location = to;
      history.state = data;
      popStateHandler.onPopState({ state: data, path: to });
    } catch (error) {
      window.location.replace(base + to);
    }
}

const START = Symbol()
let historyState = START

const popStateHandler = {
  listeners: [],
  listen(callback) {
    this.listeners.push(callback);
    return () => {
      this.listeners = this.listeners.filter((l) => l !== callback);
    };
  },
  onPopState(event) {
    const to = getCurrentLocation(base);
    const from = history.location;
    history.location = to
    this.listeners.forEach((callback) => {
      callback(to, from, event.state);
    });
  },
  destroy() {
    window.removeEventListener('popstate', this.onPopState);
  },
};

window.addEventListener('popstate', (event) => {
  popStateHandler.onPopState(event);
});

代码解读:

  1. normalizeBase(base) 这个函数负责规范化 base URL。base 是你的应用部署的根路径,例如 /app/。规范化的过程包括:

    • 如果 base 为空,则设置为 /
    • 如果 base. 开头,则将其解析为绝对路径。
    • 如果 base 不以 / 结尾,则添加 /
  2. getCurrentLocation(base) 这个函数用于获取当前 URL,并移除 base 部分。例如,如果当前 URL 是 https://example.com/app/aboutbase/app/,那么 getCurrentLocation 会返回 about

  3. push(to, data)replace(to, data) 这两个函数分别用于向历史记录中添加一个新的条目和替换当前的条目。它们的核心是调用 window.history.pushState(data, '', base + to)window.history.replaceState(data, '', base + to)。这两个方法是 HTML5 History API 的一部分,它们允许你修改 URL 而无需重新加载页面。data 参数用于存储与该历史记录条目相关的数据,可以在 popstate 事件中访问。

  4. go(delta)back()forward() 这些函数用于在历史记录中前进或后退。它们直接调用 window.history.go(delta),其中 delta 是一个整数,表示要前进或后退的步数。back()forward() 只是 go() 的简写形式。

  5. resolve(to): 这个函数用来标准化to的格式,to可以是一个字符串,也可以是一个对象,如果是对象则将其解析为字符串路径。

  6. listen(callback) 重点来了!这个函数用于监听 URL 的变化。它通过监听 popstate 事件来实现。popstate 事件在浏览器的历史记录发生变化时触发,例如用户点击了浏览器的前进或后退按钮。listen 函数将 callback 添加到 popStateHandler.listeners 数组中,当 popstate 事件发生时,popStateHandler.onPopState 会调用所有注册的回调函数。

  7. popStateHandler 对象: 这个对象负责管理 popstate 事件的监听器。它包含一个 listeners 数组,用于存储所有的回调函数。onPopState 方法在 popstate 事件发生时被调用,它会遍历 listeners 数组,并调用每个回调函数,将新的 URL 和历史状态传递给它们。

  8. 初始化逻辑: 如果 historyState === START,表示页面是第一次加载,那么会调用 replace 方法,用当前的 URL 替换历史记录中的第一个条目。这样做的目的是为了防止用户在刷新页面后,点击后退按钮会返回到空白页面。

popstate 事件:监听 URL 变化的秘密武器

popstate 事件是 createWebHistory 监听 URL 变化的关键。当用户点击浏览器的前进或后退按钮,或者调用 history.go()history.back()history.forward() 方法时,浏览器会触发 popstate 事件。

createWebHistory 通过监听 popstate 事件,可以感知到 URL 的变化,并执行相应的操作,例如更新 Vue 应用的状态。

第二幕:createWebHashHistory 的另类人生

createWebHistory 不同,createWebHashHistory 使用 URL 中的 # 符号来模拟页面跳转。我们来看看它的实现:

// 简化后的 createWebHashHistory 实现
function createWebHashHistory(base) {
  base = normalizeBase(base);

  const history = {
    location: getHash(), // 获取当前 hash 值
    state: historyState,

    push: (to, data) => {
      writeHash(to, data);
    },
    replace: (to, data) => {
      writeHash(to, data, true);
    },
    go: (delta) => {
      window.history.go(delta);
    },
    back: () => history.go(-1),
    forward: () => history.go(1),

    listen: (callback) => {
      const removeListener = hashChangeHandler.listen(callback);
      return removeListener;
    },
    destroy: () => {
       hashChangeHandler.destroy()
    },
  };

  //初始化页面状态
  if (historyState === START) {
        replace(history.location)
  }

  return history;
}

function normalizeBase(base) {
  if (!base) {
    return '/';
  }
  if (base.startsWith('.')) {
    base = new URL(base, document.baseURI).pathname;
  }
  if (!base.endsWith('/')) {
    base += '/';
  }
  return base;
}

function getHash() {
  let href = window.location.href;
  const hashIndex = href.indexOf('#');
  if (hashIndex < 0) {
    return '/';
  }
  href = href.slice(hashIndex + 1);
  return href;
}

function writeHash(path, data, replace) {
  if (replace === true) {
    window.location.replace(getHashBaseURL() + '#' + path);
  } else {
    window.location.hash = path;
  }
  history.location = path;
  history.state = data;

  hashChangeHandler.onHashChange({newURL: getHashBaseURL() + '#' + path, oldURL: getHashBaseURL() + '#' + history.location});
}

function getHashBaseURL() {
  const href = window.location.href;
  const hashIndex = href.indexOf('#');
  return hashIndex < 0 ? href : href.slice(0, hashIndex);
}

const START = Symbol()
let historyState = START

const hashChangeHandler = {
  listeners: [],
  listen(callback) {
    this.listeners.push(callback);
    return () => {
      this.listeners = this.listeners.filter((l) => l !== callback);
    };
  },
  onHashChange(event) {
    const to = getHash();
    const from = history.location;
    history.location = to;
    this.listeners.forEach((callback) => {
      callback(to, from, event.state);
    });
  },
  destroy() {
      window.removeEventListener('hashchange', this.onHashChange)
  },
};

window.addEventListener('hashchange', (event) => {
  hashChangeHandler.onHashChange(event);
});

代码解读:

  1. getHash() 这个函数用于获取当前 URL 中的 hash 值。它通过查找 # 符号的位置来提取 hash 值。

  2. writeHash(path, data, replace) 这个函数用于修改 URL 中的 hash 值。它直接修改 window.location.hash 属性。如果 replace 参数为 true,则使用 window.location.replace 方法,这样可以避免在历史记录中添加新的条目。

  3. listen(callback)createWebHistory 类似,这个函数用于监听 URL 的变化。但是,createWebHashHistory 监听的是 hashchange 事件。hashchange 事件在 URL 中的 hash 值发生变化时触发。

  4. hashChangeHandler 对象: 这个对象负责管理 hashchange 事件的监听器。它的工作方式与 popStateHandler 类似。

  5. getHashBaseURL(): 获取#号前面的链接

hashchange 事件:监听 Hash 变化的利器

hashchange 事件是 createWebHashHistory 监听 URL 变化的关键。当用户修改 URL 中的 hash 值时,浏览器会触发 hashchange 事件。

createWebHashHistory 通过监听 hashchange 事件,可以感知到 URL 的变化,并执行相应的操作。

第三幕:URL 变化监听机制大PK

特性 createWebHistory createWebHashHistory
使用的 API History API window.location.hash
监听的事件 popstate hashchange
URL 形式 标准 URL # 的 URL
服务器端配置需求 需要 不需要

总结:

createWebHistorycreateWebHashHistory 都提供了监听 URL 变化的能力,但它们使用了不同的 API 和事件。createWebHistory 使用 History API 和 popstate 事件,可以创建更“优雅”的 URL,但需要服务器端的配置支持。createWebHashHistory 使用 window.location.hashhashchange 事件,不需要服务器端的配置,但 URL 中会带有一个 # 符号。

希望今天的讲解能帮助你更好地理解 Vue Router 的内部机制。下次再见!

发表回复

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