深度解析Vue中的路由跳转$router.push VS location.href
前言
在 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.pushvslocation.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 架构的设计哲学。
以上就是深度解析Vue中的路由跳转$router.push VS location.href的详细内容,更多关于Vue路由跳转的资料请关注脚本之家其它相关文章!
相关文章
Vue 数值改变页面没有刷新的问题解决(数据改变视图不更新的问题)
这篇文章主要介绍了Vue 数值改变页面没有刷新的问题解决(数据改变视图不更新的问题),本文结合实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下2023-09-09


最新评论