Vue Router 路由實現原理
一、概念
通過改變 URL,在不重新請求頁面的情況下,更新頁面檢視。
二、實現方式
更新檢視但不重新請求頁面,是前端路由原理的核心之一,目前在瀏覽器環境中這一功能的實現主要有2
種方式:
1.Hash--- 利用 URL 中的hash("#");
2.利用History interface在HTML5中新增的方法。
Vue 中,它是通過mode這一引數控制路由的實現模式:
const router=new VueRouter({ mode:'history', routes:[...] })
建立 VueRouter 的例項物件時,mode 以構造引數的形式傳入,如下程式碼:
src/index.js
export default class VueRouter{
mode: string; // 傳入的字串引數,指示history類別
history: HashHistory | HTML5History | AbstractHistory; // 實際起作用的物件屬性,必須是以上三個類的列舉
fallback: boolean; // 如瀏覽器不支援,'history'模式需回滾為'hash'模式
constructor (options: RouterOptions = {}) {
let mode = options.mode || 'hash' // 預設為'hash'模式
if (this.fallback) {
mode = 'hash'
}
if (!inBrowser) {
mode = 'abstract' // 不在瀏覽器環境下執行需強制為'abstract'模式
}
this.mode = mode
// 根據mode確定history實際的類並例項化
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}
}
init (app: any /* Vue component instance */) {
const history = this.history
// 根據history的類別執行相應的初始化操作和監聽
if (history instanceof HTML5History) {
history.transitionTo(history.getCurrentLocation())
} else if (history instanceof HashHistory) {
const setupHashListener = () => {
history.setupListeners()
}
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
)
}
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
}
// VueRouter類暴露的以下方法實際是呼叫具體history物件的方法
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.history.push(location, onComplete, onAbort)
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.history.replace(location, onComplete, onAbort)
}
}
src/index.js export default class VueRouter{ mode: string; // 傳入的字串引數,指示history類別 history: HashHistory | HTML5History | AbstractHistory; // 實際起作用的物件屬性,必須是以上三個類的列舉 fallback: boolean; // 如瀏覽器不支援,'history'模式需回滾為'hash'模式 constructor (options: RouterOptions = {}) { let mode = options.mode || 'hash' // 預設為'hash'模式 this.fallback = mode === 'history' && !supportsPushState // 通過supportsPushState判斷瀏覽器是否支援'history'模式 if (this.fallback) { mode = 'hash' } if (!inBrowser) { mode = 'abstract' // 不在瀏覽器環境下執行需強制為'abstract'模式 } this.mode = mode // 根據mode確定history實際的類並例項化 switch (mode) { case 'history': this.history = new HTML5History(this, options.base) break case 'hash': this.history = new HashHistory(this, options.base, this.fallback) break case 'abstract': this.history = new AbstractHistory(this, options.base) break default: if (process.env.NODE_ENV !== 'production') { assert(false, `invalid mode: ${mode}`) } } } init (app: any /* Vue component instance */) { const history = this.history // 根據history的類別執行相應的初始化操作和監聽 if (history instanceof HTML5History) { history.transitionTo(history.getCurrentLocation()) } else if (history instanceof HashHistory) { const setupHashListener = () => { history.setupListeners() } history.transitionTo( history.getCurrentLocation(), setupHashListener, setupHashListener ) } history.listen(route => { this.apps.forEach((app) => { app._route = route }) }) } // VueRouter類暴露的以下方法實際是呼叫具體history物件的方法 push (location: RawLocation, onComplete?: Function, onAbort?: Function) { this.history.push(location, onComplete, onAbort) } replace (location: RawLocation, onComplete?: Function, onAbort?: Function) { this.history.replace(location, onComplete, onAbort) } }
mode引數:
1.預設 hash
2. history。如果瀏覽器不支援 history 新特性,則採用 hash
3. 如果不在瀏覽器環境下,就採用 abstract(Node環境下)
mode 區別:
1. mode:"hash" 多了 “#”
http://localhost:8080/#/login
2.mode:"history"
http://localhost:8080/recommend
HashHistory:
hash("#") 的作用是載入 URL 中指示網頁中的位置。
# 本身以及它後面的字元稱職位 hash,可通過 window.location.hash 獲取
特點:
1. hash 雖然出現在 url 中,但不會被包括在 http 請求中,它是用來指導瀏覽器動作的,對伺服器端完全無用,因此,改變 hash 不會重新載入頁面。
2. 可以為 hash 的改變新增監聽事件:
window.addEventListener("hashchange",funcRef,false)
3.每一次改變 hash(window.localtion.hash),都會在瀏覽器訪問歷史中增加一個記錄。
利用 hash 的以上特點,就可以來實現前端路由"更新檢視但不重新請求頁面"的功能了。
HashHistory擁有兩個方法,一個是push, 一個是replace
1 |
兩個方法:HashHistory.push() 和 HashHistory.replace()
|
HashHistory.push() 將新路由新增到瀏覽器訪問歷史的棧頂
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.transitionTo(location, route => {
pushHash(route.fullPath)
onComplete && onComplete(route)
}, onAbort)
}
function pushHash (path) {
window.location.hash = path
}
HashHisttory.push()
push (location: RawLocation, onComplete?: Function, onAbort?: Function) { this.transitionTo(location, route => { pushHash(route.fullPath) onComplete && onComplete(route) }, onAbort) } function pushHash (path) { window.location.hash = path }
從設定路由改變到檢視更新的流程:
$router.push() --> HashHistory.push() --> History.transitionTo() --> History.updateRoute() --> {app._route = route} --> vm.render() |
解析:
1 $router.push() //呼叫方法 2 HashHistory.push() //根據hash模式呼叫,設定hash並新增到瀏覽器歷史記錄(新增到棧頂)(window.location.hash= XXX) 3 History.transitionTo() //監測更新,更新則呼叫History.updateRoute() 4 History.updateRoute() //更新路由 5 {app._route= route} //替換當前app路由 6 vm.render() //更新檢視
transitionTo()方法是父類中定義的是用來處理路由變化中的基礎邏輯的,push() 方法最主要的是對 window 的 hash 進行了直接賦值:
window.location.hash=route.fullPath
hash的改變會自動新增到瀏覽器的訪問歷史記錄中。
那麼檢視的更新是怎麼實現的呢,我們來看看父類 History 中的transitionTo()方法:
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) { const route = this.router.match(location, this.current) this.confirmTransition(route, () => { this.updateRoute(route) ... }) } updateRoute (route: Route) { this.cb && this.cb(route) } listen (cb: Function) { this.cb = cb }
可以看到,當路由變化時,呼叫了Hitory
中的this.cb
方法,而this.cb
方法是通過History.listen(cb)
進行設定的,回到VueRouter
類定義中,找到了在init()
中對其進行了設定:
init (app: any /* Vue component instance */) { this.apps.push(app) history.listen(route => { this.apps.forEach((app) => { app._route = route }) }) }
HashHistory.replace()
replace()方法與push()方法不同之處在於,它並不是將新路由新增到瀏覽器訪問歷史的棧頂,而是替換掉當前的路由
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {this.transitionTo(location, route => {
replaceHash(route.fullPath)
onComplete && onComplete(route)
}, onAbort)
}
function replaceHash (path) {
const i = window.location.href.indexOf('#')
window.location.replace(
window.location.href.slice(0, i >= 0 ? i : 0) + '#' + path
)
}
HashHisttory.replace()
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) { this.transitionTo(location, route => { replaceHash(route.fullPath) onComplete && onComplete(route) }, onAbort) } function replaceHash (path) { const i = window.location.href.indexOf('#') window.location.replace( window.location.href.slice(0, i >= 0 ? i : 0) + '#' + path ) }
HTML5History
History interface是瀏覽器歷史記錄棧提供的介面,通過back()、forward()、go()等方法,我們可以讀取瀏覽器歷史記錄棧的資訊,進行各種跳轉操作。
從 HTML5開始,History interface提供了2個新的方法:pushState()、replaceState() 使得我們可以對瀏覽器歷史記錄棧進行修改:
window.history.pushState(stateObject,title,url) window.history,replaceState(stateObject,title,url)
stateObject:當瀏覽器跳轉到新的狀態時,將觸發 Popstate 事件,該事件將攜帶這個 stateObject引數的副本
title:所新增記錄的標題
url:所新增記錄的 url
這2
個方法有個共同的特點:當呼叫他們修改瀏覽器歷史棧後,雖然當前url
改變了,但瀏覽器不會立即傳送請求該url
,這就為單頁應用前端路由,更新檢視但不重新請求頁面提供了基礎
1.push
與hash模式類似,只是將window.hash改為history.pushState
2.replace
與hash模式類似,只是將window.replace改為history.replaceState
3.監聽地址變化
在HTML5History的建構函式中監聽popState(window.onpopstate)
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
pushState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
replaceState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
// src/util/push-state.js
export function pushState (url?: string, replace?: boolean) {
saveScrollPosition()
// try...catch the pushState call to get around Safari
// DOM Exception 18 where it limits to 100 pushState calls
const history = window.history
try {
if (replace) {
history.replaceState({ key: _key }, '', url)
} else {
_key = genKey()
history.pushState({ key: _key }, '', url)
}
} catch (e) {
window.location[replace ? 'replace' : 'assign'](url)
}
}
export function replaceState (url?: string) {
pushState(url, true)
}
push (location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this this.transitionTo(location, route => { pushState(cleanPath(this.base + route.fullPath)) handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort) } replace (location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this this.transitionTo(location, route => { replaceState(cleanPath(this.base + route.fullPath)) handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort) } // src/util/push-state.js export function pushState (url?: string, replace?: boolean) { saveScrollPosition() // try...catch the pushState call to get around Safari // DOM Exception 18 where it limits to 100 pushState calls const history = window.history try { if (replace) { history.replaceState({ key: _key }, '', url) } else { _key = genKey() history.pushState({ key: _key }, '', url) } } catch (e) { window.location[replace ? 'replace' : 'assign'](url) } } export function replaceState (url?: string) { pushState(url, true) }
兩種模式比較
-
pushState設定的新URL可以是與當前URL同源的任意URL;而hash只可修改#後面的部分,故只可設定與當前同文檔的URL
-
pushState通過stateObject可以新增任意型別的資料到記錄中;而hash只可新增短字串
-
pushState可額外設定title屬性供後續使用
-
history模式則會將URL修改得就和正常請求後端的URL一樣,如後端沒有配置對應/user/id的路由處理,則會返回404錯誤