前言
前端的路由模式包括了 Hash 模式和 History 模式。
vue-router 在初始化的時候,會根據 mode
來判斷使用不同的路由模式,從而 new 出了不同的物件例項。例如 history 模式就用 HTML5History
,hash 模式就用 HashHistory
。
init (app: any /* Vue component instance */) {
this.app = app
const { mode, options, fallback } = this
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, fallback)
break
case 'abstract':
this.history = new AbstractHistory(this)
break
default:
assert(false, `invalid mode: ${mode}`)
}
this.history.listen(route => {
this.app._route = route
})
}
複製程式碼
本次重點來了解一下 HTML5History
和 HashHistory
的實現。
HashHistory
vue-router 通過 new 一個 HashHistory
來實現 Hash 模式路由。
this.history = new HashHistory(this, options.base, fallback)
複製程式碼
三個引數分別代表:
- this:Router 例項
- base:應用的基路徑
- fallback:History 模式,但不支援 History 而被轉成 Hash 模式
HashHistory 繼承 History 類,有一些屬性與方法都來自於 History 類。先來看下 HashHistory 的建構函式 constructor。
constructor
建構函式主要做了四件事情。
- 通過 super 呼叫父類建構函式,這個先放一邊。
- 處理 History 模式,但不支援 History 而被轉成 Hash 模式的情況。
- 確保 # 後面有斜槓,沒有則加上。
- 實現跳轉到 hash 頁面,並監聽 hash 變化事件。
constructor (router: VueRouter, base: ?string, fallback: boolean) {
super(router, base)
// check history fallback deeplinking
if (fallback && this.checkFallback()) {
return
}
ensureSlash()
this.transitionTo(getHash(), () => {
window.addEventListener('hashchange', () => {
this.onHashChange()
})
})
}
複製程式碼
下面細講一下這幾件事情的細節。
checkFallback
先來看建構函式做的第二件事情,fallback 為 true 的情況,一般是低版本的瀏覽器(IE9)不支援 History 模式,所以會被降級為 Hash 模式。
同時需要通過 checkFallback
方法來檢測 url。
checkFallback () {
// 去掉 base 字首
const location = getLocation(this.base)
// 如果不是以 /# 開頭
if (!/^\/#/.test(location)) {
window.location.replace(
cleanPath(this.base + '/#' + location)
)
return true
}
}
複製程式碼
先通過 getLocation 方法來去掉 base 字首,接著正則判斷 url 是否以 /# 為開頭。如果不是,則將 url 替換成以 /# 為開頭。最後跳出 constructor,因為在 IE9 下以 Hash 方式的 url 切換路由,它會使得整個頁面進行重新整理,後面的監聽 hashchange 不會起作用,所以直接 return 跳出。
再來看看 checkFallback 裡面呼叫的 getLocation
和 cleanPath
方法的實現。
getLocation
方法主要是去掉 base 字首。在 vue-router 官方文件裡搜尋 base
,可以知道它是應用的基路徑。
export function getLocation (base: string): string {
let path = window.location.pathname
if (base && path.indexOf(base) === 0) {
path = path.slice(base.length)
}
return (path || '/') + window.location.search + window.location.hash
}
複製程式碼
cleanPath
方法則是將雙斜槓替換成單斜槓,保證 url 路徑正確。
export function cleanPath (path: string): string {
return path.replace(/\/\//g, '/')
}
複製程式碼
ensureSlash
接下來來看看建構函式做的第三件事情。
ensureSlash
方法做的事情就是確保 url 根路徑帶上斜槓,沒有的話則加上。
function ensureSlash (): boolean {
const path = getHash()
if (path.charAt(0) === '/') {
return true
}
replaceHash('/' + path)
return false
}
複製程式碼
ensureSlash 通過 getHash
來獲取 url 的 # 符號後面的路徑,再通過 replaceHash
來替換路由。
function getHash (): string {
// We can't use window.location.hash here because it's not
// consistent across browsers - Firefox will pre-decode it!
const href = window.location.href
const index = href.indexOf('#')
return index === -1 ? '' : href.slice(index + 1)
}
複製程式碼
由於 Firefox 瀏覽器的原因(原始碼註釋裡已經寫出來了),所以不能通過 window.location.hash
來獲取,而是通過 window.location.href
來獲取。
function replaceHash (path) {
const i = window.location.href.indexOf('#')
window.location.replace(
window.location.href.slice(0, i >= 0 ? i : 0) + '#' + path
)
}
複製程式碼
replaceHash
方法做的事情則是更換 # 符號後面的 hash 路由。
onHashChange
最後看看建構函式做的第四件事情。
this.transitionTo(getHash(), () => {
window.addEventListener('hashchange', () => {
this.onHashChange()
})
})
複製程式碼
transitionTo
是父類 History 的一個方法,比較的複雜,主要是實現了 守衛導航 的功能。這裡也暫時先放一放,以後再深入瞭解。
接下來的是監聽 hashchange 事件,當 hash 路由發生的變化,會呼叫 onHashChange
方法。
onHashChange () {
if (!ensureSlash()) {
return
}
this.transitionTo(getHash(), route => {
replaceHash(route.fullPath)
})
}
複製程式碼
當 hash 路由發生的變化,即頁面發生了跳轉時,首先取保路由是以斜槓開頭的,然後觸發守衛導航,最後更換新的 hash 路由。
HashHistory 還分別實現了 push
、replace
、go
等程式設計式導航,有興趣可以直接看原始碼,這裡就不一一講解了,主要也是運用了上面的方法來實現。
HTML5History
vue-router 通過 new 一個 HTML5History
來實現 History 模式路由。
this.history = new HTML5History(this, options.base)
複製程式碼
HTML5History 也是繼承與 History 類。
constructor
HTML5History 的建構函式做了這麼幾件事情:
- 呼叫父類
transitionTo
方法,觸發守衛導航,以後細講。 - 監聽
popstate
事件。 - 如果有滾動行為,則監聽滾動條滾動。
constructor (router: VueRouter, base: ?string) {
super(router, base)
this.transitionTo(getLocation(this.base))
const expectScroll = router.options.scrollBehavior
window.addEventListener('popstate', e => {
_key = e.state && e.state.key
const current = this.current
this.transitionTo(getLocation(this.base), next => {
if (expectScroll) {
this.handleScroll(next, current, true)
}
})
})
if (expectScroll) {
window.addEventListener('scroll', () => {
saveScrollPosition(_key)
})
}
}
複製程式碼
下面細講一下這幾件事情的細節。
scroll
先從監聽滾動條滾動事件說起吧。
window.addEventListener('scroll', () => {
saveScrollPosition(_key)
})
複製程式碼
滾動條滾動後,vue-router 就會儲存滾動條的位置。這裡有兩個要了解的,一個是 saveScrollPosition
方法,一個是 _key
。
const genKey = () => String(Date.now())
let _key: string = genKey()
複製程式碼
_key
是一個當前時間戳,每次瀏覽器的前進或後退,_key 都將作為引數傳入,從而跳轉的頁面也能獲取到。那麼 _key 是做什麼用呢。
來看看 saveScrollPosition
的實現就知道了:
export function saveScrollPosition (key: string) {
if (!key) return
window.sessionStorage.setItem(key, JSON.stringify({
x: window.pageXOffset,
y: window.pageYOffset
}))
}
複製程式碼
vue-router 將滾動條位置儲存在 sessionStorage,其中的鍵就是 _key
了。
所以每一次的瀏覽器滾動,滾動條的位置將會被儲存在 sessionStorage 中,以便後面的取出使用。
popstate
瀏覽器的前進與後退會觸發 popstate
事件。這時同樣會呼叫 transitionTo 觸發守衛導航,如果有滾動行為,則呼叫 handleScroll
方法。
handleScroll 方法程式碼比較多,我們先來看看是怎麼使用滾動行為的。
scrollBehavior (to, from, savedPosition) {
if (savedPosition) {
return savedPosition
} else {
return { x: 0, y: 0 }
}
}
複製程式碼
如果要模擬“滾動到錨點”的行為:
scrollBehavior (to, from, savedPosition) {
if (to.hash) {
return {
selector: to.hash
}
}
}
複製程式碼
所以至少有三個要判斷,一個是 savedPosition(即儲存的滾動條位置),一個是 selector,還有一個就是 xy 座標。
再來看 handleScroll(刪掉一些判斷):
handleScroll (to: Route, from: Route, isPop: boolean) {
const router = this.router
const behavior = router.options.scrollBehavior
// wait until re-render finishes before scrolling
router.app.$nextTick(() => {
let position = getScrollPosition(_key)
const shouldScroll = behavior(to, from, isPop ? position : null)
if (!shouldScroll) {
return
}
const isObject = typeof shouldScroll === 'object'
if (isObject && typeof shouldScroll.selector === 'string') {
const el = document.querySelector(shouldScroll.selector)
if (el) {
position = getElementPosition(el)
} else if (isValidPosition(shouldScroll)) {
position = normalizePosition(shouldScroll)
}
} else if (isObject && isValidPosition(shouldScroll)) {
position = normalizePosition(shouldScroll)
}
if (position) {
window.scrollTo(position.x, position.y)
}
})
}
複製程式碼
從 if 判斷開始,如果有 selector
,則獲取對應的元素的座標。
否則,則使用 scrollBehavior
返回的值作為座標,其中有可能是 savedPosition 的座標,也有可能是自定義的 xy 座標。
通過一系列校驗後,最終呼叫 window.scrollTo
方法來設定滾動條位置。
其中有三個方法用來對座標進行處理的,分別是:
- getElementPosition:獲取元素座標
- isValidPosition:驗證座標是否有效
- normalizePosition:格式化座標
程式碼量不大,具體的程式碼細節感興趣的可以看一下。
同樣,HTML5History 也分別實現了 push
、replace
、go
等程式設計式導航。
最後
至此,HashHistory 和 HTML5History 的實現就大致瞭解了。在閱讀的過程中,我們不斷地遇到了父類 History
與其 transitionTo
方法,下一篇就來對其進行深入瞭解吧。