Vue 路由跳转深度解析 $router.push VS location.href

简介: 本文深度剖析 Vue SPA 中 `this.$router.push()` 与 `window.location.href` 的本质差异:前者基于 Vue Router 实现无刷新导航,完整触发守卫、保持状态、支持懒加载;后者绕过框架导致刷新、丢状态、失效守卫。涵盖原理、源码、模式对比及最佳实践,助你写出高可用、高性能的路由逻辑。(239字)

前言

在 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 架构的设计哲学。
相关文章
|
10月前
|
Rust Java Go
Go、Rust、Kotlin、Python 与 Java 从性能到生态,全面解读五大主流编程语言
本文系统对比Go、Rust、Kotlin、Python与Java五大主流语言,从性能、并发、类型系统到生态、学习曲线等维度深入分析,结合代码示例与应用场景,助你精准选型,把握技术趋势。
2276 7
|
6月前
|
JavaScript 前端开发 NoSQL
Node.js 后端开发全解析:从核心原理架构到实战应用
本文系统解析Node.js核心原理(V8+libuv、事件循环)、后端分层/MVC架构、Express/NestJS实战案例,并客观分析其高并发优势与CPU密集型短板,助力开发者精准选型与高效落地。(239字)
729 2
|
存储
第42节: Vue3 组件之间传值
第42节: Vue3 组件之间传值
475 1
|
XML C# 数据格式
WPF技术之xmlns
在WPF中,xmlns是XML命名空间(XML Namespace)的缩写,用于引入不同的XML命名空间,以便在XAML文件中使用特定的命名空间中的元素、属性和类型。
1037 1
|
JavaScript 前端开发 安全
nodejs 跨域设置
nodejs 跨域设置
504 0
|
前端开发 开发者 容器
Web站,前端接入钉钉扫码登录!
Web站,前端接入钉钉扫码登录!
|
7月前
|
运维 算法 网络安全
阿里云SSL证书重大升级:2月起订阅制上线,有效期缩至 200 天
自2026年2月起,阿里云SSL证书将升级为订阅服务模式,以应对CA/B论坛新规——单张证书有效期缩短至200天。用户可选1/2/3年订阅,在有效期内自动获取多张新证书,保障服务连续性,降低运维压力。(239字)
|
5月前
|
传感器 数据采集 人工智能
AI+数字孪生:从实时映射到智能决策的技术架构与实践路径
当数字孪生遇上AI,物理世界拥有了“预演未来”的能力。2026年,“可执行数字孪生”兴起——凡拓数创自研AI 3D引擎支持多物理场仿真与千万次并行训练,已在机器人Sim2Real迁移、工业预测性维护等场景落地见效。
|
7月前
|
数据采集 人工智能 安全
2026年AI Agent员工打造指南:阿里云及本地零成本部署OpenClaw(Clawdbot),解锁700+实用Skills
2026年,OpenClaw(原Clawdbot、Moltbot)凭借13万+GitHub Star的超高人气,成为打造私人AI员工的首选工具。但很多人部署后发现,裸奔的OpenClaw就像“聪明却无执行力的实习生”——能聊天、能推理,却无法独立完成代码部署、数据采集、自动化办公等实操任务。其实核心问题在于缺少“技能武装”:模型是AI的大脑,而Skills是AI的手脚,没有Skills的OpenClaw,只是一个空有智慧的“瘫痪天才”。
1101 5