一、概念
通過改變 URL,在不重新請求頁面的情況下,更新頁面視圖。
二、實現方式
更新視圖但不重新請求頁面,是前端路由原理的核心之一,目前在瀏覽器環境中這一功能的實現主要有2種方式:
1.Hash --- 利用 URL 中的hash("#");
2.利用 History interface 在HTML5中新增的方法。
Vue 中,它是通過 mode 這一參數控制路由的實現模式:
const router=new VueRouter({ mode:'history', routes:[...] })
創建 VueRouter 的實例對象時,mode 以構造參數的形式傳入,如下代碼:
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 參數:
1.默認 hash
2. history。如果瀏覽器不支持 history 新特性,則采用 hash
3. 如果不在瀏覽器環境下,就采用 abstract(Node環境下)

mode 區別:
1. mode:"hash" 多了 “#”
http://localhost:8080/#/login
2.mode:"history"
http://localhost:8080/recommend
HashHistory:
hash("#") 的作用是加載 URL 中指示網頁中的位置。
# 本身以及它后面的字符稱職位 hash,可通過 window.location.hash 獲取
特點:
1. hash 雖然出現在 url 中,但不會被包括在 http 請求中,它是用來指導瀏覽器動作的,對服務器端完全無用,因此,改變 hash 不會重新加載頁面。
2. 可以為 hash 的改變添加監聽事件:
window.addEventListener("hashchange",funcRef,false)
3. 每一次改變 hash(window.localtion.hash),都會在瀏覽器訪問歷史中增加一個記錄。
利用 hash 的以上特點,就可以來實現前端路由"更新視圖但不重新請求頁面"的功能了。
HashHistory 擁有兩個方法,一個是 push, 一個是 replace
兩個方法:HashHistory.push() 和 HashHistory.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 }

從設置路由改變到視圖更新的流程:
$router.push() --> HashHistory.push() --> History.transitionTo() --> History.updateRoute() --> {app._route = route} --> vm.render()
解析:
1 $router.push() //調用方法 2 HashHistory.push() //根據hash模式調用,設置hash並添加到瀏覽器歷史記錄(添加到棧頂)(window.location.hash= XXX) 3 History.transitionTo() //監測更新,更新則調用History.updateRoute() 4 History.updateRoute() //更新路由 5 {app._route= route} //替換當前app路由 6 vm.render() //更新視圖
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 }) }) }
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 ) }

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,這就為單頁應用前端路由,更新視圖但不重新請求頁面提供了基礎
1.push
與hash模式類似,只是將window.hash改為history.pushState
2.replace
與hash模式類似,只是將window.replace改為history.replaceState
3.監聽地址變化
在HTML5History的構造函數中監聽popState(window.onpopstate)
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) }
兩種模式比較
-
pushState設置的新URL可以是與當前URL同源的任意URL;而hash只可修改#后面的部分,故只可設置與當前同文檔的URL
-
pushState通過stateObject可以添加任意類型的數據到記錄中;而hash只可添加短字符串
-
pushState可額外設置title屬性供后續使用
-
history模式則會將URL修改得就和正常請求后端的URL一樣,如后端沒有配置對應/user/id的路由處理,則會返回404錯誤
