[vue-router]01-路由基础:Hash与History模式

3 阅读14分钟

【Vue 路由系列 01】Hash 与 History 模式:前端路由的技术实现

"你说的 history.back 事件根本不存在。" —— 这句话在模拟面试中击中了最痛的点。

上一篇(00 序章) 已经建立了完整的底座:浏览器的五种导航场景、历史栈记录类型之分(真实导航 vs pushState)、以及浏览器给 JS 留的两个"把手"。如果你还没读过,强烈建议先花 15 分钟读完那篇 —— 本篇的所有内容都建立在那套概念之上。

这篇进入 How 层(技术实现层):基于 00 篇的底座,具体讲 Vue Router 的 Hash 模式和 History 模式分别用了什么 API、写了什么代码来实现前端路由。同时包含 pushState 不触发 popstate 这个经典陷阱、scrollBehavior 的原理、以及服务端 fallback 配置等面试高频考点。


一、前端路由的"偷天换日":两种模式的本质区别

回顾 00 篇的结论:前端路由的核心任务只有两件事——拦截点击链接(preventDefault) + 在后退/前进时事后感知(hashchange / popstate)。但 Hash 和 History 两种模式完成这两件事的方式完全不同。这一章是连接"概念"与"API"的桥梁。

1.1 前端路由要解决的核心问题

回顾 00 篇的分析,前端路由需要在五种场景下让 SPA 正常工作:

场景前端路由的目标解决思路
① 点击链接不刷新页面,切换组件preventDefault + JS 改 URL + JS 渲染组件
②③ 后退/前进组件跟着切回去监听事件 → 读 URL → 重新匹配 → 重新渲染
④ 输入 URL别 404Hash 天然免疫 / History 需要服务端 fallback
⑤ 刷新别 404同上

场景①②③ 是前端代码自己能解决的,场景④⑤ 必须依赖外部配合。 这就是 Hash 和 History 两种模式的设计出发点不同。

1.2 Hash 模式的"偷天换日"

Hash 模式利用了一个浏览器特性:# 后面的内容是客户端专属的,永远不会发给服务器

URL: example.com/#/about#号前面的部分(example.com/) → 服务器收到的
         #号后面的部分(#/about)        → 只有 JS 能读到,服务器永远看不到

基于这个特性,Hash 模式在每个场景下的做法:

