上一篇,小編講到在vue-router中是經過mode這一參數控制路由的實現模式的。今天就讓咱們深刻去觀摩vue-router源碼是如何實現路由的html
路由這個概念最早是後端出現的。在之前用模板引擎開發頁面時,常常會看到這樣的地址前端
http://www.vueRouter.com/login
大體流程能夠當作這樣:vue
即:路由就是跟後端服務器的一種交互方式,經過不一樣的路徑,來請求不一樣的資源,請求不一樣的頁面是路由的其中一種功能vue-router
隨着前端應用的業務功能愈來愈複雜、用戶對於使用體驗的要求愈來愈高,單頁應用(SPA)成爲前端應用的主流形式。大型單頁應用最顯著特色之一就是採用前端路由系統,經過改變URL,在不從新請求頁面的狀況下,更新頁面視圖。json
"更新視圖但不從新請求頁面"是前端路由原理的核心之一,目前在瀏覽器環境中這一功能的實現主要有兩種方式:後端
hash示例:數組
http://www.vueRouter.com/login
hash模式:hash值是URL 的錨部分(從 # 號開始的部分)。hash值的變化並不會致使瀏覽器向服務器發起請求,瀏覽器不發起請求,從而不會刷新界面。另外每次 hash 值的變化,還會觸發hashchange 這個事件,經過這個事件咱們就能夠知道 hash 值發生了哪些變化。而後咱們即可以監聽hashchange來實現更新頁面部份內容的操做:瀏覽器
function updateDom () { // todo 匹配 hash 作 dom 更新操做 } window.addEventListener('hashchange', updateDom)
若是不想要很醜的 hash,咱們能夠用路由的 history 模式,這種模式充分利用 history.pushState API 來完成 URL 跳轉而無須從新加載頁面。前端框架
const router = new VueRouter({ mode: 'history', routes: [...] })
咱們找到VueRouter類的定義,摘錄與mode參數有關的部分以下:服務器
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) } }
在瀏覽器環境下的兩種方式,分別就是在HTML5History,HashHistory兩個類中實現的。他們都定義在src/history文件夾下,繼承自同目錄下base.js文件中定義的History類。History中定義的是公用和基礎的方法,直接看會一頭霧水,咱們先從HTML5History,HashHistory兩個類中看着親切的push(), 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 }
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 }
能夠看到,當路由變化時,調用了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組件實例,但咱們知道Vue做爲漸進式的前端框架,自己的組件定義中應該是沒有有關路由內置屬性_route,若是組件中要有這個屬性,應該是在插件加載的地方,即VueRouter的install()方法中混合入Vue對象的,查看install.js源碼,有以下一段:
export function install (Vue) { Vue.mixin({ beforeCreate () { if (isDef(this.$options.router)) { this._router = this.$options.router this._router.init(this) Vue.util.defineReactive(this, '_route', this._router.history.current) } registerInstance(this, this) }, }) }
經過Vue.mixin()方法,全局註冊一個混合,影響註冊以後全部建立的每一個 Vue 實例,該混合在beforeCreate鉤子中經過Vue.util.defineReactive()定義了響應式的_route屬性。所謂響應式屬性,即當_route值改變時,會自動調用Vue實例的render()方法,更新視圖。
總結,從設置路由改變到視圖更新的流程以下:
$router.push() --> HashHistory.push() --> History.transitionTo() --> History.updateRoute() --> {app._route = route} --> vm.render()
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 ) }
以上討論的VueRouter.push()和VueRouter.replace()是能夠在vue組件的邏輯代碼中直接調用的,除此以外在瀏覽器中,用戶還能夠直接在瀏覽器地址欄中輸入改變路由,所以VueRouter還須要能監聽瀏覽器地址欄中路由的變化,並具備與經過代碼調用相同的響應行爲。在HashHistory中這一功能經過setupListeners實現:
setupListeners () { window.addEventListener('hashchange', () => { if (!ensureSlash()) { return } this.transitionTo(getHash(), route => { replaceHash(route.fullPath) }) }) }
該方法設置監聽了瀏覽器事件hashchange,調用的函數爲replaceHash,即在瀏覽器地址欄中直接輸入路由至關於代碼調用了replace()方法
History interface是瀏覽器歷史記錄棧提供的接口,經過back(), forward(), go()等方法,咱們能夠讀取瀏覽器歷史記錄棧的信息,進行各類跳轉操做。
window.history.pushState(stateObject, title, URL) window.history.replaceState(stateObject, title, URL)
咱們來看vue-router中的源碼:
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) }
在HTML5History中添加對修改瀏覽器地址欄URL的監聽是直接在構造函數中執行的:
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) } }) }) }
固然了HTML5History用到了HTML5的新特特性,是須要特定瀏覽器版本的支持的,前文已經知道,瀏覽器是否支持是經過變量supportsPushState來檢查的:
// src/util/push-state.js export const supportsPushState = inBrowser && (function () { const ua = window.navigator.userAgent if ( (ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) && ua.indexOf('Mobile Safari') !== -1 && ua.indexOf('Chrome') === -1 && ua.indexOf('Windows Phone') === -1 ) { return false } return window.history && 'pushState' in window.history })()
根據MDN的介紹,調用history.pushState()相比於直接修改hash主要有如下優點:
抽象模式是屬於最簡單的處理了,由於不涉及和瀏覽器地址相關記錄關聯在一塊兒;總體流程依舊和 HashHistory 是同樣的,只是這裏經過數組來模擬瀏覽器歷史記錄堆棧信息。
export class AbstractHistory extends History { index: number; stack: Array<Route>; // ... push (location: RawLocation) { this.transitionTo(location, route => { // 更新歷史堆棧信息 this.stack = this.stack.slice(0, this.index + 1).concat(route) // 更新當前所處位置 this.index++ }) } replace (location: RawLocation) { this.transitionTo(location, route => { // 更新歷史堆棧信息 位置則不用更新 由於是 replace 操做 // 在堆棧中也是直接 replace 掉的 this.stack = this.stack.slice(0, this.index).concat(route) }) } // 對於 go 的模擬 go (n: number) { // 新的歷史記錄位置 const targetIndex = this.index + n // 超出返回了 if (targetIndex < 0 || targetIndex >= this.stack.length) { return } // 取得新的 route 對象 // 由於是和瀏覽器無關的 這裏獲得的必定是已經訪問過的 const route = this.stack[targetIndex] // 因此這裏直接調用 confirmTransition 了 // 而不是調用 transitionTo 還要走一遍 match 邏輯 this.confirmTransition(route, () => { // 更新 this.index = targetIndex this.updateRoute(route) }) } ensureURL () { // noop } }
整個的和 history 相關的代碼到這裏已經分析完畢了,雖然有三種模式,可是總體執行過程仍是同樣的,惟一差別的就是在處理location更新時的具體邏輯不一樣。
歡迎拍磚哈