前端的路由模式包括了 Hash 模式和 History 模式。html
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
的實現。vue
vue-router 經過 new 一個 HashHistory
來實現 Hash 模式路由。vue-router
this.history = new HashHistory(this, options.base, fallback)
複製代碼
三個參數分別表明:編程
HashHistory 繼承 History 類,有一些屬性與方法都來自於 History 類。先來看下 HashHistory 的構造函數 constructor。瀏覽器
構造函數主要作了四件事情。session
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()
})
})
}
複製代碼
下面細講一下這幾件事情的細節。app
先來看構造函數作的第二件事情,fallback 爲 true 的狀況,通常是低版本的瀏覽器(IE9)不支持 History 模式,因此會被降級爲 Hash 模式。ide
同時須要經過 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
方法作的事情就是確保 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 路由。
最後看看構造函數作的第四件事情。
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
等編程式導航,有興趣能夠直接看源碼,這裏就不一一講解了,主要也是運用了上面的方法來實現。
vue-router 經過 new 一個 HTML5History
來實現 History 模式路由。
this.history = new HTML5History(this, options.base)
複製代碼
HTML5History 也是繼承與 History 類。
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)
})
}
}
複製代碼
下面細講一下這幾件事情的細節。
先從監聽滾動條滾動事件提及吧。
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
事件。這時一樣會調用 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
方法來設置滾動條位置。
其中有三個方法用來對座標進行處理的,分別是:
代碼量不大,具體的代碼細節感興趣的能夠看一下。
一樣,HTML5History 也分別實現了 push
、replace
、go
等編程式導航。
至此,HashHistory 和 HTML5History 的實現就大體瞭解了。在閱讀的過程當中,咱們不斷地遇到了父類 History
與其 transitionTo
方法,下一篇就來對其進行深刻了解吧。