淺析vue-router實(shí)現(xiàn)原理及兩種模式
之前用Vue開(kāi)發(fā)單頁(yè)應(yīng)用,發(fā)現(xiàn)不管路由怎么變化,瀏覽器地址欄總是會(huì)有一個(gè)'#'號(hào)。
當(dāng)時(shí)檢查自己的代碼,沒(méi)有發(fā)現(xiàn)請(qǐng)求的地址帶'#',當(dāng)時(shí)也很納悶,但是由于沒(méi)有影響頁(yè)面的渲染以及向后臺(tái)發(fā)送請(qǐng)求,當(dāng)時(shí)也沒(méi)有在意。最近看了一下vue-router的實(shí)現(xiàn)原理,才逐漸揭開(kāi)了這個(gè)謎題。
vue-router 的兩種方式(瀏覽器環(huán)境下)
1. Hash (對(duì)應(yīng)HashHistory)
hash(“#”)符號(hào)的本來(lái)作用是加在URL中指示網(wǎng)頁(yè)中的位置:
http://www.example.com/index.html#print
#符號(hào)本身以及它后面的字符稱(chēng)之為hash(也就是我之前為什么地址欄都會(huì)有一個(gè)‘#'),可通過(guò)window.location.hash屬性讀取。它具有如下特點(diǎn):
hash雖然出現(xiàn)在URL中,但不會(huì)被包括在HTTP請(qǐng)求中。它是用來(lái)指導(dǎo)瀏覽器動(dòng)作的,對(duì)服務(wù)器端完全無(wú)用,因此,改變hash不會(huì)重新加載頁(yè)面
2.可以為hash的改變添加監(jiān)聽(tīng)事件:
window.addEventListener("hashchange", funcRef, false)
每一次改變hash(window.location.hash),都會(huì)在瀏覽器的訪(fǎng)問(wèn)歷史中增加一個(gè)記錄
利用hash的以上特點(diǎn),就可以來(lái)實(shí)現(xiàn)前端路由“更新視圖但不重新請(qǐng)求頁(yè)面”的功能了。
2. History (對(duì)應(yīng)HTML5History)
History接口 是瀏覽器歷史記錄棧提供的接口,通過(guò)back(), forward(), go()等方法,我們可以讀取瀏覽器歷史記錄棧的信息,進(jìn)行各種跳轉(zhuǎn)操作。
從HTML5開(kāi)始,History interface提供了兩個(gè)新的方法:pushState(), replaceState()使得我們可以對(duì)瀏覽器歷史記錄棧進(jìn)行修改:
window.history.pushState(stateObject, title, URL) window.history.replaceState(stateObject, title, URL)
stateObject: 當(dāng)瀏覽器跳轉(zhuǎn)到新的狀態(tài)時(shí),將觸發(fā)popState事件,該事件將攜帶這個(gè)stateObject參數(shù)的副本 title: 所添加記錄的標(biāo)題 URL: 所添加記錄的URL
這兩個(gè)方法有個(gè)共同的特點(diǎn):當(dāng)調(diào)用他們修改瀏覽器歷史記錄棧后,雖然當(dāng)前URL改變了,但瀏覽器不會(huì)刷新頁(yè)面,這就為單頁(yè)應(yīng)用前端路由“更新視圖但不重新請(qǐng)求頁(yè)面”提供了基礎(chǔ)。 瀏覽器歷史記錄可以看作一個(gè)「棧」。棧是一種后進(jìn)先出的結(jié)構(gòu),可以把它想象成一摞盤(pán)子,用戶(hù)每點(diǎn)開(kāi)一個(gè)新網(wǎng)頁(yè),都會(huì)在上面加一個(gè)新盤(pán)子,叫「入棧」。用戶(hù)每次點(diǎn)擊「后退」按鈕都會(huì)取走最上面的那個(gè)盤(pán)子,叫做「出?!?。而每次瀏覽器顯示的自然是最頂端的盤(pán)子的內(nèi)容。
vue-router 的作用
vue-router的作用就是通過(guò)改變URL,在不重新請(qǐng)求頁(yè)面的情況下,更新頁(yè)面視圖。簡(jiǎn)單的說(shuō)就是,雖然地址欄的地址改變了,但是并不是一個(gè)全新的頁(yè)面,而是之前的頁(yè)面某些部分進(jìn)行了修改。
export default new Router({ // mode: 'history', //后端支持可開(kāi) routes: constantRouterMap })
這是Vue項(xiàng)目中常見(jiàn)的一段初始化vue-router的代碼,之前沒(méi)仔細(xì)研究過(guò)vue-router,不知道還有一個(gè)mode屬性,后來(lái)看了相關(guān)文章后了解到,mode屬性用來(lái)指定vue-router使用哪一種模式。在沒(méi)有指定mode的值,則使用hash模式。
源碼分析
首先看一下vue-router的構(gòu)造函數(shù)
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': //非瀏覽器環(huán)境下 this.history = new AbstractHistory(this, options.base) break default: if (process.env.NODE_ENV !== 'production') { assert(false, `invalid mode: ${mode}`) } } }
主要是先獲取mode的值,如果mode的值為 history 但是瀏覽器不支持 history 模式,那么就強(qiáng)制設(shè)置mode值為 hash 。如果支持則為 history 。接下來(lái),根據(jù)mode的值,來(lái)選擇vue-router使用哪種模式。
case 'history': this.history = new HTML5History(this, options.base) break case 'hash': this.history = new HashHistory(this, options.base, this.fallback) break
這樣就有了兩種模式。確定好了vue-router使用哪種模式后,就到了init。 先來(lái)看看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類(lèi)暴露的以下方法實(shí)際是調(diào)用具體history對(duì)象的方法 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) } }
如果是HTML5History,則執(zhí)行
history.transitionTo(history.getCurrentLocation())
如果是Hash模式,則執(zhí)行
const setupHashListener = () => { history.setupListeners() } history.transitionTo( history.getCurrentLocation(), setupHashListener, setupHashListener )
可以看出,兩種模式都執(zhí)行了transitionTo( )函數(shù)。 接下來(lái)看一下兩種模式分別是怎么執(zhí)行的,首先看一下Hash模式
HashHistory.push()
我們來(lái)看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 }
transitionTo()方法是父類(lèi)中定義的是用來(lái)處理路由變化中的基礎(chǔ)邏輯的,push()方法最主要的是對(duì)window的hash進(jìn)行了直接賦值:
window.location.hash = route.fullPath hash
的改變會(huì)自動(dòng)添加到瀏覽器的訪(fǎng)問(wèn)歷史記錄中。
那么視圖的更新是怎么實(shí)現(xiàn)的呢,我們來(lái)看父類(lèi)History中transitionTo()方法的這么一段:
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) { // 調(diào)用 match 得到匹配的 route 對(duì)象 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 }
可以看到,當(dāng)路由變化時(shí),調(diào)用了History中的this.cb方法,而this.cb方法是通過(guò)History.listen(cb)進(jìn)行設(shè)置的?;氐絍ueRouter類(lèi)定義中,找到了在init()方法中對(duì)其進(jìn)行了設(shè)置:
init (app: any /* Vue component instance */) { this.apps.push(app) history.listen(route => { this.apps.forEach((app) => { app._route = route }) }) }
代碼中的app指的是Vue的實(shí)例,._route本不是本身的組件中定義的內(nèi)置屬性,而是在Vue.use(Router)加載vue-router插件的時(shí)候,通過(guò)Vue.mixin()方法,全局注冊(cè)一個(gè)混合,影響注冊(cè)之后所有創(chuàng)建的每個(gè) Vue 實(shí)例,該混合在beforeCreate鉤子中通過(guò)Vue.util.defineReactive()
定義了響應(yīng)式的_route。所謂響應(yīng)式屬性,即當(dāng)_route值改變時(shí),會(huì)自動(dòng)調(diào)用Vue實(shí)例的render()方法,更新視圖。vm.render()是根據(jù)當(dāng)前的 _route 的path,name等屬性,來(lái)將路由對(duì)應(yīng)的組件渲染到. 所以總結(jié)下來(lái),從路由改變到視圖的更新流程如下:
this.$router.push(path) --> HashHistory.push() --> History.transitionTo() --> const route = this.router.match(location, this.current)會(huì)進(jìn)行地址匹配,得到一個(gè)對(duì)應(yīng)當(dāng)前地址的route(路由信息對(duì)象) --> History.updateRoute(route) --> app._route=route (Vue實(shí)例的_route改變) 由于_route屬性是采用vue的數(shù)據(jù)劫持,當(dāng)_route的值改變時(shí),會(huì)執(zhí)行響應(yīng)的render( ) -- > vm.render() 具體是在<router-view></router-view> 中render --> window.location.hash = route.fullpath (瀏覽器地址欄顯示新的路由的path)
HashHistory.replace()
說(shuō)完了HashHistory.push(),該說(shuō)HashHistory.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 ) }
可以看出來(lái),HashHistory.replace它與push()的實(shí)現(xiàn)結(jié)構(gòu)上基本相似,不同點(diǎn)在于它不是直接對(duì)window.location.hash
進(jìn)行賦值,而是調(diào)用window.location.replace方法將路由進(jìn)行替換。這樣不會(huì)將新路由添加到瀏覽器訪(fǎng)問(wèn)歷史的棧頂,而是替換掉當(dāng)前的路由。
監(jiān)聽(tīng)地址欄
可以看出來(lái),上面的過(guò)程都是在代碼內(nèi)部進(jìn)行路由的改變的,比如項(xiàng)目中常見(jiàn)的this.$router.push(),
等方法。然后將瀏覽器的地址欄置為新的hash值。那么如果直接在地址欄中輸入U(xiǎn)RL從而改變路由呢,例如
我將dashboadr刪除,然后置為article/hotSpot,然后回車(chē),vue又是如何處理的呢?
setupListeners () { window.addEventListener('hashchange', () => { if (!ensureSlash()) { return } this.transitionTo(getHash(), route => { replaceHash(route.fullPath) }) }) }
該方法設(shè)置監(jiān)聽(tīng)了瀏覽器事件hashchange,調(diào)用的函數(shù)為replaceHash,即在瀏覽器地址欄中直接輸入路由相當(dāng)于代碼調(diào)用了replace()方法.后面的步驟自然與HashHistory.replace()相同,一樣實(shí)現(xiàn)頁(yè)面渲染。
HTML5History
HTML5History模式的vue-router 代碼結(jié)構(gòu)以及更新視圖的邏輯與hash模式基本類(lèi)似,和HashHistory的步驟基本一致,只是HashHistory的push和replace()變成了HTML5History.pushState()和HTML5History.replaceState()
在HTML5History中添加對(duì)修改瀏覽器地址欄URL的監(jiān)聽(tīng)是直接在構(gòu)造函數(shù)中執(zhí)行的,對(duì)HTML5History的popstate 事件進(jìn)行監(jiān)聽(tīng):
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模式不同模式下處理邏輯的分析了。
總結(jié)
以上所述是小編給大家介紹的vue-router實(shí)現(xiàn)原理及兩種模式分析,希望對(duì)大家有所幫助!
相關(guān)文章
解決axios post 后端無(wú)法接收數(shù)據(jù)的問(wèn)題
今天小編就為大家分享一篇解決axios post 后端無(wú)法接收數(shù)據(jù)的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-10-10vue數(shù)據(jù)監(jiān)聽(tīng)解析Object.defineProperty與Proxy區(qū)別
這篇文章主要為大家介紹了vue數(shù)據(jù)監(jiān)聽(tīng)解析Object.defineProperty Proxy源碼示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03Spring Boot/VUE中路由傳遞參數(shù)的實(shí)現(xiàn)代碼
在路由時(shí)傳遞參數(shù),一般有兩種形式,一種是拼接在url地址中,另一種是查詢(xún)參數(shù)。這篇文章主要介紹了Spring Boot/VUE中路由傳遞參數(shù),需要的朋友可以參考下2018-03-03vue.js 圖片上傳并預(yù)覽及圖片更換功能的實(shí)現(xiàn)代碼
這篇文章主要介紹了vue.js 圖片上傳并預(yù)覽及圖片更換功能,小編主要圍繞我們?nèi)粘J褂霉δ艿睦幼鲋v解,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-08-08關(guān)于Nuxt的五種渲染模式的差異和使用場(chǎng)景全解析
這篇文章主要介紹了關(guān)于Nuxt的五種渲染模式的差異和使用場(chǎng)景全解析,在過(guò)去傳統(tǒng)開(kāi)發(fā)中,頁(yè)面渲染任務(wù)是由服務(wù)端完成的,那么Nuxt是如何渲染的呢,需要的朋友可以參考下2023-04-04Vue中computed屬性和watch,methods的區(qū)別
本文主要介紹了Vue中computed屬性和watch,methods的區(qū)別,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-05-05如何解決element-ui動(dòng)態(tài)加載級(jí)聯(lián)選擇器默認(rèn)選中問(wèn)題
這篇文章主要介紹了如何解決element-ui動(dòng)態(tài)加載級(jí)聯(lián)選擇器默認(rèn)選中問(wèn)題,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的朋友可以參考一下2022-09-09解決vue點(diǎn)擊控制單個(gè)樣式的問(wèn)題
今天小編就為大家分享一篇解決vue點(diǎn)擊控制單個(gè)樣式的問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-09-09Vue中使用vue2-perfect-scrollbar制作滾動(dòng)條
這篇文章主要介紹了Vue中使用vue2-perfect-scrollbar滾動(dòng)條,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-06-06Vue?3?表格時(shí)間監(jiān)控與動(dòng)態(tài)后端請(qǐng)求觸發(fā)詳解?附Demo展示
在Vue3中,使用el-table組件渲染表格數(shù)據(jù),通過(guò)el-table-column指定內(nèi)容,時(shí)間點(diǎn)需前端校準(zhǔn),用getTime()比較,到達(dá)時(shí)觸發(fā)操作,異步API請(qǐng)求可用async/await處理,setInterval實(shí)現(xiàn)定時(shí)監(jiān)控,配合條件判斷防止重復(fù)請(qǐng)求2024-09-09