1. 程式人生 > 實用技巧 >Vue Router 路由實現原理

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'模式
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)
}
}

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)
}

  兩種模式比較

  1. pushState設定的新URL可以是與當前URL同源的任意URL;而hash只可修改#後面的部分,故只可設定與當前同文檔的URL

  2. pushState通過stateObject可以新增任意型別的資料到記錄中;而hash只可新增短字串

  3. pushState可額外設定title屬性供後續使用

  4. history模式則會將URL修改得就和正常請求後端的URL一樣,如後端沒有配置對應/user/id的路由處理,則會返回404錯誤

隨筆整理自
  https://segmentfault.com/a/1190000014822765
  https://www.jianshu.com/p/4295aec31302
感謝博主分享