人人都会AI编程

12.8 路由原理:hash 与 history 模式的底层实现

更新时间:2026-07-10

Vue Router 提供了三种路由模式:hashhistorymemory。其中前两种是浏览器端单页应用(SPA)最常用的模式,它们核心要做的是同一件事:让 URL 变化时,不向服务器重新请求整个页面,而是只切换前端的视图组件。它们的实现完全依赖浏览器原生 API,理解这些底层机制,能帮你更好地处理路由相关的 bug、配置服务器,甚至手动实现一个简易路由器。

hash 模式:基于锚点的兼容方案

hash 模式下的 URL 格式为:https://example.com/#/user/profile。其中 # 以及它后面的部分叫做哈希值(hash),它原本的作用是用于页内锚点定位,但前端路由系统利用它来实现无刷新的页面切换。

底层原理

  • 浏览器不会将 hash 变化发送给服务器。改变 # 后面的内容,浏览器只认为是当前页面内部的位置跳转,不会触发页面重新加载。
  • 当 hash 变化时,浏览器会触发 hashchange 事件。我们只需要监听这个事件,从中读取新的 hash,然后渲染对应的组件即可。
  • 初始加载时,读取 window.location.hash 来决定渲染哪个组件。

极简实现
下面是用原生 JavaScript 手动实现一个 hash 路由器的核心逻辑:

class HashRouter {
  constructor(routes) {
    this.routes = routes;              // 路由表 { '/home': HomeComponent, ... }
    this.currentComponent = null;
    window.addEventListener('hashchange', () => this.onHashChange());
    // 首次加载,手动触发匹配
    this.onHashChange();
  }
  onHashChange() {
    const hash = window.location.hash.slice(1) || '/'; // 去掉 #
    const component = this.routes[hash];
    if (component) {
      // 模拟渲染:实际应用中会通知 Vue 更新 router-view 中的组件
      document.getElementById('app').innerHTML = component();
    }
  }
  push(path) {
    window.location.hash = path; // 修改 hash 会触发 hashchange
  }
}

// 使用示例
const routes = {
  '/': () => '<h1>首页</h1>',
  '/about': () => '<h1>关于我们</h1>'
};
const router = new HashRouter(routes);
  • 兼容性hashchange 事件兼容所有浏览器,包括 IE8。这使得 hash 模式成为历史遗留项目或要求极致兼容场景的可靠选择。
  • 优点:无需任何服务器特殊配置,部署到任何静态服务器都能直接运行。
  • 缺点:URL 中带有 #,不够美观,对 SEO 也不友好(虽然现代爬虫已经有所改善,但 # 仍然会让 URL 显得不那么“标准”)。

history 模式:基于 HTML5 History API 的现代方案

history 模式下的 URL 格式为:https://example.com/user/profile。看上去和普通的服务端路由完全一样,但它在跳转时同样不重新加载页面。

底层原理

  • 依赖于 HTML5 引入的 history.pushState()history.replaceState() 方法。这两个方法可以在不刷新页面的情况下,改变浏览器地址栏的 URL,并同时在会话历史中添加一条记录。
  • 当前进/后退(用户点击浏览器按钮)时,会触发 popstate 事件。注意:pushStatereplaceState 调用时不会触发 popstate,只有浏览器的后退/前进等操作才会触发。因此需要在全局点击拦截或编程式导航里主动更新视图。
  • 初始加载时,读取 window.location.pathname 来决定渲染哪个组件。

极简实现

class HistoryRouter {
  constructor(routes) {
    this.routes = routes;
    this.currentComponent = null;
    // 监听浏览器前进/后退
    window.addEventListener('popstate', () => this.onPopState());
    this.onPopState(); // 首次加载
  }
  onPopState() {
    const path = window.location.pathname;
    const component = this.routes[path] || this.routes['*'];
    if (component) {
      document.getElementById('app').innerHTML = component();
    }
  }
  push(path) {
    history.pushState(null, '', path); // 改变 URL,不刷新页面
    this.onPopState(); // 手动触发渲染更新
  }
  replace(path) {
    history.replaceState(null, '', path);
    this.onPopState();
  }
}
  • pushState 第一参数:可以存放一个状态对象,通过 history.state 获取,用于在返回时恢复页面状态而不必重新加载数据。

关键制约:服务器配置
因为 history 模式下的 URL 看起来是真实的文件路径(如 /user/profile),当用户直接访问这个 URL 或刷新页面时,浏览器会向服务器请求这个路径。如果服务器没有对该路径的配置,通常会返回 404 错误。

必须的服务器配置示例:

  • Nginx
  location / {
    try_files $uri $uri/ /index.html;
  }
  

含义:先尝试找请求的文件 $uri,找不到找目录 $uri/,再找不到就返回 index.html,由前端路由接管。

  • Node.js (Express)
  const history = require('connect-history-api-fallback');
  app.use(history());
  
  • Vite 开发服务器内置了 history fallback,所以本地开发时刷新不会有 404 问题。

如果不进行该配置,刷新页面就会看到白屏和 404 错误。这就是 history 模式部署时最常见的坑。

hash 与 history 对比

| 特性 | hash 模式 | history 模式 |
|------|-----------|-------------|
| URL 外观 | 带有 #,如 /#/about | 干净,如 /about |
| SEO | 较差,# 后内容通常不被搜索引擎收录(但部分引擎已改进) | 良好,与普通 URL 无异 |
| 兼容性 | 极好(IE8+) | 需 HTML5 支持(IE10+) |
| 服务器配置 | 无需特殊配置 | 必须配置后端 fallback |
| 实现原理 | hashchange 事件 | pushState + popstate 事件 |

Vue Router 中如何切换模式

在创建 router 实例时,通过 history 选项指定:

import { createRouter, createWebHashHistory, createWebHistory } from 'vue-router'

// hash 模式
const router = createRouter({
  history: createWebHashHistory(),
  routes: [...]
})

// history 模式
const router = createRouter({
  history: createWebHistory(),
  routes: [...]
})

Vue Router 在底层就是根据你选择的 history 类型,采取上面描述的最原生事件监听机制,并结合 Vue 的响应式系统,来动态切换 router-view 中渲染的组件。理解这一点之后,排查“为什么路由跳转没反应”、“刷新后 404”等问题就变得有据可循了。