前端(vue)入門到精通課程,老師在線輔導(dǎo):聯(lián)系老師
Apipost = Postman + Swagger + Mock + Jmeter 超好用的API調(diào)試工具:點(diǎn)擊使用
路由的概念相信大部分同學(xué)并不陌生,我們?cè)谟?Vue
開發(fā)過實(shí)際項(xiàng)目的時(shí)候都會(huì)用到 Vue-Router
這個(gè)官方插件來幫我們解決路由的問題。它的作用就是根據(jù)不同的路徑映射到不同的視圖。本文不再講述路由的基礎(chǔ)使用和API
,不清楚的同學(xué)可以自行查閱官方文檔vue-router3 對(duì)應(yīng) vue2 和 vue-router4 對(duì)應(yīng) vue3。
今天我們主要是談?wù)?code>Vue-Router的實(shí)現(xiàn)原理,感興趣的小伙伴可以繼續(xù)往下看,大佬請(qǐng)止步。
本文 vue-router 版本為 3.5.3
路由
既然我們?cè)诜治雎酚?,我們首先來說說什么是路由,什么是后端路由、什么是前端路由。
路由就是根據(jù)不同的 url
地址展示不同的內(nèi)容或頁面,早期路由的概念是在后端出現(xiàn)的,通過服務(wù)器端渲染后返回頁面,隨著頁面越來越復(fù)雜,服務(wù)器端壓力越來越大。后來ajax
異步刷新的出現(xiàn)使得前端也可以對(duì)url
進(jìn)行管理,此時(shí),前端路由就出現(xiàn)了。(學(xué)習(xí)視頻分享:web前端開發(fā)、編程基礎(chǔ)視頻)
我們先來說說后端路由
后端路由
后端路由又可稱之為服務(wù)器端路由,因?yàn)閷?duì)于服務(wù)器來說,當(dāng)接收到客戶端發(fā)來的HTTP
請(qǐng)求,就會(huì)根據(jù)所請(qǐng)求的URL
,來找到相應(yīng)的映射函數(shù),然后執(zhí)行該函數(shù),并將函數(shù)的返回值發(fā)送給客戶端。
對(duì)于最簡單的靜態(tài)資源服務(wù)器,可以認(rèn)為,所有URL
的映射函數(shù)就是一個(gè)文件讀取操作。 對(duì)于動(dòng)態(tài)資源,映射函數(shù)可能是一個(gè)數(shù)據(jù)庫讀取操作,也可能是進(jìn)行一些數(shù)據(jù)的處理,等等。
然后根據(jù)這些讀取的數(shù)據(jù),在服務(wù)器端就使用相應(yīng)的模板來對(duì)頁面進(jìn)行渲染后,再返回渲染完畢的HTML
頁面。早期的jsp
就是這種模式。
前端路由
剛剛也介紹了,在前后端沒有分離的時(shí)候,服務(wù)端都是直接將整個(gè) HTML
返回,用戶每次一個(gè)很小的操作都會(huì)引起頁面的整個(gè)刷新(再加上之前的網(wǎng)速還很慢,所以用戶體驗(yàn)可想而知)。
在90年代末的時(shí)候,微軟首先實(shí)現(xiàn)了 ajax(Asynchronous JavaScript And XML)
這個(gè)技術(shù),這樣用戶每次的操作就可以不用刷新整個(gè)頁面了,用戶體驗(yàn)就大大提升了。
雖然數(shù)據(jù)能異步獲取不用每個(gè)點(diǎn)擊都去請(qǐng)求整個(gè)網(wǎng)頁,但是頁面之間的跳轉(zhuǎn)還是會(huì)加載整個(gè)網(wǎng)頁,體驗(yàn)不是特別好,還有沒有更好的方法呢?
至此異步交互體驗(yàn)的更高級(jí)版本 SPA單頁應(yīng)用
就出現(xiàn)了。單頁應(yīng)用不僅僅是在頁面交互是無刷新的,連頁面跳轉(zhuǎn)都是無刷新的。既然頁面的跳轉(zhuǎn)是無刷新的,也就是不再向后端請(qǐng)求返回 HTML
頁面。
頁面跳轉(zhuǎn)都不從后端獲取新的HTML
頁面,那應(yīng)該怎么做呢?所以就有了現(xiàn)在的前端路由。
可以理解為,前端路由就是將之前服務(wù)端根據(jù) url 的不同返回不同的頁面的任務(wù)交給前端來做。在這個(gè)過程中,js會(huì)實(shí)時(shí)檢測url的變化,從而改變顯示的內(nèi)容。
前端路由優(yōu)點(diǎn)是用戶體驗(yàn)好,用戶操作或頁面跳轉(zhuǎn)不會(huì)刷新頁面,并且能快速展現(xiàn)給用戶。缺點(diǎn)是首屏加載慢,因?yàn)樾枰?code>js動(dòng)態(tài)渲染展示內(nèi)容。而且由于內(nèi)容是js
動(dòng)態(tài)渲染的所以不利于SEO
。
下面我們正式進(jìn)入Vue-Router
原理分析階段。
分析Vue-Router.install方法
我們先來看看install.js
,這個(gè)方法會(huì)在Vue.use(VueRouter)
的時(shí)候被調(diào)用。
// install.js import View from './components/view' import Link from './components/link' export let _Vue export function install (Vue) { // 不會(huì)重復(fù)安裝 if (install.installed && _Vue === Vue) return install.installed = true _Vue = Vue const isDef = v => v !== undefined // 為router-view組件關(guān)聯(lián)路由組件 const registerInstance = (vm, callVal) => { let i = vm.$options._parentVnode // 調(diào)用vm.$options._parentVnode.data.registerRouteInstance方法 // 而這個(gè)方法只在router-view組件中存在,router-view組件定義在(../components/view.js @71行) // 所以,如果vm的父節(jié)點(diǎn)為router-view,則為router-view關(guān)聯(lián)當(dāng)前vm,即將當(dāng)前vm做為router-view的路由組件 if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) { i(vm, callVal) } } Vue.mixin({ beforeCreate () { // 這里只會(huì)進(jìn)來一次,因?yàn)橹挥蠽ue根實(shí)例才會(huì)有router屬性。 if (isDef(this.$options.router)) { // 所以這里的this就是Vue根實(shí)例 this._routerRoot = this this._router = this.$options.router this._router.init(this) // 將 _route 變成響應(yīng)式 Vue.util.defineReactive(this, '_route', this._router.history.current) } else { // 子組件會(huì)進(jìn)入這里,這里也是把Vue根實(shí)例保存帶_routerRoot屬性上 this._routerRoot = (this.$parent && this.$parent._routerRoot) || this } // 為router-view組件關(guān)聯(lián)路由組件 registerInstance(this, this) }, destroyed () { // destroyed hook觸發(fā)時(shí),取消router-view和路由組件的關(guān)聯(lián) registerInstance(this) } }) // 在原型上注入$router、$route屬性,方便快捷訪問 Object.defineProperty(Vue.prototype, '$router', { // 上面說到每個(gè)組件的_routerRoot都是Vue根實(shí)例,所以都能訪問_router get () { return this._routerRoot._router } }) // 每個(gè)組件訪問到的$route,其實(shí)最后訪問的都是Vue根實(shí)例的_route Object.defineProperty(Vue.prototype, '$route', { get () { return this._routerRoot._route } }) // 注冊(cè)router-view、router-link兩個(gè)全局組件 Vue.component('RouterView', View) Vue.component('RouterLink', Link) const strats = Vue.config.optionMergeStrategies // use the same hook merging strategy for route hooks strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created }
主要做了如下幾件事情:
避免重復(fù)安裝
為了確保 install
邏輯只執(zhí)行一次,用了 install.installed
變量做已安裝的標(biāo)志位。
傳遞Vue引用減少打包體積
用一個(gè)全局的 _Vue
來接收參數(shù) Vue
,因?yàn)樽鳛?Vue
的插件對(duì) Vue
對(duì)象是有依賴的,但又不能去單獨(dú)去 import Vue
,因?yàn)槟菢訒?huì)增加包體積,所以就通過這種方式拿到 Vue
對(duì)象。
注冊(cè)全局混入
Vue-Router
安裝最重要的一步就是利用 Vue.mixin
,在beforeCreate
和destroyed
生命周期函數(shù)中注入路由邏輯。
Vue.mixin
我們知道就是全局 mixin
,所以也就相當(dāng)于每個(gè)組件的beforeCreate
和destroyed
生命周期函數(shù)中都會(huì)有這些代碼,并在每個(gè)組件中都會(huì)運(yùn)行。
Vue.mixin({ beforeCreate () { if (isDef(this.$options.router)) { this._routerRoot = this this._router = this.$options.router this._router.init(this) Vue.util.defineReactive(this, '_route', this._router.history.current) } else { this._routerRoot = (this.$parent && this.$parent._routerRoot) || this } registerInstance(this, this) }, destroyed () { registerInstance(this) } })
在這兩個(gè)鉤子中,this
是指向當(dāng)時(shí)正在調(diào)用鉤子的vue實(shí)例
。
這兩個(gè)鉤子中的邏輯,在安裝流程中是不會(huì)被執(zhí)行的,只有在組件實(shí)例化時(shí)執(zhí)行到鉤子時(shí)才會(huì)被調(diào)用
先看混入的 beforeCreate
鉤子函數(shù)
它先判斷了this.$options.router
是否存在,我們?cè)?code>new Vue({router})時(shí),router
才會(huì)被保存到到Vue根實(shí)例
的$options
上,而其它Vue實(shí)例
的$options
上是沒有router
的,所以if
中的語句只在this === new Vue({router})
時(shí),才會(huì)被執(zhí)行,由于Vue根實(shí)例
只有一個(gè),所以這個(gè)邏輯只會(huì)被執(zhí)行一次。
對(duì)于根 Vue
實(shí)例而言,執(zhí)行該鉤子函數(shù)時(shí)定義了 this._routerRoot
表示它自身(Vue
根實(shí)例);this._router
表示 VueRouter
的實(shí)例 router
,它是在 new Vue
的時(shí)候傳入的;
另外執(zhí)行了 this._router.init()
方法初始化 router
,這個(gè)邏輯在后面講初始化的時(shí)候再介紹。
然后用 defineReactive
方法把 this._route
變成響應(yīng)式對(duì)象,保證_route
變化時(shí),router-view
會(huì)重新渲染,這個(gè)我們后面在router-view
組件中會(huì)細(xì)講。
我們?cè)倏聪?code>else中具體干了啥
主要是為每個(gè)組件定義_routerRoot
,對(duì)于子組件而言,由于組件是樹狀結(jié)構(gòu),在遍歷組件樹的過程中,它們?cè)趫?zhí)行該鉤子函數(shù)的時(shí)候 this._routerRoot
始終指向的離它最近的傳入了 router
對(duì)象作為配置而實(shí)例化的父實(shí)例(也就是永遠(yuǎn)等于根實(shí)例)。
所以我們可以得到,在每個(gè)vue
組件都有 this._routerRoot === vue根實(shí)例
、this._routerRoot._router === router對(duì)象
對(duì)于 beforeCreate
和 destroyed
鉤子函數(shù),它們都會(huì)執(zhí)行 registerInstance
方法,這個(gè)方法的作用我們也是之后會(huì)介紹。
添加$route、$router
屬性
接著給 Vue
原型上定義了 $router
和 $route
2 個(gè)屬性的 get
方法,這就是為什么我們可以在任何組件實(shí)例上都可以訪問 this.$router
以及 this.$route
。
Object.defineProperty(Vue.prototype, '$router', { get () { return this._routerRoot._router } }) Object.defineProperty(Vue.prototype, '$route', { get () { return this._routerRoot._route } })
我們可以看到,$router
其實(shí)返回的是this._routerRoot._router
,也就是vue
根實(shí)例上的router
,因此我們可以通過this.$router
來使用router
的各種方法。
$route
其實(shí)返回的是this._routerRoot._route
,其實(shí)就是this._router.history.current
,也就是目前的路由對(duì)象,這個(gè)后面會(huì)細(xì)說。
注冊(cè)全局組件
通過 Vue.component
方法定義了全局的 <router-link>
和 <router-view>
2 個(gè)組件,這也是為什么我們?cè)趯懩0宓臅r(shí)候可以直接使用這兩個(gè)標(biāo)簽,它們的作用我想就不用筆者再說了吧。
鉤子函數(shù)的合并策略
最后設(shè)置路由組件的beforeRouteEnter
、beforeRouteLeave
、beforeRouteUpdate
守衛(wèi)的合并策略。
總結(jié)
那么到此為止,我們分析了 Vue-Router
的安裝過程,Vue
編寫插件的時(shí)候通常要提供靜態(tài)的 install
方法,我們通過 Vue.use(plugin)
時(shí)候,就是在執(zhí)行 install
方法。Vue-Router
的 install
方法會(huì)給每一個(gè)組件注入 beforeCreate
和 destoryed
鉤子函數(shù),在beforeCreate
做一些私有屬性定義和路由初始化工作。并注冊(cè)了兩個(gè)全局組件,然后設(shè)置了鉤子函數(shù)合并策略。在destoryed
做了一些銷毀工作。
下面我們?cè)賮砜纯?code>Vue-Router的實(shí)例化。
分析init方法
前面我們提到了在 install
的時(shí)候會(huì)執(zhí)行 VueRouter
的 init
方法( this._router.init(this)
),那么接下來我們就來看一下 init
方法做了什么。
init (app: any /* Vue component instance */) { // ... this.apps.push(app) // ... // main app previously initialized // return as we don't need to set up new history listener if (this.app) { return } this.app = app const history = this.history if (history instanceof HTML5History || history instanceof HashHistory) { const handleInitialScroll = routeOrError => { const from = history.current const expectScroll = this.options.scrollBehavior const supportsScroll = supportsPushState && expectScroll if (supportsScroll && 'fullPath' in routeOrError) { handleScroll(this, routeOrError, from, false) } } // 1.setupListeners 里會(huì)對(duì) hashchange或popstate事件進(jìn)行監(jiān)聽 const setupListeners = routeOrError => { history.setupListeners() handleInitialScroll(routeOrError) } // 2.初始化導(dǎo)航 history.transitionTo( history.getCurrentLocation(), setupListeners, setupListeners ) } // 3.路由全局監(jiān)聽,維護(hù)當(dāng)前的route // 當(dāng)路由變化的時(shí)候修改app._route的值 // 由于_route是響應(yīng)式的,所以修改后相應(yīng)視圖會(huì)同步更新 history.listen(route => { this.apps.forEach(app => { app._route = route }) }) }
這里主要做了如下幾件事情:
設(shè)置了路由監(jiān)聽
const setupListeners = routeOrError => { history.setupListeners() handleInitialScroll(routeOrError) }
這里會(huì)根據(jù)當(dāng)前路由模式監(jiān)聽hashchange
或popstate
事件,當(dāng)事件觸發(fā)的時(shí)候,會(huì)進(jìn)行路由的跳轉(zhuǎn)。(后面說到路由模式的時(shí)候會(huì)細(xì)說)
初始化導(dǎo)航
history.transitionTo( history.getCurrentLocation(), setupListeners, setupListeners )
進(jìn)入系統(tǒng)會(huì)進(jìn)行初始化路由匹配,渲染對(duì)應(yīng)的組件。因?yàn)榈谝淮芜M(jìn)入系統(tǒng),并不會(huì)觸發(fā)hashchange
或者popstate
事件,所以第一次需要自己手動(dòng)匹配路徑然后進(jìn)行跳轉(zhuǎn)。
路由全局監(jiān)聽
history.listen(route => { this.apps.forEach(app => { app._route = route }) })
當(dāng)路由變化的時(shí)候修改app._route
的值。由于_route
是響應(yīng)式的,所以修改后相應(yīng)視圖會(huì)同步更新。
總結(jié)
這里主要是做了一些初始化工作。根據(jù)當(dāng)前路由模式監(jiān)聽對(duì)應(yīng)的路由事件。初始化導(dǎo)航,根據(jù)當(dāng)前的url渲染初始頁面。最后切換路由的時(shí)候修改_route
,由于_route
是響應(yīng)式的,所以修改后相應(yīng)視圖會(huì)同步更新。
分析VueRouter實(shí)例化
實(shí)例化就是我們new VueRouter({routes})
的過程,我們來重點(diǎn)分析下VueRouter
的構(gòu)造函數(shù)。
constructor (options: RouterOptions = {}) { // ... // 參數(shù)初始化 this.app = null this.apps = [] this.options = options this.beforeHooks = [] this.resolveHooks = [] this.afterHooks = [] // 創(chuàng)建matcher this.matcher = createMatcher(options.routes || [], this) // 設(shè)置默認(rèn)模式和做不支持 H5 history 的降級(jí)處理 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 // 根據(jù)不同的 mode 實(shí)例化不同的 History 對(duì)象 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}`) } } }
這里主要做了如下幾件事情:
初始化參數(shù)
我們看到在最開始有些參數(shù)的初始化,這些參數(shù)到底是什么呢?
this.app
用來保存根 Vue
實(shí)例。
this.apps
用來保存持有 $options.router
屬性的 Vue
實(shí)例。
this.options
保存?zhèn)魅氲穆酚膳渲茫簿褪乔懊嬲f的RouterOptions
。
this.beforeHooks
、 this.resolveHooks
、this.afterHooks
表示一些鉤子函數(shù)。
this.fallback
表示在瀏覽器不支持 history
新api
的情況下,根據(jù)傳入的 fallback
配置參數(shù),決定是否回退到hash
模式。
this.mode
表示路由創(chuàng)建的模式。
創(chuàng)建matcher
matcher
,匹配器。簡單理解就是可以通過url
找到我們對(duì)應(yīng)的組件。這一塊內(nèi)容較多,這里筆者就不再詳細(xì)分析了。
確定路由模式
路由模式平時(shí)都會(huì)只說兩種,其實(shí)在vue-router
總共實(shí)現(xiàn)了 hash
、history
、abstract
3 種模式。
VueRouter
會(huì)根據(jù)options.mode
、options.fallback
、supportsPushState
、inBrowser
來確定最終的路由模式。
如果沒有設(shè)置mode
就默認(rèn)是hash
模式。
確定fallback
值,只有在用戶設(shè)置了mode:history
并且當(dāng)前環(huán)境不支持pushState
且用戶沒有主動(dòng)聲明不需要回退(沒設(shè)置fallback
值位undefined
),此時(shí)this.fallback
才為true
,當(dāng)fallback
為true
時(shí)會(huì)使用hash
模式。(簡單理解就是如果不支持history
模式并且只要沒設(shè)置fallback
為false
,就會(huì)啟用hash
模式)
如果最后發(fā)現(xiàn)處于非瀏覽器環(huán)境,則會(huì)強(qiáng)制使用abstract
模式。
實(shí)例化路由模式
根據(jù)mode
屬性值來實(shí)例化不同的對(duì)象。VueRouter
的三種路由模式,主要由下面的四個(gè)核心類實(shí)現(xiàn)
-
History
- 基礎(chǔ)類
- 位于
src/history/base.js
-
HTML5History
- 用于支持
pushState
的瀏覽器 src/history/html5.js
- 用于支持
-
HashHistory
- 用于不支持
pushState
的瀏覽器 src/history/hash.js
- 用于不支持
-
AbstractHistory
- 用于非瀏覽器環(huán)境(服務(wù)端渲染)
src/history/abstract.js
HTML5History
、HashHistory
、AbstractHistory
三者都是繼承于基礎(chǔ)類History
。
這里我們?cè)敿?xì)分析下HTML5History
和HashHistory
類。
HTML5History類
當(dāng)我們使用history
模式的時(shí)候會(huì)實(shí)例化HTML5History類
// src/history/html5.js ... export class HTML5History extends History { _startLocation: string constructor (router: Router, base: ?string) { // 調(diào)用父類構(gòu)造函數(shù)初始化 super(router, base) this._startLocation = getLocation(this.base) } // 設(shè)置監(jiān)聽,主要是監(jiān)聽popstate方法來自動(dòng)觸發(fā)transitionTo setupListeners () { if (this.listeners.length > 0) { return } const router = this.router const expectScroll = router.options.scrollBehavior const supportsScroll = supportsPushState && expectScroll // 若支持scroll,初始化scroll相關(guān)邏輯 if (supportsScroll) { this.listeners.push(setupScroll()) } const handleRoutingEvent = () => { const current = this.current // 某些瀏覽器,會(huì)在打開頁面時(shí)觸發(fā)一次popstate // 此時(shí)如果初始路由是異步路由,就會(huì)出現(xiàn)`popstate`先觸發(fā),初始路由后解析完成,進(jìn)而導(dǎo)致route未更新 // 所以需要避免 const location = getLocation(this.base) if (this.current === START && location === this._startLocation) { return } // 路由地址發(fā)生變化,則跳轉(zhuǎn),如需滾動(dòng)則在跳轉(zhuǎn)后處理滾動(dòng) this.transitionTo(location, route => { if (supportsScroll) { handleScroll(router, route, current, true) } }) } // 監(jiān)聽popstate事件 window.addEventListener('popstate', handleRoutingEvent) this.listeners.push(() => { window.removeEventListener('popstate', handleRoutingEvent) }) } // 可以看到 history模式go方法其實(shí)是調(diào)用的window.history.go(n) go (n: number) { window.history.go(n) } // push方法會(huì)主動(dòng)調(diào)用transitionTo進(jìn)行跳轉(zhuǎn) 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方法會(huì)主動(dòng)調(diào)用transitionTo進(jìn)行跳轉(zhuǎn) 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) } ensureURL (push?: boolean) { if (getLocation(this.base) !== this.current.fullPath) { const current = cleanPath(this.base + this.current.fullPath) push ? pushState(current) : replaceState(current) } } getCurrentLocation (): string { return getLocation(this.base) } } export function getLocation (base: string): string { let path = window.location.pathname const pathLowerCase = path.toLowerCase() const baseLowerCase = base.toLowerCase() // base="/a" shouldn't turn path="/app" into "/a/pp" // https://github.com/vuejs/vue-router/issues/3555 // so we ensure the trailing slash in the base if (base && ((pathLowerCase === baseLowerCase) || (pathLowerCase.indexOf(cleanPath(baseLowerCase + '/')) === 0))) { path = path.slice(base.length) } return (path || '/') + window.location.search + window.location.hash }
可以看到HTML5History
類主要干了如下幾件事。
-
繼承于
History類
,并調(diào)用父類構(gòu)造函數(shù)初始化。 -
實(shí)現(xiàn)了
setupListeners
方法,在該方法中檢查了是否需要支持滾動(dòng)行為,如果支持,則初始化滾動(dòng)相關(guān)邏輯,監(jiān)聽了popstate事件
,并在popstate
觸發(fā)時(shí)自動(dòng)調(diào)用transitionTo
方法。 -
實(shí)現(xiàn)了
go、push、replace
等方法,我們可以看到,history
模式其實(shí)就是使用的history api
。
// 可以看到 history模式go方法其實(shí)是調(diào)用的window.history.go(n) go (n: number) { window.history.go(n) } // push、replace調(diào)用的是util/push-state.js,里面實(shí)現(xiàn)了push和replace方法 // 實(shí)現(xiàn)原理也是使用的history api,并且在不支持history api的情況下使用location api export function pushState (url?: string, replace?: boolean) { ... const history = window.history try { if (replace) { const stateCopy = extend({}, history.state) stateCopy.key = getStateKey() // 調(diào)用的 history.replaceState history.replaceState(stateCopy, '', url) } else { // 調(diào)用的 history.pushState history.pushState({ key: setStateKey(genStateKey()) }, '', url) } } catch (e) { window.location[replace ? 'replace' : 'assign'](url) } } export function replaceState (url?: string) { pushState(url, true) }
總結(jié)
所以history
模式的原理就是在js
中路由的跳轉(zhuǎn)(也就是使用push
和replace
方法)都是通過history api
,history.pushState
和 history.replaceState
兩個(gè)方法完成,通過這兩個(gè)方法我們知道了路由的變化,然后根據(jù)路由映射關(guān)系來實(shí)現(xiàn)頁面內(nèi)容的更新。
對(duì)于直接點(diǎn)擊瀏覽器的前進(jìn)后退按鈕或者js
調(diào)用 this.$router.go()
、this.$router.forward()
、this.$router.back()
、或者原生js
方法history.back()
、history.go()
、history.forward()
的,都會(huì)觸發(fā)popstate
事件,通過監(jiān)聽這個(gè)事件我們就可以知道路由發(fā)生了哪些變化然后來實(shí)現(xiàn)更新頁面內(nèi)容。
注意history.pushState
和 history.replaceState
這兩個(gè)方法并不會(huì)觸發(fā)popstate
事件。在這兩個(gè)方法里面他是有手動(dòng)調(diào)用transitionTo
方法的。
接下來我們?cè)賮砜纯?strong>HashHistory類
HashHistory類
當(dāng)我們使用hash
模式的時(shí)候會(huì)實(shí)例化HashHistory類
。
//src/history/hash.js ... export class HashHistory extends History { constructor (router: Router, base: ?string, fallback: boolean) { super(router, base) // check history fallback deeplinking if (fallback && checkFallback(this.base)) { return } ensureSlash() } setupListeners () { if (this.listeners.length > 0) { return } const router = this.router const expectScroll = router.options.scrollBehavior const supportsScroll = supportsPushState && expectScroll if (supportsScroll) { this.listeners.push(setupScroll()) } const handleRoutingEvent = () => { const current = this.current if (!ensureSlash()) { return } this.transitionTo(getHash(), route => { if (supportsScroll) { handleScroll(this.router, route, current, true) } if (!supportsPushState) { replaceHash(route.fullPath) } }) } // 事件優(yōu)先使用 popstate // 判斷supportsPushState就是通過return window.history && typeof window.history.pushState === 'function' const eventType = supportsPushState ? 'popstate' : 'hashchange' window.addEventListener( eventType, handleRoutingEvent ) this.listeners.push(() => { window.removeEventListener(eventType, handleRoutingEvent) }) } // 其實(shí)也是優(yōu)先使用history的pushState方法來實(shí)現(xiàn),不支持再使用location修改hash值 push (location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this this.transitionTo( location, route => { pushHash(route.fullPath) handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort ) } // 其實(shí)也是優(yōu)先使用history的replaceState方法來實(shí)現(xiàn),不支持再使用location修改replace方法 replace (location: RawLocation, onComplete?: Function, onAbort?: Function) { const { current: fromRoute } = this this.transitionTo( location, route => { replaceHash(route.fullPath) handleScroll(this.router, route, fromRoute, false) onComplete && onComplete(route) }, onAbort ) } // 也是使用的history go方法 go (n: number) { window.history.go(n) } ensureURL (push?: boolean) { const current = this.current.fullPath if (getHash() !== current) { push ? pushHash(current) : replaceHash(current) } } getCurrentLocation () { return getHash() } } function checkFallback (base) { const location = getLocation(base) if (!/^/#/.test(location)) { window.location.replace(cleanPath(base + '/#' + location)) return true } } function ensureSlash (): boolean { const path = getHash() if (path.charAt(0) === '/') { return true } replaceHash('/' + path) return false } // 獲取 # 后面的內(nèi)容 export function getHash (): string { // We can't use window.location.hash here because it's not // consistent across browsers - Firefox will pre-decode it! let href = window.location.href const index = href.indexOf('#') // empty path if (index < 0) return '' href = href.slice(index + 1) return href } function getUrl (path) { const href = window.location.href const i = href.indexOf('#') const base = i >= 0 ? href.slice(0, i) : href return `${base}#${path}` } function pushHash (path) { if (supportsPushState) { pushState(getUrl(path)) } else { window.location.hash = path } } function replaceHash (path) { if (supportsPushState) { replaceState(getUrl(path)) } else { window.location.replace(getUrl(path)) } }
可以看到HashHistory
類主要干了如下幾件事。
-
繼承于
History類
,并調(diào)用父類構(gòu)造函數(shù)初始化。這里比HTML5History
多了回退操作,所以,需要將history
模式的url
替換成hash
模式,即添加上#
,這個(gè)邏輯是由checkFallback
實(shí)現(xiàn)的 -
實(shí)現(xiàn)了
setupListeners
方法,在該方法中檢查了是否需要支持滾動(dòng)行為,如果支持,則初始化滾動(dòng)相關(guān)邏輯。 監(jiān)聽了popstate事件或hashchange事件
,并在相應(yīng)事件觸發(fā)時(shí),調(diào)用transitionTo
方法實(shí)現(xiàn)跳轉(zhuǎn)。
通過
const eventType = supportsPushState ? 'popstate' : 'hashchange'
我們可以發(fā)現(xiàn)就算是hash
模式優(yōu)先使用的還是popstate
事件。
-
實(shí)現(xiàn)了
go、push、replace
等方法。
我們可以看到,hash
模式實(shí)現(xiàn)的push、replace
方法其實(shí)也是優(yōu)先使用history
里面的方法,也就是history api
。
// 可以看到 hash 模式go方法其實(shí)是調(diào)用的window.history.go(n) go (n: number) { window.history.go(n) } // 在支持新的history api情況下優(yōu)先使用history.pushState實(shí)現(xiàn) // 否則使用location api function pushHash (path) { if (supportsPushState) { pushState(getUrl(path)) } else { window.location.hash = path } } // 在支持新的history api情況下優(yōu)先使用history.replaceState實(shí)現(xiàn) // 否則使用location api function replaceHash (path) { if (supportsPushState) { replaceState(getUrl(path)) } else { window.location.replace(getUrl(path)) } }
總結(jié)
在瀏覽器鏈接里面我們改變hash
值是不會(huì)重新向后臺(tái)發(fā)送請(qǐng)求的,也就不會(huì)刷新頁面。并且每次 hash
值的變化,還會(huì)觸發(fā)hashchange
這個(gè)事件。
所以hash
模式的原理就是通過監(jiān)聽hashchange
事件,通過這個(gè)事件我們就可以知道 hash
值發(fā)生了哪些變化然后根據(jù)路由映射關(guān)系來實(shí)現(xiàn)頁面內(nèi)容的更新。(這里hash
值的變化不管是通過js
修改的還是直接點(diǎn)擊瀏覽器的前進(jìn)后退按鈕都會(huì)觸發(fā)hashchange
事件)
對(duì)于hash
模式,如果是在瀏覽器支持history api
情況下,hash
模式的實(shí)現(xiàn)其實(shí)是和history
模式一樣的。只有在不支持history api
情況下才會(huì)監(jiān)聽hashchange
事件。這個(gè)我們可以在源碼中看出來。
總結(jié)
總的來說就是使用 Vue.util.defineReactive
將實(shí)例的 _route
設(shè)置為響應(yīng)式對(duì)象。在push, replace
方法里會(huì)主動(dòng)更新屬性 _route
。而 go,back,forward
,或者通過點(diǎn)擊瀏覽器前進(jìn)后退的按鈕則會(huì)在 hashchange
或者 popstate
的回調(diào)中更新 _route
。_route
的更新會(huì)觸發(fā) RoterView
的重新渲染。
對(duì)于第一次進(jìn)入系統(tǒng),并不會(huì)觸發(fā)hashchange
或者popstate
事件,所以第一次需要自己手動(dòng)匹配路徑然后通過transitionTo
方法進(jìn)行跳轉(zhuǎn),然后渲染對(duì)應(yīng)的視圖。
(學(xué)習(xí)視頻分享:web前端開發(fā)、編程基礎(chǔ)視頻)