深度解析Vue中的路由跳轉$router.push?VS?location.href
前言
在 Vue 單頁應用(SPA)中,頁面跳轉是一個看似簡單實則暗藏玄機的話題。this.$router.push('/login') 和 window.location.href = '/#/login' 表面上都能實現(xiàn)"跳轉到登錄頁"的效果,但其背后的實現(xiàn)機制、對應用狀態(tài)的影響、以及在不同場景下的行為差異,直接關系到應用的穩(wěn)定性、用戶體驗和可維護性。
本文將從底層原理、架構設計、源碼實現(xiàn)等多個維度進行深度剖析。
一、架構層面的根本差異
1.1 整體架構對比圖
┌─────────────────────────────────────────────────────────────────────┐
│ Vue 應用架構層面 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ this.$router.push('/login') │ │
│ │ │ │
│ │ Vue Component ──? Vue Router ──? Route Matcher │ │
│ │ │ │ │ │ │
│ │ │ ▼ ▼ │ │
│ │ │ Navigation Guards Component │ │
│ │ │ │ Lifecycle │ │
│ │ │ ▼ │ │ │
│ │ │ [權限校驗] ▼ │ │
│ │ │ [數(shù)據預取] DOM 更新 │ │
│ │ │ [過渡動畫] (無刷新) │ │
│ │ ▼ │ │
│ │ 保持應用狀態(tài)、Vuex store、事件監(jiān)聽器等 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ ══════════════════════════════════════════════════════════════ │
│ 分 割 線 │
│ ══════════════════════════════════════════════════════════════ │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ window.location.href = '/#/login' │ │
│ │ │ │
│ │ Vue App ──?──? Browser Navigation ──? Page Reload? │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ 直接修改 URL 可能觸發(fā): │ │
│ │ 繞過 Vue Router - 完整頁面刷新 │ │
│ │ - 重新下載資源 │ │
│ │ - 丟失應用狀態(tài) │ │
│ │ - 重啟 Vue 實例 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
1.2 核心差異總結
| 維度 | $router.push() | location.href |
|---|---|---|
| 控制權 | Vue Router 完全控制 | 瀏覽器原生控制 |
| 應用狀態(tài) | 保持完整 | 可能丟失 |
| 代碼執(zhí)行 | 在 Vue 上下文中 | 脫離 Vue 上下文 |
| 可預測性 | 高(遵循路由配置) | 低(依賴瀏覽器行為) |
二、Vue Router 的底層實現(xiàn)原理
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()的完整執(zhí)行流程
┌─────────────────────────────────────────────────────────────────────┐
│ $router.push() 完整執(zhí)行流程 │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ 1. 參數(shù)標準化 │
│ push('/login') 或 │
│ push({ path: '/login', query: {} }) │
└──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ 2. 路由匹配 │
│ - 根據配置的 routes 數(shù)組匹配 │
│ - 解析動態(tài)路由參數(shù) │
│ - 處理嵌套路由 │
└──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ 3. 觸發(fā)導航守衛(wèi) │
│ ┌────────────────────────────────────┐ │
│ │ 3.1 失活組件的 beforeRouteLeave │ │
│ └────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────┐ │
│ │ 3.2 全局 beforeEach │ │
│ │ (權限校驗、登錄攔截等) │ │
│ └────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────┐ │
│ │ 3.3 重用組件的 beforeRouteUpdate │ │
│ └────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────┐ │
│ │ 3.4 路由配置的 beforeEnter │ │
│ └────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────┐ │
│ │ 3.5 激活組件的 beforeRouteEnter │ │
│ └────────────────────────────────────┘ │
└──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ 4. 導航確認 │
│ - 所有守衛(wèi)都調用 next() 才會繼續(xù) │
│ - 任一守衛(wèi)調用 next(false) 則取消 │
│ - 可重定向到其他路由 │
└──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ 5. 更新 URL (根據路由模式) │
│ ┌────────────────────────────────────┐ │
│ │ Hash Mode: │ │
│ │ window.location.hash = '#/login'│ │
│ │ (觸發(fā) hashchange 事件) │ │
│ └────────────────────────────────────┘ │
│ ┌────────────────────────────────────┐ │
│ │ History Mode: │ │
│ │ history.pushState({}, '', url) │ │
│ │ (不觸發(fā)頁面刷新) │ │
│ └────────────────────────────────────┘ │
└──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ 6. 更新應用狀態(tài) │
│ - 更新 this.$route 響應式對象 │
│ - 觸發(fā) <router-view> 重新渲染 │
│ - 執(zhí)行組件生命周期鉤子 │
└──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ 7. 執(zhí)行 afterEach 鉤子 │
│ (可用于分析、進度條結束等) │
└──────────────────────────────────────────┘
│
▼
┌──────────────┐
│ 導航完成 │
└──────────────┘
2.3 源碼級別的關鍵實現(xiàn)
// Vue Router 簡化版源碼示意
class VueRouter {
push(location, onComplete, onAbort) {
// 1. 標準化 location 參數(shù)
location = normalizeLocation(location, this.currentRoute);
// 2. 匹配路由記錄
const route = this.matcher.match(location);
// 3. 確認導航(執(zhí)行導航守衛(wèi))
this.confirmTransition(route, () => {
// 4. 更新當前路由
this.updateRoute(route);
// 5. 更新 URL
this.ensureURL();
// 6. 觸發(fā)回調
onComplete && onComplete(route);
}, onAbort);
}
confirmTransition(route, onComplete, onAbort) {
// 提取所有需要執(zhí)行的導航守衛(wèi)
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)
);
// 順序執(zhí)行守衛(wèi)
runQueue(queue, (guard, next) => {
guard(route, this.currentRoute, (to) => {
if (to === false) {
// 取消導航
onAbort();
} else if (isRoute(to)) {
// 重定向
this.push(to);
} else {
// 繼續(xù)下一個守衛(wèi)
next();
}
});
}, onComplete);
}
}
三、Hash 模式 vs History 模式深度解析
3.1 Hash 模式原理
┌─────────────────────────────────────────────────────────────────────┐
│ Hash 模式工作原理 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ URL 結構: https://example.com/#/user/profile?id=123 │
│ └────────────────────┘ └──────────────────────┘ │
│ 基礎 URL hash 部分 │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 特點: │ │
│ │ 1. hash 變化不會觸發(fā)頁面刷新 │ │
│ │ 2. hash 不屬于 URL 路徑,不會被發(fā)送到服務器 │ │
│ │ 3. 通過 hashchange 事件監(jiān)聽變化 │ │
│ │ 4. 兼容性好,無需服務器配置 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ 事件監(jiān)聽流程: │
│ ┌──────────┐ hash變化 ┌─────────────┐ 觸發(fā) ┌─────┐│
│ │ 瀏覽器 │ ─────────────? │ 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 事件 // 監(jiān)聽前進/后退 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 關鍵特點: │ │
│ │ 1. URL 更美觀,沒有 # 號 │ │
│ │ 2. 需要服務器配置支持(所有路徑都返回 index.html) │ │
│ │ 3. pushState/replaceState 不會觸發(fā)頁面刷新 │ │
│ │ 4. popstate 事件僅在瀏覽器前進/后退時觸發(fā) │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ 服務器配置示例: │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 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的執(zhí)行機制
4.1 瀏覽器導航流程
┌─────────────────────────────────────────────────────────────────────┐
│ window.location.href = '/login' 執(zhí)行流程 │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ 1. 瀏覽器解析新 URL │
│ - 解析協(xié)議、域名、端口、路徑 │
└──────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ 2. 判斷是否需要刷新頁面 │
│ ┌────────────────────────────────────┐ │
│ │ 需要刷新的情況: │ │
│ │ - 協(xié)議不同 │ │
│ │ - 域名/端口不同 │ │
│ │ - 完整路徑變化(非 hash 部分) │ │
│ └────────────────────────────────────┘ │
│ ┌────────────────────────────────────┐ │
│ │ 不刷新的情況: │ │
│ │ - 僅 hash 部分變化 │ │
│ │ (如 /page → /page#section) │ │
│ └────────────────────────────────────┘ │
└──────────────────────────────────────────┘
│
┌───────────┴───────────┐
│ │
▼ ▼
需要刷新 僅 Hash 變化
│ │
▼ ▼
┌────────────────────────┐ ┌─────────────────────┐
│ 3a. 完整頁面刷新 │ │ 3b. 僅滾動到錨點 │
│ ┌──────────────────┐ │ │ 或觸發(fā) hashchange │
│ │ - 卸載當前頁面 │ │ │ │
│ │ - 清除所有狀態(tài) │ │ │ ? Vue Router │
│ │ (Vuex、事件等) │ │ │ 無法感知此變化! │
│ │ - 發(fā)起新HTTP請求 │ │ │ │
│ │ - 重新加載資源 │ │ └─────────────────────┘
│ │ - 重新初始化 Vue │ │
│ └──────────────────┘ │
└────────────────────────┘
│
▼
┌──────────────────────────────────────────┐
│ 4. 更新瀏覽器歷史記錄 │
│ (可通過后退按鈕返回) │
└──────────────────────────────────────────┘
4.2 關鍵問題:繞過 Vue Router 的后果
場景:History 模式下執(zhí)行
window.location.href = '/login'
問題分析:
瀏覽器行為:
1. 檢測到完整路徑變化(從 /home 到 /login)
2. 觸發(fā)頁面刷新
3. 向服務器請求 /login 資源
服務器響應:
- 如果配置了 SPA fallback → 返回 index.html → Vue 重新啟動
- 如果未配置 → 返回 404
Vue 應用狀態(tài):
- Vuex store 被重置
- 所有組件實例銷毀
- 事件監(jiān)聽器清除
定時器、WebSocket 等需要重新建立
五、導航守衛(wèi)完整流程圖
5.1 導航守衛(wèi)執(zhí)行順序
┌─────────────────────────────────────────────────────────────────────┐
│ 導航守衛(wèi)完整執(zhí)行順序 │
│ (從 /home 導航到 /user/profile) │
└─────────────────────────────────────────────────────────────────────┘
觸發(fā)導航: 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(); │ │
│ │ } │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 【階段二:全局前置守衛(wèi)】 │
│ │
│ 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 (全局,在所有組件內守衛(wèi)之后) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 用途:導航確認前的最后檢查 │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 【階段三:路由獨享守衛(wèi)】 │
│ │
│ 4. beforeEnter (路由配置中) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 用途:特定路由的權限控制、數(shù)據預加載 │ │
│ │ 示例: │ │
│ │ { │ │
│ │ path: '/admin', │ │
│ │ component: Admin, │ │
│ │ beforeEnter: (to, from, next) => { │ │
│ │ if (store.state.user.role !== 'admin') { │ │
│ │ next('/403'); │ │
│ │ } else { │ │
│ │ next(); │ │
│ │ } │ │
│ │ } │ │
│ │ } │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 【階段四:組件內守衛(wèi)】 │
│ │
│ 5. beforeRouteEnter (目標組件 User) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ ? 注意:此時組件實例還未創(chuàng)建,無法訪問 this │ │
│ │ 用途:獲取路由數(shù)據,通過回調訪問組件實例 │ │
│ │ 示例: │ │
│ │ beforeRouteEnter(to, from, next) { │ │
│ │ // ? 錯誤:this 未定義 │ │
│ │ // this.loadUser(); │ │
│ │ │ │
│ │ // ? 正確:通過回調訪問實例 │ │
│ │ next(vm => { │ │
│ │ vm.loadUser(to.params.id); │ │
│ │ }); │ │
│ │ } │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 6. beforeRouteUpdate (當路由參數(shù)變化時復用組件) │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 場景:/user/1 → /user/2 (同一個 User 組件) │ │
│ │ 示例: │ │
│ │ beforeRouteUpdate(to, from, next) { │ │
│ │ this.loadUser(to.params.id); │ │
│ │ next(); │ │
│ │ } │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 【階段五:導航確認】 │
│ │
│ 所有守衛(wèi)都調用 next() → 導航確認 │
│ 任一守衛(wèi)調用 next(false) → 導航取消 │
│ 任一守衛(wèi)調用 next('/path') → 重定向 │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 【階段六:更新視圖】 │
│ │
│ 1. 失活 Home 組件 → beforeDestroy → destroyed │
│ 2. 激活 User 組件 → beforeCreate → created → beforeMount → mounted│
│ 3. 更新 <router-view> │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 【階段七:全局后置鉤子】 │
│ │
│ afterEach (全局注冊) │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ 用途:進度條結束、頁面標題設置、埋點統(tǒng)計 │ │
│ │ 示例: │ │
│ │ router.afterEach((to, from) => { │ │
│ │ NProgress.done(); │ │
│ │ document.title = to.meta.title || 'My App'; │ │
│ │ analytics.trackPageView(to.fullPath); │ │
│ │ }); │ │
│ └──────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
▼
導航完成
5.2 守衛(wèi)對比:$router.pushvslocation.href
┌─────────────────────────────────────────────────────────────────────┐
│ 導航守衛(wèi)觸發(fā)對比 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ this.$router.push('/login') │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ ? beforeRouteLeave → 觸發(fā) │ │
│ │ ? beforeEach → 觸發(fā) │ │
│ │ ? beforeEnter → 觸發(fā) │ │
│ │ ? beforeRouteEnter → 觸發(fā) │ │
│ │ ? afterEach → 觸發(fā) │ │
│ │ ? 組件生命周期 → 正常執(zhí)行 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ window.location.href = '/#/login' (Hash 模式) │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ ? beforeRouteLeave → 不觸發(fā) │ │
│ │ ? beforeEach → 不觸發(fā) │ │
│ │ ? beforeEnter → 不觸發(fā) │ │
│ │ ? beforeRouteEnter → 不觸發(fā) │ │
│ │ ? afterEach → 不觸發(fā) │ │
│ │ ? 可能觸發(fā) hashchange → Vue Router 可能捕獲到變化 │ │
│ │ ? 組件狀態(tài) → 可能不一致 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
│ window.location.href = '/login' (History 模式) │
│ ┌──────────────────────────────────────────────────────────────┐ │
│ │ ? 所有導航守衛(wèi) → 全部失效 │ │
│ │ ?? 頁面完全刷新 → Vue 應用重啟 │ │
│ │ ?? 應用狀態(tài)丟失 → Vuex 重置 │ │
│ │ ?? 內存泄漏風險 → 未清理的事件監(jiān)聽器、定時器 │ │
│ └──────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
六、實戰(zhàn)場景深度對比
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 參數(shù)
// 2. Vuex 狀態(tài)可能丟失
// 3. 導航守衛(wèi)失效,安全檢查被繞過
} 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 狀態(tài)已清空,但應用可能未正確重置
}
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 檢查權限
- 未登錄 → 重定向到登錄頁
- 已登錄 → 繼續(xù)
2. 開始加載 Admin.vue 的 chunk
- 顯示加載指示器
- 網絡請求獲取 JS 文件
3. chunk 加載完成
- 執(zhí)行 beforeEnter
- 創(chuàng)建組件實例
- 渲染頁面
location.href 的風險
window.location.href = '/admin'
問題:
1. 如果是 History 模式 → 頁面刷新
2. 向服務器請求 /admin 資源
3. 服務器可能返回 404(SPA fallback 未配置)
4. 即使返回 index.html,也重新下載了所有資源
5. 懶加載的優(yōu)勢被破壞
七、性能與用戶體驗對比
7.1 性能對比測試
性能對比(模擬數(shù)據)
測試場景:從首頁跳轉到用戶中心(包含 3 個 API 請求)
this.$router.push('/user') 導航開始到頁面可交互:
- 路由守衛(wèi)執(zhí)行: ~5ms
- 組件實例化: ~10ms
- DOM 更新: ~15ms
- API 請求: ~200-500ms
總計: ~230-530ms
優(yōu)勢:
- 無白屏時間
- 可顯示加載狀態(tài)
- 保持滾動位置(可配置)
- 資源無需重新下載
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
劣勢:
- 明顯白屏時間
- 重復下載資源
- 丟失應用狀態(tài)
- 滾動位置重置
性能差距:約 3-5 倍
7.2 用戶體驗對比
用戶體驗對比
this.$router.push()
- 無白屏,過渡流暢
- 可顯示頁面加載進度條
- 支持頁面切換動畫
- 保持全局狀態(tài)(主題、語言等)
- 可實現(xiàn)頁面緩存功能
- 支持 prefetch 預加載
window.location.href
- 明顯的白屏閃爍
- 瀏覽器原生加載指示器
- 無過渡動畫
- 所有狀態(tài)丟失,需重新初始化
- 用戶需要重新等待所有資源加載
- 表單數(shù)據、未保存內容丟失
八、常見陷阱與最佳實踐
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);
// 或者更好的做法:在導航守衛(wèi)中處理 陷阱 3:忽略路由守衛(wèi)的權限檢查
// ? 危險:繞過權限檢查
if (userClickedAdminLink) {
window.location.href = '/admin';
}
// 即使沒有權限,也能訪問 admin 頁面(雖然 API 會拒絕)
// ? 正確:通過路由跳轉,觸發(fā)權限檢查
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. 統(tǒng)一使用路由導航
// 創(chuàng)建導航工具函數(shù)
// 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. 合理使用路由守衛(wèi)
// 權限守衛(wèi)
router.beforeEach(async (to, from, next) => {
// 白名單路由直接通過
if (to.meta.whiteList) return next();
// 檢查登錄狀態(tài)
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.;
// 場景 2:完全重置應用狀態(tài)(如退出登錄)
function logout() {
clearAllStorage();
window.location.href = '/login'; // 完全重置應用
}
// 場景 3:非 Vue 管理的頁面(如靜態(tài)頁面)
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模式) |
| 導航守衛(wèi) | 完整觸發(fā) | 完全繞過 |
| 組件生命周期 | 正常執(zhí)行 | 可能丟失 |
| 應用狀態(tài) | 保持完整 | 可能重置 |
| 過渡動畫 | 支持 | 不支持 |
| 路由懶加載 | 按需加載 | 可能重復下載 |
| 模式兼容性 | 自動適配 | 需手動處理 |
| 參數(shù)傳遞 | query/params | 手動拼接 URL |
| 編程控制 | Promise 返回 | 無返回值 |
| 錯誤處理 | 可捕獲錯誤 | 無錯誤處理 |
| 性能 | 高(無重新加載) | 低(重新加載) |
| SEO 友好 | 需 SSR | 原生支持 |
| 推薦程度 | 強烈推薦 | 僅限特殊場景 |
9.2 選擇決策流程圖
需要在 Vue 應用中跳轉頁面
│
▼
┌─────────────────┐
│ 是否為 Vue SPA │
│ 內部路由? │
└─────────────────┘
│
┌────────────┴────────────┐
│ │
是 否
│ │
▼ ▼
┌──────────────┐ ┌─────────────────┐
│ 使用 │ │ 跳轉到外部網站 │
│ $router.push │ │ 或非 Vue 頁面 │
└──────────────┘ └─────────────────┘
│ │
▼ ▼
┌──────────────────┐ window.location.href
│ 需要權限檢查? │
└──────────────────┘
│
┌────────┴────────┐
│ │
是 否
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ beforeEach │ │ 直接 push │
│ 守衛(wèi)攔截 │ └─────────────┘
└─────────────┘
│
▼
┌──────────────────────┐
│ 需要新窗口打開? │
└──────────────────────┘
│
┌────┴────┐
│ │
是 否
│ │
▼ ▼
┌────────┐ ┌────────┐
│resolve │ │ push │
│+ open │ └────────┘
└────────┘
結語
this.$router.push() 和 window.location.href 雖然都能實現(xiàn)頁面跳轉,但本質上是兩種完全不同的架構思路的體現(xiàn):
$router.push()代表了現(xiàn)代 SPA 應用的設計理念:狀態(tài)驅動、組件化、聲明式,充分利用框架的能力,實現(xiàn)流暢的用戶體驗和可維護的代碼結構。location.href則是傳統(tǒng)多頁應用的遺留方式:命令式、頁面中心、狀態(tài)獨立,在 SPA 中使用會破壞應用的完整性和用戶體驗。
核心建議:在 Vue SPA 中,99% 的場景都應該使用 $router.push(),只有在外部跳轉、完全重置應用等特殊場景下,才考慮使用 location.href。
理解這兩者的區(qū)別,不僅能幫助你寫出更好的代碼,更能讓你深入理解 SPA 架構的設計哲學。
以上就是深度解析Vue中的路由跳轉$router.push VS location.href的詳細內容,更多關于Vue路由跳轉的資料請關注腳本之家其它相關文章!
相關文章
關于ElementUI的el-upload組件二次封裝的問題
這篇文章主要介紹了關于ElementUI的el-upload組件二次封裝的問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-10-10
vue修改proxyTable解決跨域請求,報404的問題及解決
這篇文章主要介紹了vue修改proxyTable解決跨域請求,報404的問題及解決方案,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-07-07
Element InputNumber計數(shù)器的使用方法
這篇文章主要介紹了Element InputNumber計數(shù)器的使用方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-07-07
vue3+element 分片上傳與分片下載功能實現(xiàn)方法詳解
這篇文章主要介紹了vue3+element 分片上傳與分片下載功能實現(xiàn)方法,結合實例形式詳細分析了vue3+element 分片上傳與下載相關實現(xiàn)技巧與操作注意事項,需要的朋友可以參考下2023-06-06

