vue-router 源碼:路由模式

前言

前端的路由模式包括了 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
  })
}
複製代碼

本次重點來了解一下 HTML5HistoryHashHistory 的實現。vue

HashHistory

vue-router 經過 new 一個 HashHistory 來實現 Hash 模式路由。vue-router

this.history = new HashHistory(this, options.base, fallback)
複製代碼

三個參數分別表明:編程

  • this:Router 實例
  • base:應用的基路徑
  • fallback:History 模式,但不支持 History 而被轉成 Hash 模式

HashHistory 繼承 History 類,有一些屬性與方法都來自於 History 類。先來看下 HashHistory 的構造函數 constructor。瀏覽器

constructor

構造函數主要作了四件事情。session

  1. 經過 super 調用父類構造函數,這個先放一邊。
  2. 處理 History 模式,但不支持 History 而被轉成 Hash 模式的狀況。
  3. 確保 # 後面有斜槓,沒有則加上。
  4. 實現跳轉到 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()
    })
  })
}
複製代碼

下面細講一下這幾件事情的細節。app

checkFallback

先來看構造函數作的第二件事情,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 裏面調用的 getLocationcleanPath 方法的實現。

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 還分別實現了 pushreplacego 等編程式導航,有興趣能夠直接看源碼,這裏就不一一講解了,主要也是運用了上面的方法來實現。

HTML5History

vue-router 經過 new 一個 HTML5History 來實現 History 模式路由。

this.history = new HTML5History(this, options.base)
複製代碼

HTML5History 也是繼承與 History 類。

constructor

HTML5History 的構造函數作了這麼幾件事情:

  1. 調用父類 transitionTo 方法,觸發守衛導航,之後細講。
  2. 監聽 popstate 事件。
  3. 若是有滾動行爲,則監聽滾動條滾動。
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 也分別實現了 pushreplacego 等編程式導航。

最後

至此,HashHistory 和 HTML5History 的實現就大體瞭解了。在閱讀的過程當中,咱們不斷地遇到了父類 History 與其 transitionTo 方法,下一篇就來對其進行深刻了解吧。

相關文章
相關標籤/搜索