Logo

Vue-Router 中 hash 和 history 的区别

前端

hash 和 history 的区别

  • hash 模式: 通过 URL 中的哈希 (#) 部分来保持状态。
  • history 模式: 通过 HTML5 History API 来保持状态。

Hash 模式工作机制

  • URL 形式: http://example.com/#/path
  • 实现方式: 使用 URL 中的哈希 (#) 部分,后面的路径由前端路由控制。
  • 浏览器兼容性: 兼容性好,支持所有浏览器,包括较旧的版本。
  • 服务器配置: 不需要特殊配置,因为请求不会被发送到服务器。

History 模式工作机制

  • URL 形式: http://example.com/path
  • 实现方式: 使用 HTML5 History API (pushStatereplaceState) 来管理前端路由。
  • 浏览器兼容性: 现代浏览器支持,但不兼容 IE9 及更早版本。
  • 服务器配置: 会发送真实的请求,所以需要服务器配置以支持单页面应用。服务器需要返回 index.html 对于所有的路由请求,否则刷新页面时会出现 404 错误。

使用 nodejs 作为服务,可以这样写:

fs.readFile('index.html', 'utf-8', (err, content) => {
    if (err) {
      console.log('We cannot open "index.htm" file.')
    }

    res.writeHead(200, {
      'Content-Type': 'text/html; charset=utf-8'
    })

    res.end(content)
})

Vue Router 源码分析

Hash 模式源码解析

  1. 初始化:
    Vue Router 初始化时,根据配置选择 hash 模式并创建相应 实例(HashHistory),类在初始化时确保 URL 包含哈希值。

  2. 启动监听:
    HashHistory 类中,会启动对 hashchange 事件的监听,这样每当 URL 哈希部分变化时,路由器能够感知到变化并做出相应处理。

  3. 获取和设置哈希:
    通过 getHashpushHash 方法,获取当前哈希值和向 URL 添加新的哈希值。这些方法操作 window.location.hash 来读取和更新哈希。

以下是相关源码的简化版分析:

HashHistory 类:

class HashHistory extends History {
  constructor(router, base) {
    super(router, base);
    // 确保 URL 有哈希值
    ensureSlash();
  }

  setupListeners() {
    window.addEventListener('hashchange', () => {
      // 当 hash 变化时,触发响应逻辑
      this.transitionTo(getHash(), () => {
        // 更新 URL 或其他操作
        replaceHash(this.current.fullPath);
      });
    });
  }

  push(location) {
    // 导航到新地址
    this.transitionTo(location, route => {
      pushHash(route.fullPath);
    });
  }

  replace(location) {
    // 替换当前地址
    this.transitionTo(location, route => {
      replaceHash(route.fullPath);
    });
  }
}

辅助函数:

// 获取当前哈希值
function getHash() {
  let href = window.location.href;
  let index = href.indexOf('#');
  return index === -1 ? '' : href.slice(index + 1);
}

// 更新哈希值
function pushHash(path) {
  window.location.hash = path;
}

// 替换哈希值
function replaceHash(path) {
  let i = window.location.href.indexOf('#');
  window.location.replace(
    window.location.href.slice(0, i >= 0 ? i : 0) + '#' + path
  );
}

// 确保 URL 有哈希值
function ensureSlash() {
  if (window.location.hash.charAt(1) !== '/') {
    window.location.hash = '/';
  }
}

History 模式源码解析

history 模式的实现主要依赖于 HTML5 History API,包括 pushStatereplaceState 方法,以及监听 popstate 事件。以下是一个简要的分析:

  1. 初始化:
    Vue Router 初始化时,根据配置选择 history 模式并创建相应的 History 实例(HTML5History)。

  2. 启动监听:
    HTML5History 类中,会启动对 popstate 事件的监听,这样每当浏览器的历史记录发生变化时,路由器能够感知到变化并做出相应处理。

  3. 导航操作:
    通过 pushStatereplaceState 方法更新浏览器的历史记录,同时不会触发页面刷新。

HTML5History 类:

class HTML5History extends History {
  constructor(router, base) {
    super(router, base);
    // 确保基路径存在
    this.ensureSlash();
  }

  setupListeners() {
    window.addEventListener('popstate', event => {
      this.transitionTo(getLocation(this.base), route => {
        // 更新 URL 或其他操作
      });
    });
  }

  push(location, onComplete, onAbort) {
    const { fullPath } = this.router.resolve(location, this.current, false);
    this.transitionTo(fullPath, route => {
      pushState(cleanPath(this.base + fullPath));
      onComplete && onComplete(route);
    }, onAbort);
  }

  replace(location, onComplete, onAbort) {
    const { fullPath } = this.router.resolve(location, this.current, false);
    this.transitionTo(fullPath, route => {
      replaceState(cleanPath(this.base + fullPath));
      onComplete && onComplete(route);
    }, onAbort);
  }
}

辅助函数:

function getLocation(base) {
  let path = window.location.pathname;
  if (base && path.indexOf(base) === 0) {
    path = path.slice(base.length);
  }
  return (path || '/') + window.location.search + window.location.hash;
}

function pushState(url) {
  window.history.pushState({ key: genKey() }, '', url);
}

function replaceState(url) {
  window.history.replaceState({ key: genKey() }, '', url);
}

function cleanPath(path) {
  return path.replace(/\/\//g, '/');
}

function genKey() {
  return Date.now().toFixed(3);
}

路由匹配原理

transitionTo 方法是路由器的核心方法,用于进行路由匹配和导航。Vue Router 在切换页面时利用了 Vue 的虚拟 DOM 机制来高效地管理组件的创建和销毁。

transitionTo 源码解析:

// Vue Router 源码简化版
transitionTo(location, onComplete, onAbort) {
  const route = this.match(location);
  this.confirmTransition(route, () => {
    this.updateRoute(route);
    onComplete && onComplete(route);
  }, onAbort);
}

<router-view> 组件的核心是一个渲染函数,它根据当前的路由状态动态渲染对应的组件。

// Vue Router 源码简化版
export default {
  name: 'RouterView',
  functional: true,
  render(h, { parent, data }) {
    const route = parent.$route;
    const matched = route.matched[0];
    if (!matched) {
      return h();// 如果没有匹配的路由,则返回空
    }
    // 获取默认的路由组件(假设没有使用命名视图,默认情况下路由组件存储在 default 属性中)
    const component = matched.components.default;
    // 使用 Vue 的渲染函数 h 创建一个虚拟节点,component 是要渲染的 Vue 组件。
    return h(component, data);
  }
}

手写 mini hash router

这个任务要求实现一个简单的 hash 路由器,该路由器应该具备路由匹配、导航等功能。

实现路由匹配

首先,需要实现路由匹配的功能。这个功能应该能够根据当前的 URL 路径,找到对应的组件或页面。

实现导航

接下来,需要实现导航功能。这个功能应该能够根据用户点击的链接,更新 URL 路径,并触发路由匹配和页面更新。

<!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>
    <script>
      class Router {
        constructor() {
          this.routes = {} // 存放路由path及callback
          this.currentUrl = ''

          // 绑定 this 到 refresh 方法
          this.refresh = this.refresh.bind(this)

          // 监听路由change调用相对应的路由回调
          window.addEventListener('load', this.refresh, false)
          window.addEventListener('hashchange', this.refresh, false)
        }

        // 注册路由及其对应的回调
        route(path, callback) {
          console.log(callback);
          this.routes[path] = callback || function () {}
        }

        // 更新 currentUrl 并调用相应的回调
        refresh() {
          this.currentUrl = location.hash.slice(1) || '/'
          this.routes[this.currentUrl] && this.routes[this.currentUrl]()
        }

        // 手动跳转到指定路径
        push(path) {
          location.hash = path
        }
      }

      // 使用示例
      const router = new Router()
      router.route('/', function () {
        console.log('Home page')
      })
      router.route('/about', function() {
        console.log('About page');
      });

      // 手动导航
      router.push('/about'); // 会在控制台输出 'About page'
    </script>
  </body>
</html>

总结

  • hash 模式: 兼容性好,支持所有浏览器,包括较旧的版本。
  • history 模式: 需要 HTML5 History API 支持,但提供了更好的用户体验。
  • 服务器配置: hash 模式不需要特殊配置,而 history 模式需要服务器配置以支持单页面应用。

参考