1. 程式人生 > 程式設計 >淺析vue-router實現原理及兩種模式

淺析vue-router實現原理及兩種模式

之前用Vue開發單頁應用,發現不管路由怎麼變化,瀏覽器位址列總是會有一個'#'號。

淺析vue-router實現原理及兩種模式
淺析vue-router實現原理及兩種模式

當時檢查自己的程式碼,沒有發現請求的地址帶'#',當時也很納悶,但是由於沒有影響頁面的渲染以及向後臺傳送請求,當時也沒有在意。最近看了一下vue-router的實現原理,才逐漸揭開了這個謎題。

vue-router 的兩種方式(瀏覽器環境下)

1. Hash (對應HashHistory)

hash(“#”)符號的本來作用是加在URL中指示網頁中的位置:

http://www.example.com/index.html#print

#符號本身以及它後面的字元稱之為hash(也就是我之前為什麼位址列都會有一個‘#'),可通過window.location.hash屬性讀取。它具有如下特點:

hash雖然出現在URL中,但不會被包括在HTTP請求中。它是用來指導瀏覽器動作的,對伺服器端完全無用,因此,改變hash不會重新載入頁面

2.可以為hash的改變新增監聽事件:

window.addEventListener("hashchange",funcRef,false)

每一次改變hash(window.location.hash),都會在瀏覽器的訪問歷史中增加一個記錄

利用hash的以上特點,就可以來實現前端路由“更新檢視但不重新請求頁面”的功能了。

2. History (對應HTML5History)

History介面 是瀏覽器歷史記錄棧提供的介面,通過back(),forward(),go()等方法,我們可以讀取瀏覽器歷史記錄棧的資訊,進行各種跳轉操作。

從HTML5開始,History interface提供了兩個新的方法:pushState(),replaceState()使得我們可以對瀏覽器歷史記錄棧進行修改:

window.history.pushState(stateObject,title,URL)
window.history.replaceState(stateObject,URL)

stateObject: 當瀏覽器跳轉到新的狀態時,將觸發popState事件,該事件將攜帶這個stateObject引數的副本 title: 所新增記錄的標題 URL: 所新增記錄的URL

這兩個方法有個共同的特點:當呼叫他們修改瀏覽器歷史記錄棧後,雖然當前URL改變了,但瀏覽器不會重新整理頁面,這就為單頁應用前端路由“更新檢視但不重新請求頁面”提供了基礎。 瀏覽器歷史記錄可以看作一個「棧」。棧是一種後進先出的結構,可以把它想象成一摞盤子,使用者每點開一個新網頁,都會在上面加一個新盤子,叫「入棧」。使用者每次點選「後退」按鈕都會取走最上面的那個盤子,叫做「出棧」。而每次瀏覽器顯示的自然是最頂端的盤子的內容。

vue-router 的作用

vue-router的作用就是通過改變URL,在不重新請求頁面的情況下,更新頁面檢視。簡單的說就是,雖然位址列的地址改變了,但是並不是一個全新的頁面,而是之前的頁面某些部分進行了修改。

export default new Router({
 // mode: 'history',//後端支援可開
 routes: constantRouterMap
})

這是Vue專案中常見的一段初始化vue-router的程式碼,之前沒仔細研究過vue-router,不知道還有一個mode屬性,後來看了相關文章後瞭解到,mode屬性用來指定vue-router使用哪一種模式。在沒有指定mode的值,則使用hash模式。

原始碼分析

首先看一下vue-router的建構函式

constructor (options: RouterOptions = {}) {
 this.app = null
 this.apps = []
 this.options = options
 this.beforeHooks = []
 this.resolveHooks = []
 this.afterHooks = []
 this.matcher = createMatcher(options.routes || [],this)

 let mode = options.mode || 'hash'
 this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
 if (this.fallback) {
 mode = 'hash'
 }
 if (!inBrowser) {
 mode = 'abstract'
 }
 this.mode = mode

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

主要是先獲取mode的值,如果mode的值為 history 但是瀏覽器不支援 history 模式,那麼就強制設定mode值為 hash 。如果支援則為 history 。接下來,根據mode的值,來選擇vue-router使用哪種模式。

case 'history':
 this.history = new HTML5History(this,options.base)
 break
case 'hash':
 this.history = new HashHistory(this,this.fallback)
 break

這樣就有了兩種模式。確定好了vue-router使用哪種模式後,就到了init。 先來看看router 的 init 方法就幹了哪些事情,在 src/index.js 中

init (app: any /* Vue component instance */) {
// ....
 const history = this.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,onAbort?: Function) {
 this.history.replace(location,onAbort)
 }
}

如果是HTML5History,則執行

history.transitionTo(history.getCurrentLocation())

如果是Hash模式,則執行

const setupHashListener = () => {
 history.setupListeners()
 }
 history.transitionTo(
 history.getCurrentLocation(),setupHashListener
 )

可以看出,兩種模式都執行了transitionTo( )函式。 接下來看一下兩種模式分別是怎麼執行的,首先看一下Hash模式

HashHistory.push()

我們來看HashHistory中的push()方法:

push (location: RawLocation,onAbort?: Function) {
 this.transitionTo(location,route => {
 pushHash(route.fullPath)
 onComplete && onComplete(route)
 },onAbort)
}

function pushHash (path) {
 window.location.hash = path
}

transitionTo()方法是父類中定義的是用來處理路由變化中的基礎邏輯的,push()方法最主要的是對window的hash進行了直接賦值:

window.location.hash = route.fullPath hash的改變會自動新增到瀏覽器的訪問歷史記錄中。

那麼檢視的更新是怎麼實現的呢,我們來看父類History中transitionTo()方法的這麼一段:

transitionTo (location: RawLocation,onAbort?: Function) {
 // 呼叫 match 得到匹配的 route 物件
 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
}

可以看到,當路由變化時,呼叫了History中的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
 })
 })
}

