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