场景①(点击链接):
  <a href="#/about">
    ↓
  preventDefault(可选,其实不拦也行,因为 # 链接本来就不触发整页导航)
    ↓
  设置 location.hash = '#/about'
    ↓
  浏览器自动:推栈 + 变URL + 触发 hashchange(不发请求!)
    ↓
  Vue Router 监听 hashchange → 解析 hash → 匹配路由 → 渲染组件 ✓

场景②③(后退/前进):
  用户点后退
    ↓
  浏览器弹栈 + 恢复 URL(含 hash 部分)
    ↓
  hash 变了 → 触发 hashchange(不发请求!)
    ↓
  Vue Router 收到回调 → 同上 ✓

场景④⑤(输入URL / 刷新):
  用户输入 example.com/#/about 并回车
    ↓
  浏览器只请求 example.com/
    ↓
  服务器返回 index.html ✓(因为这个文件真实存在)
    ↓
  SPA 加载完毕 → JS 读取 location.hash → 匹配路由 → 渲染 ✓

Hash 模式的特点:五种场景全部自洽,不需要任何服务端配合。 代价是 URL 丑陋(带 #)且 SEO 不友好。

1.3 History 模式的"偷天换日"

History 模式利用了 HTML5 的 pushState API:可以改变 URL 和历史栈,但不触发页面刷新

history.pushState(state, title, url)
                    ↑
              这个 API 让浏览器:
              - 往历史栈推一条记录 ✓
              - 改变 URL 栏 ✓
              - 但!不刷新页面!不发请求!不触发事件!

基于这个 API,History 模式在每个场景下的做法:

场景①(点击链接):
  <a href="/about">
    ↓
  必须 preventDefault!(不拦的话浏览器真的会去请求 /about)
    ↓
  手动调用 history.pushState({}, '', '/about')
    ↓
  浏览器:推栈 + 变 URL(不刷新、不发请求、不触发事件)
    ↓
  pushState 不触发任何事件!
  所以 Vue Router 必须手动调用 onChange() → 匹配路由 → 渲染 ✓

场景②③(后退/前进):
  用户点后退
    ↓
  浏览器弹栈 + 恢复 URL(照做,无法阻止!)
    ↓
  目标是 pushState 记录 → 触发 popstate 事件(不是请求服务器,而是发通知)
    ↓
  (参见 00 篇 2.2 节:浏览器检查记录类型,
     pushState 记录走 popstate 分支,不放权给 JS 处理)
    ↓
  Vue Router 监听 popstate → 读取 URL → 匹配路由 → 渲染 ✓

场景④⑤(输入URL / 刷新):
  用户输入 example.com/about 并回车
    ↓
  浏览器向服务器发送 GET /about 请求!!!
    ↓
  如果服务端没配置 fallback → 404 💀
    ↓
  如果服务端配置了 try_files → 返回 index.html ✓
    ↓
  SPA 加载 → JS 读取 URL pathname → 匹配路由 → 渲染 ✓

History 模式的特点:URL 干净美观,但场景④⑤ 存在 404 风险,必须服务端配合兜底。

1.4 两种模式的本质区别(一句话版)

聊了这么多,归根结底就是一个问题:

当用户进行某种操作时,浏览器会不会向服务器发送一个新的 HTTP 请求?

Hash 模式History 模式
点击链接后❌ 不发(# 变化不算请求)❌ 不发(pushState 不触发请求)
后退/前进后❌ 不发(同上)❌ 不发(popstate 只是通知,目标为 pushState 记录时)
输入 URL 后❌ 不发(只请求 # 前的部分)✅ 会发(完整的 URL 路径)
F5 刷新后❌ 不发(同上)✅ 会发(同上)

这就是两种模式所有差异的根本来源。 所有 API 设计、服务端配置要求、兼容性问题,都是从这里派生出去的。


二、Hash 模式详解

2.1 什么是 Hash

URL 中 # 后面的部分叫做 hash(也叫 fragment):

https://example.com/users#/profile/123
                        ↑↑↑↑↑↑↑↑↑↑↑
                        这部分就是 hash

hash 有几个重要特性:

// 1. hash 的变化不会触发浏览器向服务器请求页面
//    无论 # 后面怎么变,浏览器只会请求 https://example.com/users

// 2. hash 改变会在浏览器历史中添加记录(可以前进/后退)
location.hash = '/home'      // 历史栈新增一条
location.hash = '/about'     // 又新增一条
history.back()               // 回到 /home ✓

// 3. 可以通过 JS 监听 hash 的变化
window.addEventListener('hashchange', (e) => {
  console.log('hash 变成了:', location.hash)
  console.log('旧 hash:', e.oldURL)
  console.log('新 hash:', e.newURL)
})

2.2 Hash 模式的工作流程

对照 00 篇的底座图看,Hash 模式做的事情很清楚:利用 # 不参与 HTTP 请求的特性 + hashchange 事件驱动

路径一:用户点击 <router-link>(主动导航)

用户点击 <router-link to="/about">
    ↓
Vue Router 阻止 <a> 的默认行为(preventDefault)  ← 对应 00"把手A"
    ↓
执行: location.hash = '#/about'                   ← 只改 # 后面部分
    ↓
浏览器行为:
  ├─ URL 变成 example.com/#/about
  ├─ 历史栈新增一条记录 ✓
  ├─ 但因为只是 hash 变化,浏览器不发 HTTP 请求!  ← 这是 Hash 的核心特性
  └─ 触发 hashchange 事件                          ← 对应 00"把手B"
    ↓
Vue Router 的 hashchange 监听器收到回调
    ↓
解析 location.hash → 去掉 '#' → 得到 '/about'
    ↓
匹配路由 → 渲染 About 组件

路径二:用户点浏览器后退按钮(被动导航)

用户点击浏览器 ◀ 后退按钮
    ↓
浏览器自己执行后退操作:
  ├─ 历史栈弹出一条记录 ✓
  ├─ URL 恢复为上一个(包括 # 后面部分)
  ├─ hash 发生了变化
  └─ 触发 hashchange 事件                           ← 同样是"把手B"
    ↓
Vue Router 收到回调 → 同上,解析 hash → 匹配 → 渲染

注意和 History 模式的关键差异:Hash 模式下,不管是点击还是后退,都会触发事件hashchange)。因为 location.hash = 'xxx' 本身就会触发 hashchange。所以 Hash 模式不需要像 History 那样在 push 之后手动调 onChange——事件本身就会驱动流程。

这也是为什么 Hash 模式的实现比 History 简单:只需要监听一个事件就够了,不用分主动/被动两条路处理。

2.3 Hash 模式的优缺点

优点缺点
兼容性极好,IE8 都支持URL 丑陋,带 #
无需服务端配置SEO 不友好(搜索引擎通常忽略 hash 内容)
部署简单,随便丢个静态服务器就能跑hash 不会被传送到服务端(#后的内容不会出现在 HTTP 请求里)

2.4 一个最小化的 Hash 路由实现

理解原理的最好方式是自己写一个:

class HashRouter {
  constructor() {
    this.routes = {}       // 存储路由映射: { '/home': HomeComponent, ... }
    this.currentUrl = ''   // 当前路径
    
    // 监听 hashchange 事件
    window.addEventListener('hashchange', () => this.refresh())
    
    // 初始化时也执行一次(处理首次访问带 hash 的情况)
    window.addEventListener('DOMContentLoaded', () => this.refresh())
  }
  
  // 注册路由
  route(path, handler) {
    this.routes[path] = handler
  }
  
  // 核心方法:根据当前 hash 渲染对应组件
  refresh() {
    // location.hash 的格式是 "#/about",需要去掉 "#"
    this.currentUrl = location.hash.slice(1) || '/'
    
    const handler = this.routes[this.currentUrl]
    if (handler) {
      handler()
    } else {
      console.log(`404: 未找到路由 ${this.currentUrl}`)
    }
  }
}

// 使用示例
const router = new HashRouter()

router.route('/', () => {
  document.getElementById('app').innerHTML = '<h1>首页</h1>'
})

router.route('/about', () => {
  document.getElementById('app').innerHTML = '<h1>关于我们</h1>'
})

// 用户点击时只需要改变 hash:
// <a href="#/">首页</a>
// <a href="#/about">关于</a>

这就是 Vue Router Hash 模式的本质——在浏览器原生导航的底座上,利用 hashchange 事件 + # 不发请求的特性,实现了一套完整的前端路由。生产级的 Vue Router 只是在这个基础上加了动态路由匹配、嵌套路由、导航守卫等高级特性,核心原理不变。


三、History 模式:更优雅但需要配合

有了 00 篇的概念基础,History 模式的原理就一句话能说清:pushState 改 URL(不刷新),用 popstate 监听后退(事后感知)。 难点在于理解它和 Hash 模式在"浏览器发不发请求"上的本质差异——这在第一章已经讲清楚了。

3.1 HTML5 History API

HTML5 提供了一组新的 API,让 JavaScript 可以在不刷新页面的情况下操作浏览器历史记录

// 核心方法:往历史栈中推入一条新记录
// 参数1: 状态对象(可以在 popstate 事件中取回)
// 参数2: 标题(目前浏览器都忽略这个参数)
// 参数3: 新的 URL(必须同源)
history.pushState({ page: 1 }, '', '/users')

// 替换当前历史记录(不会增加历史条数)
history.replaceState({ page: 2 }, '', '/settings')

// 前进/后退
history.forward()
history.back()
history.go(-2)  // 后退两步

注意 pushState 和直接修改 location.href 的区别

操作是否刷新页面是否发请求历史栈变化
location.href = '/about'✅ 刷新✅ 发请求导致导航,新页面有自己的历史栈
<a href="/about">✅ 刷新✅ 发请求+1
history.pushState('', '', '/about')❌ 不刷新❌ 不发请求+1

这就是 History 模式能实现 SPA 的基础:pushState 改变了 URL 但不刷新页面。

3.2 popstate 事件 —— ⚠️ 面试高频考点

这是我在面试中翻车的点,现在彻底搞清楚。

当用户点击浏览器的前进 / 后退按钮,或者调用 history.back() / history.forward() / history.go() 时,会触发 popstate 事件。

window.addEventListener('popstate', (event) => {
  console.log('状态变化:', event.state)
  // event.state 就是 pushState 时存入的第一个参数(状态对象)
})

// 示例流程
history.pushState({ page: 'home' }, '', '/')    // 不触发 popstate!
history.pushState({ page: 'about' }, '', '/about')  // 不触发 popstate!

// 此时点击浏览器的后退按钮
// 触发 popstate 事件,event.state = { page: 'home' }

// 或者手动调用
history.back()  // 同样触发 popstate,event.state = { page: 'home' }

3.3 ⚠️ 极其重要的细节:pushState/replaceState 不会触发 popstate

这是最容易混淆的地方(我在面试中就在这里翻车):

// ❌ 常见误解:以为 pushState 会触发 popstate
history.pushState({ id: 1 }, '', '/page1')
// popstate 不会被触发!
history.replaceState({ id: 2 }, '', '/page2')  
// 同样不触发 popstate!

// ✅ 实际情况:只有"历史记录切换操作"才触发 popstate
history.pushState({ id: 1 }, '', '/page1')
history.pushState({ id: 2 }, '', '/page2')
history.back()  // ← 只有这里才触发 popstate

为什么?因为 pushState/replaceState主动推送,你已经知道发生了什么;而 popstate被动响应——用户点了后退按钮或者代码调用了 history.back(),你需要在这个回调里做出反应。

💡 顺便一提pushState 不仅不触发 popstate,它也不会触发 hashchange(即使你改的是 hash)。这在用 pushState 实现 Hash 路由时要特别注意。

3.4 我面试时的错误 vs 正确理解

❌ 我当时说的是

"触发的是 history.back 事件,vue-router 内部监听这个事件"

错误分析

  1. history.back() 是一个方法/API,不是事件
  2. 它触发的真正的事件popstate
  3. Vue Router 内部确实监听了事件,但它监听的是 popstate,不是什么 "back 事件"

✅ 正确的说法

"History 模式下,Vue Router 通过 history.pushState 来改变 URL 不刷新页面,同时通过监听 popstate 事件来响应用户的前进/后退操作。"

3.5 History 模式的工作流程

对照 00 篇和第一章的概念图看,每一步都能对应上。

路径一:用户点击 <router-link>(主动导航)

用户点击 <router-link to="/about">
    ↓
Vue Router 阻止 <a> 的默认行为(preventDefault)  ← 对应 00 篇"把手A:拦截"
    ↓
执行 history.pushState({}, '', '/about')          ← 对应 00 篇"①推栈 + ②变URL"
    ↓
URL 变成 example.com/about(页面不刷新!)         ← 浏览器不会发请求
    ↓
pushState 不触发任何事件!
    ↓
Vue Router 手动调用 onChange()                     ← 自己触发路由匹配
    ↓
匹配 /about → 渲染 About 组件                      ← 对应 00 篇"④渲染(组件级)"

路径二:用户点浏览器后退按钮(被动导航)

用户点击浏览器 ◀ 后退按钮
    ↓
浏览器自己执行后退操作(Vue Router 管不了!)     ← 00 篇的"路径二"
  ├─ ① 历史栈弹出一条记录 ✓
  ├─ ② URL 变回前一个地址 ✓
  └─ ③ 浏览器检查目标记录类型(参见 002.2 节)
       目标是 pushState 记录
       → 不可能走 HTTP 加载流程
       → 改为触发 popstate 事件作为通知           ← 对应 00"把手B:事件通知"
    ↓
Vue Router 的 popstate 监听器收到回调
    ↓
从 location.pathname 读取当前 URL(或从 event.state 取)
    ↓
匹配路由 → 渲染对应组件                          ← 接管了页面展示

对比两张路径图,核心差异就一个点

点击链接(主动)点后退(被动)
谁发起的JS 代码 (router.push)用户/浏览器
preventDefault✅ 拦截了 <a> 默认行为不适用(不是点击)
pushState✅ 手动调用❌ 不调用(浏览器自己弹栈)
触发什么事件?不触发任何事件触发 popstate
谁触发 onChangepush 方法里手动调的popstate 回调里调的
最终结果路由匹配 + 组件渲染路由匹配 + 组件渲染

两条路最终汇入同一个 onChange——这就是 Vue Router 能统一处理两种情况的原因。


⚠️ 重要辨析:SPA 内回退 vs 跨页回退

上面路径二描述的只是一种情况——在 SPA 内部、后退目标是 pushState 记录时。但实际开发中,用户的后退行为可能跨越 SPA 边界。两种情况的差异极大,必须分清(完整分析参见 00 篇 2.2 节):

情况 A:SPA 内部回退(pushState → pushState)
从 /about 后退到 /home:
  ├─ 目标是 pushState 记录
  ├─ 不发 HTTP 请求
  ├─ 触发 popstate → Vue Router 接管
  └─ 结果:无网络请求、无白屏、组件级切换 ✓
情况 B:从 SPA 回退到入口页(pushState → 真实导航)
从 /home 后退到 /(首次加载的入口):
  ├─ 目标是真实导航记录!(浏览器自己推的)
  ├─ 浏览器发起 GET / 请求 → 整页重新加载
  └─ 结果:有白屏闪烁、SPA 状态全部丢失 💥
一张表总结
SPA 内回退退到 SPA 入口跨站回退
目标记录类型pushState真实导航真实导航(可能不同源)
是否发 HTTP 请求?❌ 不发✅ 发✅ 发
是否触发 popstate?✅ 触发❌ 不触发❌ 不触发
Vue Router 能接管吗?✅ 能❌ 不能❌ 不能
页面是否刷新?❌ 不刷新✅ 整页加载✅ 整页跳转

面试高频追问:"History 模式下点后退按钮会向服务器发请求吗?"
正确回答看后退的目标是什么记录。 如果目标是 pushState 推入的同源记录(即 SPA 内部跳转),则不发请求,只触发 popstate 由前端路由接管;如果目标是真实导航记录(如 SPA 的入口页)或者跨站记录,则会发起完整的 HTTP 请求导致整页加载。不能简单地说"会"或"不会"。


四、两种模式的全面对比

维度Hash 模式History 模式
URL 格式example.com/#/aboutexample.com/about
底层 APIhashchange 事件pushState + popstate 事件
是否需要服务端配置❌ 不需要必须配置
兼容性IE8+IE10+(现代浏览器没问题)
SEO差(搜索引擎忽略 # 后内容)好(正常 URL,可被爬取)
服务器 404 问题不会出现⚠️ 用户直接访问 /about 会返回 404
第三方应用接入方便(hash 不影响后端路由)可能冲突(需要后端配合排除)

History 模式的 404 问题与解决方案

这是 History 模式最大的坑:

场景:用户在地址栏输入 example.com/about 然后按回车
    ↓
浏览器向服务器发送 GET /about 请求
    ↓
服务器:我根本没有 /about 这个文件啊!→ 返回 404
    ↓
用户看到 404 页面 💀

但在 SPA 内部点击链接跳转到 /about 是正常的,因为那是 pushState 操作,不会请求服务器。

解决方案:让服务端对所有路径返回同一个 index.html,然后由前端路由接管:

# Nginx 配置
server {
    listen 80;
    root /usr/share/nginx/html;
    
    location / {
        try_files $uri $uri/ /index.html;  # 所有找不到的路径都回退到 index.html
    }
}
// Node.js Express 配置
app.get('*', (req, res) => {
  res.sendFile(path.join(__dirname, 'dist/index.html'))
})
# Apache 配置(.htaccess)
<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /
  RewriteRule ^index\.html$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . /index.html [L]
</IfModule>

五、第三种模式:Abstract(内存模式)

前面讲的是浏览器环境下的两种模式。但 Vue Router 其实还支持第三种模式——Abstract 模式(也叫内存模式):

import { createRouter, createMemoryHistory } from 'vue-router'

const router = createRouter({
  history: createMemoryHistory(),  // 不依赖浏览器 API
  routes: [...]
})

5.1 它是什么

Abstract 模式不操作浏览器的 URL 和历史记录,而是在内存中维护一个虚拟的路由状态。它不需要 hashchangepushStatepopstate 这些浏览器 API,完全靠 JavaScript 自己管理"当前路由是谁"。

5.2 适用场景

场景为什么用 Abstract
SSR(服务端渲染)Node.js 环境没有 windowhistory 对象,只能用内存模式
单元测试测试时不需要真实浏览器环境,用内存模式模拟路由跳转
Electron / React Native非浏览器环境,没有原生 History API
小程序小程序有自己的路由体系,Vue Router 只能以内存模式运行

5.3 三种模式对比

模式创建函数依赖适用环境
HashcreateWebHashHistory()hashchange 事件浏览器(兼容性最好)
HistorycreateWebHistory()pushState + popstate浏览器(需服务端配合)
AbstractcreateMemoryHistory()非浏览器环境(SSR、测试)

面试中如果被问到"Vue Router 有几种模式",回答三种比回答两种更能体现你的知识完整性。


六、Vue Router 中的模式选择

// Vue Router 4 的创建方式
const router = createRouter({
  // 选择模式
  history: createWebHistory(),        // History 模式,URL: /about
  // history: createWebHashHistory(),  // Hash 模式,URL: #/about
  
  routes: [
    { path: '/', component: Home },
    { path: '/about', component: About },
  ]
})

createWebHistory 内部做了什么?

Vue Router 4 的 createWebHistory() 底层会监听浏览器的 popstate 事件。它的简化伪代码:

📌 官方参考:Vue Router 的 RouterHistory 接口定义了一个 listen 方法,用于"当从外部触发导航(如浏览器后退和前进按钮)时将监听器附加到 History 实现"。对应到浏览器环境,这个外部导航就是通过 popstate 事件触发。

function createWebHistory(base = '') {
  // 内部维护的 state
  let currentUrl = base || '/'
  
  // 设置初始状态
  // 使用 replaceState 确保首次加载时不产生额外的历史记录
  history.replaceState({ url: currentUrl }, '', currentUrl)
  
  // 监听 popstate 事件(响应用户前进/后退)
  window.addEventListener('popstate', (e) => {
    const targetUrl = location.pathname
    // 通知 Vue Router 更新当前路由并重新匹配
    onChange(targetUrl)
  })
  
  // 提供 push 方法(用于编程式导航)
  function push(to) {
    history.pushState({ url: to }, '', to)
    // pushState 不触发事件!所以这里需要手动触发路由变更
    onChange(to)
  }
  
  // 提供 replace 方法
  function replace(to) {
    history.replaceState({ url: to }, '', to)
    onChange(to)
  }
  
  return { push, replace }
}

重点看这段伪代码的逻辑

  1. 初始化时replaceState 设定起始状态(不产生额外历史记录)
  2. 编程式导航router.push)内部调用 pushState,然后手动触发 onChange
  3. 浏览器后退/前进触发 popstate,在回调里调用 onChange
  4. 不管哪种方式,最终都是走到 onChange 进行路由匹配和组件渲染

这就是为什么 Vue Router 能同时处理主动导航被动后退两种情况。


七、scrollBehavior 与 savedPosition

这是我在面试中完全没答上来的点,但它其实很简单:

const router = createRouter({
  history: createWebHistory(),
  routes: [...],
  
  scrollBehavior(to, from, savedPosition) {
    // savedPosition 只在通过浏览器前进/后退(popstate)时可用
    // 它的值是 { top: number, left: number } 或 null
    
    if (savedPosition) {
      // 用户点了后退按钮 → 恢复之前的滚动位置
      return savedPosition
    } else if (to.hash) {
      // 目标路由带有 hash 锚点(如 /about#section2)
      return {
        el: to.hash,
        behavior: 'smooth'
      }
    } else {
      // 其他情况滚到顶部
      return { top: 0 }
    }
  }
})

savedPosition 从哪来?

它是浏览器自动维护的。当你在一个页面上滚动了 200px,然后导航走再通过后退回来时,浏览器会把 { top: 200, left: 0 } 传给 popstate 事件的关联数据中。Vue Router 把它提取出来传给你的 scrollBehavior 函数。

所以正确的回答是

Vue Router 通过 scrollBehavior 配置项和浏览器原生的 history.state(保存滚动位置),实现了后退时滚动位置的自动还原。不需要自己写 localStorage 记录滚动位置,也不需要在 beforeRouteLeave 里手动保存。


八、面试高频问题速查

Q1:Hash 模式和 History 模式的区别?

标准回答模板

Hash 模式基于 hashchange 事件,URL 带 # 号,兼容性好无需服务端配置;History 模式基于 HTML5 的 pushState + popstate,URL 更干净,但需要服务端做 fallback 配置,否则用户直接访问或 F5 刷新时会 404。选择的话,如果对 SEO 有要求或者追求更好的 URL 美观度就用 History,否则 Hash 开箱即用更省事。

更深层的理解(参见 00 篇):两者的根本差异在于浏览器在不同操作下会不会发起 HTTP 请求。Hash 模式因为 # 不参与请求,所以所有场景都安全;History 模式依赖 pushState 创建的"特殊标记记录"来实现 SPA 内回退不刷新,但跨出 SPA 边界后仍会走传统的 HTTP 加载路线。

Q2:pushStatepopstate 是什么关系?

pushState主动操作 API,用于改变 URL 和历史记录但不刷新页面;popstate被动响应事件,在用户点击前进/后退按钮或调用 history.back() 时触发。pushState 本身不会触发 popstate。Vue Router 在 History 模式下,push 方法内部调完 pushState 后会手动触发路由匹配,而浏览器后退时通过 popstate 回调来做同样的事。

Q3:用户在 History 模式下 F5 刷新为什么会 404?

因为 F5 刷新时浏览器会以当前 URL 向服务器发起真正的 HTTP GET 请求。History 模式的 URL(如 /about)在服务器上并不对应真实文件,所以返回 404。解决方法是让服务端将所有路径的请求 fallback 到 index.html,再由前端路由接管。常见配置包括 Nginx 的 try_files、Node.js Express 的通配路由、Apache 的 mod_rewrite 等。

Q4:history.back() 是事件吗?

不是。history.back() 是浏览器提供的方法(API),调用后会触发 popstate 事件。我在这里曾经犯过一个错误,把方法和事件混淆了。正确的关系是:history.back() / history.forward() / 浏览器后退按钮 → 触发 → popstate 事件。

Q5:History 模式下点后退按钮会向服务器发请求吗?

不能简单回答"会"或"不会"。 关键看后退的目标记录类型(详见 00 篇 2.2 节)。如果目标是 pushState 推入的同源记录(SPA 内部跳转),则不发请求,只触发 popstate 由前端路由接管;如果目标是真实导航记录(如 SPA 入口页)或跨站记录,则会发起完整的 HTTP 请求导致整页加载。


九、本篇小结

概念层回顾(来自 00 篇)

概念一句话记忆详细解释
五种导航场景点击 / 后退 / 前进 / 输入URL / F500 篇 2.1-2.3
核心分水岭只有点击链接能被 JS 阻止preventDefault 是唯一手段
历史栈记录类型真实导航 vs pushState决定后退时走哪条分支
Hash 天然安全# 不参与 HTTP 请求五种场景全部安全
History 有风险输入URL/F5 会请求服务器必须配服务端 fallback

技术层(本篇核心)

API/概念一句话记忆
Hash 模式hashchange 事件驱动,URL 带 #,兼容性最好,只需一个事件监听
History 模式pushState 改 URL + popstate 监听后退,URL 干净需服务端配合,需分主动/被动两条路
pushState vs popstate前者主动推(API),后者被动响应(Event);前者触发后者
history.back()方法不是事件,它触发 popstate 事件
scrollBehaviorsavedPosition 只在浏览器前进/后退时可用,自动包含滚动位置
Abstract 模式不依赖浏览器 API,适用于 SSR / 测试 / 非浏览器环境
SPA内回退 vs 跨页回退目标为 pushState 记录则 Vue Router 接管;目标为真实导航记录则整页加载

🔑 最终口诀点击可拦,后退不可拦;Hash 不请求,History 要配合。

下一篇预告:搞清楚了路由的"物理层"(URL 怎么变 + 浏览器怎么判断),下一篇我们进入 Vue Router 的"交通管制系统"——路由守卫。五种守卫的执行顺序是什么?next() 怎么用才不会死循环?导航过程中还能做数据预取吗?这些都是我在面试中踩过的坑。

👉 Vue 路由系列 02:路由守卫与死循环陷阱 —— 守卫执行全流程 + 数据预取模式 + Navigation Failure 判别


参考来源:Gemini 3.1 Pro 模拟面试记录(路由与单页应用章节)、MDN History API 文档、Vue Router 4 官方文档