程式碼中的app指的是Vue的例項,._route本不是本身的元件中定義的內建屬性,而是在Vue.use(Router)載入vue-router外掛的時候,通過Vue.mixin()方法,全域性註冊一個混合,影響註冊之後所有建立的每個 Vue 例項,該混合在beforeCreate鉤子中通過Vue.util.defineReactive()定義了響應式的_route。所謂響應式屬性,即當_route值改變時,會自動呼叫Vue例項的render()方法,更新檢視。vm.render()是根據當前的 _route 的path,name等屬性,來將路由對應的元件渲染到. 所以總結下來,從路由改變到檢視的更新流程如下:

this.$router.push(path)
 --> 
HashHistory.push() 
--> 
History.transitionTo() 
--> 
const route = this.router.match(location,this.current)會進行地址匹配,得到一個對應當前地址的route(路由資訊物件)
-->
History.updateRoute(route) 
 -->
 app._route=route (Vue例項的_route改變) 由於_route屬性是採用vue的資料劫持,當_route的值改變時,會執行響應的render( )
-- >
vm.render() 具體是在<router-view></router-view> 中render
 -->
window.location.hash = route.fullpath (瀏覽器位址列顯示新的路由的path)

HashHistory.replace()

說完了HashHistory.push(),該說HashHistory.replace()了。

replace (location: RawLocation,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
 )
}

可以看出來,HashHistory.replace它與push()的實現結構上基本相似,不同點在於它不是直接對window.location.hash進行賦值,而是呼叫window.location.replace方法將路由進行替換。這樣不會將新路由新增到瀏覽器訪問歷史的棧頂,而是替換掉當前的路由。

監聽位址列

可以看出來,上面的過程都是在程式碼內部進行路由的改變的,比如專案中常見的this.$router.push(),等方法。然後將瀏覽器的位址列置為新的hash值。那麼如果直接在位址列中輸入URL從而改變路由呢,例如

淺析vue-router實現原理及兩種模式

我將dashboadr刪除,然後置為article/hotSpot,然後回車,vue又是如何處理的呢?

setupListeners () {
 window.addEventListener('hashchange',() => {
 if (!ensureSlash()) {
 return
 }
 this.transitionTo(getHash(),route => {
 replaceHash(route.fullPath)
 })
 })
}

該方法設定監聽了瀏覽器事件hashchange,呼叫的函式為replaceHash,即在瀏覽器位址列中直接輸入路由相當於程式碼呼叫了replace()方法.後面的步驟自然與HashHistory.replace()相同,一樣實現頁面渲染。

HTML5History

HTML5History模式的vue-router 程式碼結構以及更新檢視的邏輯與hash模式基本類似,和HashHistory的步驟基本一致,只是HashHistory的push和replace()變成了HTML5History.pushState()和HTML5History.replaceState()

在HTML5History中新增對修改瀏覽器位址列URL的監聽是直接在建構函式中執行的,對HTML5History的popstate 事件進行監聽:

constructor (router: Router,base: ?string) {
 
 window.addEventListener('popstate',e => {
 const current = this.current
 this.transitionTo(getLocation(this.base),route => {
 if (expectScroll) {
 handleScroll(router,route,current,true)
 }
 })
 })
}

以上就是vue-router hash模式與history模式不同模式下處理邏輯的分析了。

總結

以上所述是小編給大家介紹的vue-router實現原理及兩種模式分析,希望對大家有所幫助!