隨着前端應用的業務功能起來越複雜,用戶對於使用體驗的要求愈來愈高,單面(SPA
)成爲前端應用的主流形式。大型單頁應用最顯著特色之一就是採用的前端路由系統,經過改變URL
,在不從新請求頁面的狀況下,更新頁面視圖。html
更新視圖但不從新請求頁面,是前端路由原理的核心之一,目前在瀏覽器環境中這一功能的實現主要有2
種方式:前端
URL
中的hash
("#"
);History interface
在HTML5
中新增的方法;vue-router
是Vue.js
框架的路由插件,它是經過mode
這一參數控制路由的實現模式的:vue
const router=new VueRouter({ mode:'history', routes:[...] })
建立VueRouter
的實例對象時,mode
以構造參數的形式傳入。ajax
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
只是一個標記,用來指示實際起做用的對象屬性history
的實現類,二者對應關係:modehistory: 'history':HTML5History; 'hash':HashHistory; 'abstract':AbstractHistory;
history
以前,會對mode
作一些校驗:若瀏覽器不支持HTML5History
方式(經過supportsPushState
變量判斷),則mode
設爲hash
;若不是在瀏覽器環境下運行,則mode
設爲abstract
;VueRouter
類中的onReady()
,push()
等方法只是一個代理,實際是調用的具體history
對象的對應方法,在init()
方法中初始化時,也是根據history
對象具體的類別執行不一樣操做HashHistory
hash
("#"
)符號的原本做用是加在URL
指示網頁中的位置:vue-router
http://www.example.com/index.html#print
#
自己以及它後面的字符稱之爲hash
可經過window.location.hash
屬性讀取.後端
hash
雖然出如今url
中,但不會被包括在http
請求中,它是用來指導瀏覽器動做的,對服務器端徹底無用,所以,改變hash
不會從新加載頁面。hash
的改變添加監聽事件:window.addEventListener("hashchange",funcRef,false)
hash
(window.location.hash
),都會在瀏覽器訪問歷史中增長一個記錄。利用hash
的以上特色,就能夠來實現前端路由"更新視圖但不從新請求頁面"的功能了。數組
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 }
能夠看到,當路由變化時,調用了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 }) }) }
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()
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 ) }
能夠看出,它與push()
的實現結構基本類似,不一樣點它不是直接對window.location.hash
進行賦值,而是調用window.location.replace
方法將路由進行替換。
上面的VueRouter.push()
和VueRouter.replace()
是能夠在vue
組件的邏輯代碼中直接調用的,除此以外在瀏覽器中,用戶還能夠直接在瀏覽器地址欄中輸入改變路由,所以還須要監聽瀏覽器地址欄中路由的變化 ,並具備與經過代碼調用相同的響應行爲,在HashHistory
中這一功能經過setupListeners
監聽hashchange
實現:
setupListeners () { window.addEventListener('hashchange', () => { if (!ensureSlash()) { return } this.transitionTo(getHash(), route => { replaceHash(route.fullPath) }) }) }
該方法設置監聽了瀏覽器事件hashchange
,調用的函數爲replaceHash
,即在瀏覽器地址欄中直接輸入路由至關於代碼調用了replace()
方法。
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
,這就爲單頁應用前端路由,更新視圖但不從新請求頁面提供了基礎。
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) }
代碼結構以及更新視圖的邏輯與hash
模式基本相似,只不過將對window.location.hash()
直接進行賦值window.location.replace()
改成了調用history.pushState()
和history.replaceState()
方法。
在HTML5History
中添加對修改瀏覽器地址欄URL
的監聽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) } }) }) }
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 })()
以上就是hash
模式與history
模式源碼導讀,這2
種模式都是經過瀏覽器接口實現的,除此以外,vue-router
還爲非瀏覽器環境準備了一個abstract
模式,其原理爲用一個數組stack
模擬出瀏覽器歷史記錄棧的功能。
通常的需求場景中,hash
模式與history
模式是差很少的,根據MDN
的介紹,調用history.pushState()
相比於直接修改hash
主要有如下優點:
pushState
設置的新url
能夠是與當前url
同源的任意url
,而hash
只可修改#
後面的部分,故只可設置與當前同文檔的url
pushState
設置的新url
能夠與當前url
如出一轍,這樣也會把記錄添加到棧中,而hash
設置的新值必須與原來不同纔會觸發記錄添加到棧中pushState
經過stateObject
能夠添加任意類型的數據記錄中,而hash
只可添加短字符串pushState
可額外設置title
屬性供後續使用history
模式的問題對於單頁應用來講,理想的使用場景是僅在進入應用時加載index.html
,後續在的網絡操做經過ajax
完成,不會根據url
從新請求頁面,可是若是用戶直接在地址欄中輸入並回車,瀏覽器重啓從新加載等特殊狀況。
hash
模式僅改變hash
部分的內容,而hash
部分是不會包含在http
請求中的(hash
帶#
):
http://oursite.com/#/user/id //如請求,只會發送http://oursite.com/
因此hash
模式下遇到根據url
請求頁面不會有問題
而history
模式則將url
修改的就和正常請求後端的url
同樣(history
不帶#
)
http://oursite.com/user/id
若是這種向後端發送請求的話,後端沒有配置對應/user/id
的get
路由處理,會返回404
錯誤。
官方推薦的解決辦法是在服務端增長一個覆蓋全部狀況的候選資源:若是 URL
匹配不到任何靜態資源,則應該返回同一個 index.html
頁面,這個頁面就是你 app
依賴的頁面。同時這麼作之後,服務器就再也不返回 404
錯誤頁面,由於對於全部路徑都會返回 index.html
文件。爲了不這種狀況,在 Vue
應用裏面覆蓋全部的路由狀況,而後在給出一個 404
頁面。或者,若是是用 Node.js
做後臺,可使用服務端的路由來匹配 URL
,當沒有匹配到路由的時候返回 404
,從而實現 fallback
。