前言
在 Vue 单页应用(SPA)中,页面跳转是一个看似简单实则暗藏玄机的话题。this.$router.push('/login') 和 window.location.href = '/#/login' 表面上都能实现"跳转到登录页"的效果,但其背后的实现机制、对应用状态的影响、以及在不同场景下的行为差异,直接关系到应用的稳定性、用户体验和可维护性。
本文将从底层原理、架构设计、源码实现等多个维度进行深度剖析。
一、架构层面的根本差异
1.1 整体架构对比图
┌─────────────────────────────────────────────────────────────────────┐
│ Vue 应用架构层面 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ this.$router.push('/login') │ │
│ │ │ │
│ │ Vue Component ──► Vue Router ──► Route Matcher │ │
│ │ │ │ │ │ │
│ │ │ ▼ ▼ │ │
│ │ │ Navigation Guards Component │ │
│ │ │ │ Lifecycle │ │
│ │ │ ▼ │ │ │
│ │ │ [权限校验] ▼ │ │
│ │ │ [数据预取] DOM 更新 │ │
│ │ │ [过渡动画] (无刷新) │ │
│ │ ▼ │ │
│ │ 保持应用状态、Vuex store、事件监听器等 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ ══════════════════════════════════════════════════════════════ │
│ 分 割 线 │
│ ══════════════════════════════════════════════════════════════ │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ window.location.href = '/#/login' │ │
│ │ │ │
│ │ Vue App ──✕──► Browser Navigation ──► Page Reload? │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ 直接修改 URL 可能触发: │ │
│ │ 绕过 Vue Router - 完整页面刷新 │ │
│ │ - 重新下载资源 │ │
│ │ - 丢失应用状态 │ │
│ │ - 重启 Vue 实例 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
1.2 核心差异总结
| 维度 | $router.push() |
location.href |
|---|---|---|
| 控制权 | Vue Router 完全控制 | 浏览器原生控制 |
| 应用状态 | 保持完整 | 可能丢失 |
| 代码执行 | 在 Vue 上下文中 | 脱离 Vue 上下文 |
| 可预测性 | 高(遵循路由配置) | 低(依赖浏览器行为) |
二、Vue Router 的底层实现原理
2.1 Vue Router 架构图
┌────────────────────────────────────────────────────────────────────────┐
│ Vue Router 内部架构 │
├────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Router │ │ Route │ │ Matcher │ │
│ │ Instance │◄────►│ Matcher │◄────►│ (路由表) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ │ ▼ ▼ │
│ │ ┌──────────────────┐ ┌─────────────────┐ │
│ │ │ Navigation │ │ Route Record │ │
│ │ │ Guards │ │ { path, comp, │ │
│ │ │ (beforeEach等) │ │ children... } │ │
│ │ └──────────────────┘ └─────────────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ History Mode │ │
│ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ Hash Mode │ │ History Mode │ │ │
│ │ │ (hashchange) │ │ (pushState) │ │ │
│ │ └──────────────┘ └──────────────┘ │ │
│ └─────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ View Updating │ │
│ │ (<router-view>) │ │
│ └──────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────────┘
2.2 $router.push() 的完整执行流程
┌─────────────────────────────────────────────────────────────────────┐
│ $router.push() 完整执行流程 │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ 1. 参数标准化 │
│ push('/login') 或 │
│ push({ path: '/login', query: {} }) │
└──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ 2. 路由匹配 │
│ - 根据配置的 routes 数组匹配 │
│ - 解析动态路由参数 │
│ - 处理嵌套路由 │
└──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ 3. 触发导航守卫 │
│ ┌────────────────────────────────────┐ │
│ │ 3.1 失活组件的 beforeRouteLeave │ │
│ └────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────┐ │
│ │ 3.2 全局 beforeEach │ │
│ │ (权限校验、登录拦截等) │ │
│ └────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────┐ │
│ │ 3.3 重用组件的 beforeRouteUpdate │ │
│ └────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────┐ │
│ │ 3.4 路由配置的 beforeEnter │ │
│ └────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────┐ │
│ │ 3.5 激活组件的 beforeRouteEnter │ │
│ └────────────────────────────────────┘ │
└──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ 4. 导航确认 │
│ - 所有守卫都调用 next() 才会继续 │
│ - 任一守卫调用 next(false) 则取消 │
│ - 可重定向到其他路由 │
└──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ 5. 更新 URL (根据路由模式) │
│ ┌────────────────────────────────────┐ │
│ │ Hash Mode: │ │
│ │ window.location.hash = '#/login'│ │
│ │ (触发 hashchange 事件) │ │
│ └────────────────────────────────────┘ │
│ ┌────────────────────────────────────┐ │
│ │ History Mode: │ │
│ │ history.pushState({}, '', url) │ │
│ │ (不触发页面刷新) │ │
│ └────────────────────────────────────┘ │
└──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ 6. 更新应用状态 │
│ - 更新 this.$route 响应式对象 │
│ - 触发 <router-view> 重新渲染 │
│ - 执行组件生命周期钩子 │
└──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ 7. 执行 afterEach 钩子 │
│ (可用于分析、进度条结束等) │
└──────────────────────────────────────────┘
│
▼
┌──────────────┐
│ 导航完成 │
└──────────────┘
2.3 源码级别的关键实现
// Vue Router 简化版源码示意
class VueRouter {
push(location, onComplete, onAbort) {
// 1. 标准化 location 参数
location = normalizeLocation(location, this.currentRoute);
// 2. 匹配路由记录
const route = this.matcher.match(location);
// 3. 确认导航(执行导航守卫)
this.confirmTransition(route, () => {
// 4. 更新当前路由
this.updateRoute(route);
// 5. 更新 URL
this.ensureURL();
// 6. 触发回调
onComplete && onComplete(route);
}, onAbort);
}
confirmTransition(route, onComplete, onAbort) {
// 提取所有需要执行的导航守卫
const queue = [].concat(
// 失活组件的 beforeRouteLeave
extractLeaveGuards(this.currentRoute),
// 全局 beforeEach
this.router.beforeHooks,
// 重用组件的 beforeRouteUpdate
extractUpdateHooks(this.currentRoute),
// 路由配置的 beforeEnter
route.matched.flatMap(m => m.beforeEnter),
// 激活组件的 beforeRouteEnter
extractEnterGuards(route)
);
// 顺序执行守卫
runQueue(queue, (guard, next) => {
guard(route, this.currentRoute, (to) => {
if (to === false) {
// 取消导航
onAbort();
} else if (isRoute(to)) {
// 重定向
this.push(to);
} else {
// 继续下一个守卫
next();
}
});
}, onComplete);
}
}
三、Hash 模式 vs History 模式深度解析
3.1 Hash 模式原理
┌─────────────────────────────────────────────────────────────────────┐
│ Hash 模式工作原理 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ URL 结构: https://example.com/#/user/profile?id=123 │
│ └────────────────────┘ └──────────────────────┘ │
│ 基础 URL hash 部分 │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 特点: │ │
│ │ 1. hash 变化不会触发页面刷新 │ │
│ │ 2. hash 不属于 URL 路径,不会被发送到服务器 │ │
│ │ 3. 通过 hashchange 事件监听变化 │ │
│ │ 4. 兼容性好,无需服务器配置 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ 事件监听流程: │
│ ┌──────────┐ hash变化 ┌─────────────┐ 触发 ┌─────┐│
│ │ 浏览器 │ ─────────────► │ hashchange │ ──────────► │Vue ││
│ │ 地址栏 │ │ 事件 │ │Router││
│ └──────────┘ └─────────────┘ └─────┘│
│ │
│ Vue Router 初始化: │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ window.addEventListener('hashchange', () => { │ │
│ │ this.transitionTo(window.location.hash.slice(1)); │ │
│ │ }); │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
3.2 History 模式原理
┌─────────────────────────────────────────────────────────────────────┐
│ History 模式工作原理 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ URL 结构: https://example.com/user/profile?id=123 │
│ └────────────────────────┘└──────────────────┘ │
│ 完整路径都是真实 URL │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 核心API: │ │
│ │ - history.pushState(state, title, url) // 添加历史记录 │ │
│ │ - history.replaceState(state, title, url) // 替换当前记录 │ │
│ │ - popstate 事件 // 监听前进/后退 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 关键特点: │ │
│ │ 1. URL 更美观,没有 # 号 │ │
│ │ 2. 需要服务器配置支持(所有路径都返回 index.html) │ │
│ │ 3. pushState/replaceState 不会触发页面刷新 │ │
│ │ 4. popstate 事件仅在浏览器前进/后退时触发 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ 服务器配置示例: │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ server { │ │
│ │ location / { │ │
│ │ try_files $uri $uri/ /index.html; │ │
│ │ } │ │
│ │ } │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ ⚡ 致命陷阱: │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 如果在 History 模式下使用: │ │
│ │ window.location.href = '/#/login' │ │
│ │ │ │
│ │ 实际会跳转到: https://example.com/#/login │ │
│ │ 这与你的 History 模式路由 /login 完全不匹配! │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
3.3 两种模式的路由模式选择决策树
开始选择路由模式
│
▼
┌─────────────────┐
│ 是否需要美观URL │
│ (无 # 号)? │
└─────────────────┘
│
┌────────────┴────────────┐
│ │
▼ ▼
否 是
│ │
▼ ▼
┌──────────────┐ ┌─────────────────┐
│ Hash Mode │ │ 是否能控制 │
│ (简单省事) │ │ 服务器配置? │
└──────────────┘ └─────────────────┘
│
┌─────────────┴─────────────┐
│ │
▼ ▼
否 是
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ Hash Mode │ │ History Mode │
│ (无服务器权限时) │ │ (最佳体验) │
└──────────────────┘ └──────────────────┘
四、window.location.href 的执行机制
4.1 浏览器导航流程
┌─────────────────────────────────────────────────────────────────────┐
│ window.location.href = '/login' 执行流程 │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ 1. 浏览器解析新 URL │
│ - 解析协议、域名、端口、路径 │
└──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ 2. 判断是否需要刷新页面 │
│ ┌────────────────────────────────────┐ │
│ │ 需要刷新的情况: │ │
│ │ - 协议不同 │ │
│ │ - 域名/端口不同 │ │
│ │ - 完整路径变化(非 hash 部分) │ │
│ └────────────────────────────────────┘ │
│ ┌────────────────────────────────────┐ │
│ │ 不刷新的情况: │ │
│ │ - 仅 hash 部分变化 │ │
│ │ (如 /page → /page#section) │ │
│ └────────────────────────────────────┘ │
└──────────────────────────────────────────┘
│
┌───────────┴───────────┐
│ │
▼ ▼
需要刷新 仅 Hash 变化
│ │
▼ ▼
┌────────────────────────┐ ┌─────────────────────┐
│ 3a. 完整页面刷新 │ │ 3b. 仅滚动到锚点 │
│ ┌──────────────────┐ │ │ 或触发 hashchange │
│ │ - 卸载当前页面 │ │ │ │
│ │ - 清除所有状态 │ │ │ ⚡ Vue Router │
│ │ (Vuex、事件等) │ │ │ 无法感知此变化! │
│ │ - 发起新HTTP请求 │ │ │ │
│ │ - 重新加载资源 │ │ └─────────────────────┘
│ │ - 重新初始化 Vue │ │
│ └──────────────────┘ │
└────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ 4. 更新浏览器历史记录 │
│ (可通过后退按钮返回) │
└──────────────────────────────────────────┘
4.2 关键问题:绕过 Vue Router 的后果
// 场景:History 模式下执行
window.location.href = '/login'
// 问题分析:
┌────────────────────────────────────────────────────────────────┐
│ 浏览器行为: │
│ 1. 检测到完整路径变化(从 /home 到 /login) │
│ 2. 触发页面刷新 │
│ 3. 向服务器请求 /login 资源 │
│ │
│ 服务器响应: │
│ - 如果配置了 SPA fallback → 返回 index.html → Vue 重新启动 │
│ - 如果未配置 → 返回 404 │
│ │
│ Vue 应用状态: │
│ - Vuex store 被重置 │
│ - 所有组件实例销毁 │
│ - 事件监听器清除 │
│ - 定时器、WebSocket 等需要重新建立 │
└────────────────────────────────────────────────────────────────┘
五、导航守卫完整流程图
5.1 导航守卫执行顺序
┌─────────────────────────────────────────────────────────────────────┐
│ 导航守卫完整执行顺序 │
│ (从 /home 导航到 /user/profile) │
└─────────────────────────────────────────────────────────────────────┘
触发导航: this.$router.push('/user/profile')
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 【阶段一:离开当前路由】 │
│ │
│ 1. beforeRouteLeave (Home组件内) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 用途:防止用户未保存就离开、清理定时器等 │ │
│ │ 示例: │ │
│ │ beforeRouteLeave(to, from, next) { │ │
│ │ if (this.hasUnsavedChanges) { │ │
│ │ const confirm = window.confirm('确定离开?'); │ │
│ │ if (!confirm) return next(false); │ │
│ │ } │ │
│ │ next(); │ │
│ │ } │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 【阶段二:全局前置守卫】 │
│ │
│ 2. beforeEach (全局注册) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 用途:权限验证、登录检查、路由拦截 │ │
│ │ 示例: │ │
│ │ router.beforeEach((to, from, next) => { │ │
│ │ const isLoggedIn = store.state.isLoggedIn; │ │
│ │ if (to.meta.requiresAuth && !isLoggedIn) { │ │
│ │ next({ path: '/login', query: { redirect: to.fullPath } });│
│ │ } else { │ │
│ │ next(); │ │
│ │ } │ │
│ │ }); │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 3. beforeResolve (全局,在所有组件内守卫之后) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 用途:导航确认前的最后检查 │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 【阶段三:路由独享守卫】 │
│ │
│ 4. beforeEnter (路由配置中) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 用途:特定路由的权限控制、数据预加载 │ │
│ │ 示例: │ │
│ │ { │ │
│ │ path: '/admin', │ │
│ │ component: Admin, │ │
│ │ beforeEnter: (to, from, next) => { │ │
│ │ if (store.state.user.role !== 'admin') { │ │
│ │ next('/403'); │ │
│ │ } else { │ │
│ │ next(); │ │
│ │ } │ │
│ │ } │ │
│ │ } │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 【阶段四:组件内守卫】 │
│ │
│ 5. beforeRouteEnter (目标组件 User) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ ⚡ 注意:此时组件实例还未创建,无法访问 this │ │
│ │ 用途:获取路由数据,通过回调访问组件实例 │ │
│ │ 示例: │ │
│ │ beforeRouteEnter(to, from, next) { │ │
│ │ // ✘ 错误:this 未定义 │ │
│ │ // this.loadUser(); │ │
│ │ │ │
│ │ // ✔ 正确:通过回调访问实例 │ │
│ │ next(vm => { │ │
│ │ vm.loadUser(to.params.id); │ │
│ │ }); │ │
│ │ } │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 6. beforeRouteUpdate (当路由参数变化时复用组件) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 场景:/user/1 → /user/2 (同一个 User 组件) │ │
│ │ 示例: │ │
│ │ beforeRouteUpdate(to, from, next) { │ │
│ │ this.loadUser(to.params.id); │ │
│ │ next(); │ │
│ │ } │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 【阶段五:导航确认】 │
│ │
│ 所有守卫都调用 next() → 导航确认 │
│ 任一守卫调用 next(false) → 导航取消 │
│ 任一守卫调用 next('/path') → 重定向 │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 【阶段六:更新视图】 │
│ │
│ 1. 失活 Home 组件 → beforeDestroy → destroyed │
│ 2. 激活 User 组件 → beforeCreate → created → beforeMount → mounted│
│ 3. 更新 <router-view> │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 【阶段七:全局后置钩子】 │
│ │
│ afterEach (全局注册) │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 用途:进度条结束、页面标题设置、埋点统计 │ │
│ │ 示例: │ │
│ │ router.afterEach((to, from) => { │ │
│ │ NProgress.done(); │ │
│ │ document.title = to.meta.title || 'My App'; │ │
│ │ analytics.trackPageView(to.fullPath); │ │
│ │ }); │ │
│ └──────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
▼
导航完成
5.2 守卫对比:$router.push vs location.href
┌─────────────────────────────────────────────────────────────────────┐
│ 导航守卫触发对比 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ this.$router.push('/login') │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ ✔ beforeRouteLeave → 触发 │ │
│ │ ✔ beforeEach → 触发 │ │
│ │ ✔ beforeEnter → 触发 │ │
│ │ ✔ beforeRouteEnter → 触发 │ │
│ │ ✔ afterEach → 触发 │ │
│ │ ✔ 组件生命周期 → 正常执行 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ window.location.href = '/#/login' (Hash 模式) │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ ✘ beforeRouteLeave → 不触发 │ │
│ │ ✘ beforeEach → 不触发 │ │
│ │ ✘ beforeEnter → 不触发 │ │
│ │ ✘ beforeRouteEnter → 不触发 │ │
│ │ ✘ afterEach → 不触发 │ │
│ │ ⚡ 可能触发 hashchange → Vue Router 可能捕获到变化 │ │
│ │ ⚡ 组件状态 → 可能不一致 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ window.location.href = '/login' (History 模式) │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ ✘ 所有导航守卫 → 全部失效 │ │
│ │ 💥 页面完全刷新 → Vue 应用重启 │ │
│ │ 💥 应用状态丢失 → Vuex 重置 │ │
│ │ 💥 内存泄漏风险 → 未清理的事件监听器、定时器 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
六、实战场景深度对比
6.1 场景一:登录拦截
┌─────────────────────────────────────────────────────────────────────┐
│ 场景:未登录用户访问需要权限的页面 │
└─────────────────────────────────────────────────────────────────────┘
【正确做法】使用 $router.push
┌────────────────────────────────────────────────────────────────┐
│ // router/index.js │
│ router.beforeEach((to, from, next) => { │
│ const isLoggedIn = store.state.auth.isLoggedIn; │
│ │
│ if (to.meta.requiresAuth && !isLoggedIn) { │
│ // 保存目标路径,登录后重定向 │
│ next({ │
│ path: '/login', │
│ query: { redirect: to.fullPath } │
│ }); │
│ } else { │
│ next(); │
│ } │
│ }); │
│ │
│ // Login.vue - 登录成功后 │
│ handleLoginSuccess() { │
│ const redirect = this.$route.query.redirect || '/dashboard';│
│ this.$router.push(redirect); // ✔ 正确 │
│ } │
└────────────────────────────────────────────────────────────────┘
【错误做法】使用 location.href
┌────────────────────────────────────────────────────────────────┐
│ // ✘ 错误示例 │
│ if (!isLoggedIn) { │
│ window.location.href = '/#/login'; │
│ // 问题: │
│ // 1. 无法保存 redirect 参数 │
│ // 2. Vuex 状态可能丢失 │
│ // 3. 导航守卫失效,安全检查被绕过 │
│ } │
└────────────────────────────────────────────────────────────────┘
6.2 场景二:Token 过期处理
┌─────────────────────────────────────────────────────────────────────┐
│ 场景:API 请求返回 401,需要跳转登录页 │
└─────────────────────────────────────────────────────────────────────┘
【完整解决方案】
┌────────────────────────────────────────────────────────────────┐
│ // utils/request.js (axios 拦截器) │
│ import router from '@/router'; │
│ import store from '@/store'; │
│ │
│ service.interceptors.response.use( │
│ response => response.data, │
│ error => { │
│ if (error.response?.status === 401) { │
│ // 清除用户信息 │
│ store.dispatch('auth/logout'); │
│ │
│ // ✔ 正确:使用 router 进行跳转 │
│ router.push({ │
│ path: '/login', │
│ query: { │
│ redirect: router.currentRoute.fullPath, │
│ message: '登录已过期,请重新登录' │
│ } │
│ }); │
│ │
│ // ✘ 错误示例 │
│ // window.location.href = '/login'; │
│ // 会丢失当前的 redirect 信息 │
│ // 且 Vuex 状态已清空,但应用可能未正确重置 │
│ } │
│ return Promise.reject(error); │
│ } │
│ ); │
└────────────────────────────────────────────────────────────────┘
6.3 场景三:路由懒加载与代码分割
┌─────────────────────────────────────────────────────────────────────┐
│ 场景:路由懒加载的加载时机 │
└─────────────────────────────────────────────────────────────────────┘
【路由配置】
┌────────────────────────────────────────────────────────────────┐
│ const routes = [ │
│ { │
│ path: '/admin', │
│ component: () => import('@/views/Admin.vue'), // 懒加载 │
│ meta: { requiresAuth: true } │
│ } │
│ ]; │
└────────────────────────────────────────────────────────────────┘
【$router.push 的懒加载流程】
┌──────────────────────────────────────────┐
│ this.$router.push('/admin') │
└──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ 1. beforeEach 检查权限 │
│ ├─ 未登录 → 重定向到登录页 │
│ └─ 已登录 → 继续 │
└──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ 2. 开始加载 Admin.vue 的 chunk │
│ - 显示加载指示器 │
│ - 网络请求获取 JS 文件 │
└──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ 3. chunk 加载完成 │
│ - 执行 beforeEnter │
│ - 创建组件实例 │
│ - 渲染页面 │
└──────────────────────────────────────────┘
【location.href 的风险】
┌────────────────────────────────────────────────────────────────┐
│ window.location.href = '/admin' │
│ │
│ 问题: │
│ 1. 如果是 History 模式 → 页面刷新 │
│ 2. 向服务器请求 /admin 资源 │
│ 3. 服务器可能返回 404(SPA fallback 未配置) │
│ 4. 即使返回 index.html,也重新下载了所有资源 │
│ 5. 懒加载的优势被破坏 │
└────────────────────────────────────────────────────────────────┘
七、性能与用户体验对比
7.1 性能对比测试
┌─────────────────────────────────────────────────────────────────────┐
│ 性能对比(模拟数据) │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 测试场景:从首页跳转到用户中心(包含 3 个 API 请求) │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ this.$router.push('/user') │ │
│ ├─────────────────────────────────────────────────────────────┤ │
│ │ 导航开始到页面可交互: │ │
│ │ - 路由守卫执行: ~5ms │ │
│ │ - 组件实例化: ~10ms │ │
│ │ - DOM 更新: ~15ms │ │
│ │ - API 请求: ~200-500ms │ │
│ │ - 总计: ~230-530ms │ │
│ │ │ │
│ │ 优势: │ │
│ │ - 无白屏时间 │ │
│ │ - 可显示加载状态 │ │
│ │ - 保持滚动位置(可配置) │ │
│ │ - 资源无需重新下载 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ window.location.href = '/user' (History 模式) │ │
│ ├─────────────────────────────────────────────────────────────┤ │
│ │ 页面刷新流程: │ │
│ │ - 卸载旧页面: ~50ms │ │
│ │ - 网络请求 HTML: ~100-300ms │ │
│ │ - 解析 HTML: ~50ms │ │
│ │ - 下载 JS/CSS: ~200-500ms │ │
│ │ - Vue 初始化: ~50-100ms │ │
│ │ - 组件渲染: ~30-50ms │ │
│ │ - API 请求: ~200-500ms │ │
│ │ - 总计: ~680-1550ms │ │
│ │ │ │
│ │ 劣势: │ │
│ │ - 明显白屏时间 │ │
│ │ - 重复下载资源 │ │
│ │ - 丢失应用状态 │ │
│ │ - 滚动位置重置 │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ 性能差距:约 3-5 倍 │
│ │
└─────────────────────────────────────────────────────────────────────┘
7.2 用户体验对比
┌─────────────────────────────────────────────────────────────────────┐
│ 用户体验对比 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ this.$router.push() │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ ✔ 无白屏,过渡流畅 │ │
│ │ ✔ 可显示页面加载进度条 │ │
│ │ ✔ 支持页面切换动画 │ │
│ │ ✔ 保持全局状态(主题、语言等) │ │
│ │ ✔ 可实现页面缓存功能 │ │
│ │ ✔ 支持 prefetch 预加载 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ window.location.href │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ ✘ 明显的白屏闪烁 │ │
│ │ ✘ 浏览器原生加载指示器 │ │
│ │ ✘ 无过渡动画 │ │
│ │ ✘ 所有状态丢失,需重新初始化 │ │
│ │ ✘ 用户需要重新等待所有资源加载 │ │
│ │ ✘ 表单数据、未保存内容丢失 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
八、常见陷阱与最佳实践
8.1 常见陷阱
┌─────────────────────────────────────────────────────────────────────┐
│ 常见陷阱汇总 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 陷阱 1:在 History 模式下硬编码 hash 路径 │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ // ✘ 错误:History 模式下跳转到 hash 路径 │ │
│ window.location.href = '/#/login'; │ │
│ // 结果:URL 变成 https://example.com/#/login │ │
│ // 与路由配置 /login 不匹配,404! │ │
│ │ │
│ // ✔ 正确:使用路由名称或路径 │ │
│ this.$router.push('/login'); │ │
│ // 或 │ │
│ this.$router.push({ name: 'Login' }); │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ 陷阱 2:在异步回调中使用 location.href │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ // ✘ 错误示例 │ │
│ setTimeout(() => { │
│ window.location.href = '/dashboard'; │ │
│ }, 1000); │ │
│ // 问题:期间用户可能已经导航到其他页面,造成混乱 │ │
│ │ │
│ // ✔ 正确:使用路由导航 │ │
│ setTimeout(() => { │ │
│ this.$router.push('/dashboard'); │ │
│ }, 1000); │ │
│ // 或者更好的做法:在导航守卫中处理 │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ 陷阱 3:忽略路由守卫的权限检查 │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ // ✘ 危险:绕过权限检查 │ │
│ if (userClickedAdminLink) { │ │
│ window.location.href = '/admin'; │ │
│ } │ │
│ // 即使没有权限,也能访问 admin 页面(虽然 API 会拒绝) │ │
│ │ │
│ // ✔ 正确:通过路由跳转,触发权限检查 │ │
│ if (userClickedAdminLink) { │ │
│ this.$router.push('/admin'); │ │
│ // beforeEach 会检查权限并拒绝导航 │ │
│ } │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ 陷阱 4:在新窗口打开链接时误用路由 │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ // ✘ 错误:无法在新窗口打开 │ │
│ this.$router.push('/help'); // 只能在当前窗口打开 │ │
│ │ │
│ // ✔ 正确:使用原生方式打开新窗口 │ │
│ const routeData = this.$router.resolve('/help'); │ │
│ window.open(routeData.href, '_blank'); │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
8.2 最佳实践
┌─────────────────────────────────────────────────────────────────────┐
│ 最佳实践指南 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 统一使用路由导航 │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ // 创建导航工具函数 │ │
│ // utils/navigation.js │ │
│ import router from '@/router'; │ │
│ │ │
│ export function navigateTo(path, query = {}) { │ │
│ return router.push({ path, query }); │ │
│ } │ │
│ │ │
│ export function navigateByName(name, params = {}) { │ │
│ return router.push({ name, params }); │ │
│ } │ │
│ │ │
│ export function redirectTo(path) { │ │
│ return router.replace(path); │ │
│ } │ │
│ │ │
│ export function openInNewTab(path) { │ │
│ const routeData = router.resolve(path); │ │
│ window.open(routeData.href, '_blank'); │ │
│ } │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ 2. 合理使用路由守卫 │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ // 权限守卫 │ │
│ router.beforeEach(async (to, from, next) => { │ │
│ // 白名单路由直接通过 │ │
│ if (to.meta.whiteList) return next(); │ │
│ │ │
│ // 检查登录状态 │ │
│ const isLoggedIn = await checkAuthStatus(); │ │
│ if (!isLoggedIn && to.meta.requiresAuth) { │ │
│ return next({ │ │
│ path: '/login', │ │
│ query: { redirect: to.fullPath } │ │
│ }); │ │
│ } │ │
│ │ │
│ // 权限检查 │ │
│ if (to.meta.roles) { │ │
│ const userRole = store.state.user.role; │ │
│ if (!to.meta.roles.includes(userRole)) { │ │
│ return next('/403'); │ │
│ } │ │
│ } │ │
│ │ │
│ next(); │ │
│ }); │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
│ 3. 何时可以使用 location.href │
│ ┌────────────────────────────────────────────────────────────────┐ │
│ // 合理使用场景: │ │
│ │ │
│ // 场景 1:跳转到外部网站 │ │
│ window.location.href = 'https://external-site.com'; │ │
│ │ │
│ // 场景 2:完全重置应用状态(如退出登录) │ │
│ function logout() { │ │
│ clearAllStorage(); │ │
│ window.location.href = '/login'; // 完全重置应用 │ │
│ } │ │
│ │ │
│ // 场景 3:非 Vue 管理的页面(如静态页面) │ │
│ window.location.href = '/static/about.html'; │ │
│ │ │
│ // 场景 4:处理特定错误(如 404 页面在 Vue 之外) │ │
│ if (isCriticalError) { │ │
│ window.location.href = '/error.html'; │ │
│ } │ │
│ └────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
九、总结与决策矩阵
9.1 完整对比表
┌─────────────────────────────────────────────────────────────────────────┐
│ 完整对比矩阵 │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────┬──────────────────────┬────────────────────────────┐ │
│ │ 特性 │ this.$router.push │ window.location.href │ │
│ ├───────────────┼──────────────────────┼────────────────────────────┤ │
│ │ 页面刷新 │ 无刷新 │ 可能刷新(History模式) │ │
│ ├───────────────┼──────────────────────┼────────────────────────────┤ │
│ │ 导航守卫 │ 完整触发 │ 完全绕过 │ │
│ ├───────────────┼──────────────────────┼────────────────────────────┤ │
│ │ 组件生命周期 │ 正常执行 │ 可能丢失 │ │
│ ├───────────────┼──────────────────────┼────────────────────────────┤ │
│ │ 应用状态 │ 保持完整 │ 可能重置 │ │
│ ├───────────────┼──────────────────────┼────────────────────────────┤ │
│ │ 过渡动画 │ 支持 │ 不支持 │ │
│ ├───────────────┼──────────────────────┼────────────────────────────┤ │
│ │ 路由懒加载 │ 按需加载 │ 可能重复下载 │ │
│ ├───────────────┼──────────────────────┼────────────────────────────┤ │
│ │ 模式兼容性 │ 自动适配 │ 需手动处理 │ │
│ ├───────────────┼──────────────────────┼────────────────────────────┤ │
│ │ 参数传递 │ query/params │ 手动拼接 URL │ │
│ ├───────────────┼──────────────────────┼────────────────────────────┤ │
│ │ 编程控制 │ Promise 返回 │ 无返回值 │ │
│ ├───────────────┼──────────────────────┼────────────────────────────┤ │
│ │ 错误处理 │ 可捕获错误 │ 无错误处理 │ │
│ ├───────────────┼──────────────────────┼────────────────────────────┤ │
│ │ 性能 │ 高(无重新加载) │ 低(重新加载) │ │
│ ├───────────────┼──────────────────────┼────────────────────────────┤ │
│ │ SEO 友好 │ 需 SSR │ 原生支持 │ │
│ ├───────────────┼──────────────────────┼────────────────────────────┤ │
│ │ 推荐程度 │ 强烈推荐 │ 仅限特殊场景 │ │
│ └───────────────┴──────────────────────┴────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
9.2 选择决策流程图
需要在 Vue 应用中跳转页面
│
▼
┌─────────────────┐
│ 是否为 Vue SPA │
│ 内部路由? │
└─────────────────┘
│
┌────────────┴────────────┐
│ │
是 否
│ │
▼ ▼
┌──────────────┐ ┌─────────────────┐
│ 使用 │ │ 跳转到外部网站 │
│ $router.push │ │ 或非 Vue 页面 │
└──────────────┘ └─────────────────┘
│ │
▼ ▼
┌──────────────────┐ window.location.href
│ 需要权限检查? │
└──────────────────┘
│
┌────────┴────────┐
│ │
是 否
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ beforeEach │ │ 直接 push │
│ 守卫拦截 │ └─────────────┘
└─────────────┘
│
▼
┌──────────────────────┐
│ 需要新窗口打开? │
└──────────────────────┘
│
┌────┴────┐
│ │
是 否
│ │
▼ ▼
┌────────┐ ┌────────┐
│resolve │ │ push │
│+ open │ └────────┘
└────────┘
结语
this.$router.push() 和 window.location.href 虽然都能实现页面跳转,但本质上是两种完全不同的架构思路的体现:
$router.push()代表了现代 SPA 应用的设计理念:状态驱动、组件化、声明式,充分利用框架的能力,实现流畅的用户体验和可维护的代码结构。location.href则是传统多页应用的遗留方式:命令式、页面中心、状态独立,在 SPA 中使用会破坏应用的完整性和用户体验。
核心建议:在 Vue SPA 中,99% 的场景都应该使用$router.push(),只有在外部跳转、完全重置应用等特殊场景下,才考虑使用location.href。
理解这两者的区别,不仅能帮助你写出更好的代码,更能让你深入理解 SPA 架构的设计哲学。