vue-router實現原理
近期面試,遇到關於vue-router實現原理的問題,在查閱了相關資料后,根據自己理解,來記錄下。
我們知道vue-router是vue的核心插件,而當前vue項目一般都是單頁面應用,也就是說vue-router是應用在單頁面應用中的。
那么,什么是單頁面應用呢?在單頁面應用出現之前,多頁面應用又是什么樣子呢?
單頁面應用與多頁面應用
單頁面
即 第一次進入頁面的時候會請求一個html文件,刷新清除一下。切換到其他組件,此時路徑也相應變化,但是並沒有新的html文件請求,頁面內容也變化了。
原理是:JS會感知到url的變化,通過這一點,可以用js動態的將當前頁面的內容清除掉,然后將下一個頁面的內容掛載到當前頁面上,這個時候的路由不是后端來做了,而是前端來做,判斷頁面到底是顯示哪個組件,清除不需要的,顯示需要的組件。這種過程就是單頁應用,每次跳轉的時候不需要再請求html文件了。
多頁面
即 每一次頁面跳轉的時候,后台服務器都會給返回一個新的html文檔,這種類型的網站也就是多頁網站,也叫做多頁應用。
原理是:傳統的頁面應用,是用一些超鏈接來實現頁面切換和跳轉的
其實剛才單頁面應用跳轉原理即 vue-router實現原理
vue-router實現原理
原理核心就是 更新視圖但不重新請求頁面。
vue-router實現單頁面路由跳轉,提供了三種方式:hash模式、history模式、abstract模式,根據mode參數來決定采用哪一種方式。
路由模式
vue-router 提供了三種運行模式:
● hash: 使用 URL hash 值來作路由。默認模式。
● history: 依賴 HTML5 History API 和服務器配置。查看 HTML5 History 模式。
● abstract: 支持所有 JavaScript 運行環境,如 Node.js 服務器端
Hash模式
hash即瀏覽器url中#后面的內容,包含#。hash是URL中的錨點,代表的是網頁中的一個位置,單單改變#后的部分,瀏覽器只會加載相應位置的內容,不會重新加載頁面。
也就是說
- 即#是用來指導瀏覽器動作的,對服務器端完全無用,HTTP請求中,不包含#。
- 每一次改變#后的部分,都會在瀏覽器的訪問歷史中增加一個記錄,使用”后退”按鈕,就可以回到上一個位置。
所以說Hash模式通過錨點值的改變,根據不同的值,渲染指定DOM位置的不同數據。
History模式
HTML5 History API提供了一種功能,能讓開發人員在不刷新整個頁面的情況下修改站點的URL,就是利用 history.pushState API 來完成 URL 跳轉而無須重新加載頁面;
由於hash模式會在url中自帶#,如果不想要很丑的 hash,我們可以用路由的 history 模式,只需要在配置路由規則時,加入"mode: 'history'",這種模式充分利用 history.pushState API 來完成 URL 跳轉而無須重新加載頁面。
有時,history模式下也會出問題:
eg:
hash模式下:xxx.com/#/id=5 請求地址為 xxx.com,沒有問題。
history模式下:xxx.com/id=5 請求地址為 xxx.com/id=5,如果后端沒有對應的路由處理,就會返回404錯誤;
為了應對這種情況,需要后台配置支持:
在服務端增加一個覆蓋所有情況的候選資源:如果 URL 匹配不到任何靜態資源,則應該返回同一個 index.html 頁面,這個頁面就是你 app 依賴的頁面。
abstract模式
abstract模式是使用一個不依賴於瀏覽器的瀏覽歷史虛擬管理后端。
根據平台差異可以看出,在 Weex 環境中只支持使用 abstract 模式。 不過,vue-router 自身會對環境做校驗,如果發現沒有瀏覽器的 API,vue-router 會自動強制進入 abstract 模式,所以 在使用 vue-router 時只要不寫 mode 配置即可,默認會在瀏覽器環境中使用 hash 模式,在移動端原生環境中使用 abstract 模式。 (當然,你也可以明確指定在所有情況下都使用 abstract 模式)。
具體更加詳細的文章,請參考:
Vue番外篇 -- vue-router淺析原理
vue-router的原理
hash模式
hash模式背后的原理是onhashchange事件
因為hash發生變化的url都會被瀏覽器記錄下來,從而你會發現瀏覽器的前進后退都可以用了,同時點擊后退時,頁面字體顏色也會發生變化。這樣一來,盡管瀏覽器沒有請求服務器,但是頁面狀態和url一一關聯起來,后來人們給它起了一個霸氣的名字叫前端路由,成為了單頁應用標配。
在hash模式下,前端路由修改的是#中的信息,而瀏覽器請求時是不帶它玩的,僅hash符號之前的url會被包含在請求中所以沒有問題.
history模式
通過history api,我們丟掉了丑陋的#,但是它也有個毛病:
不怕前進,不怕后退,就怕刷新,f5,(如果服務器中沒有相應的響應或者資源,會分分鍾刷出一個404來。),因為刷新是實實在在地去請求服務器的,不玩虛的。
history模式下,前端的url必須和實際向后端發起請求的url一致,如http://abc.com/user/id,后端如果沒有對user/id的路由處理,將返回404錯誤。
vue-router底層實現方式
2.1實例化 Vue/Vue組件創建
new Vue({ router, template: ` <div id="app"> <h1>Basic</h1> <ul> <li><router-link to="/">/</router-link></li> <li><router-link to="/foo">/foo</router-link></li> <li><router-link to="/bar">/bar</router-link></li> <router-link tag="li" to="/bar">/bar</router-link> </ul> <router-view class="view"></router-view> </div> ` }).$mount('#app')
2.2執行 vue-router 注入的 beforeCreate 鈎子函數
通過Vue.mixin() // ... Vue.mixin({ beforeCreate () { // 判斷是否有 router if (this.$options.router) { // 賦值 _router this._router = this.$options.router // 初始化 init this._router.init(this) // 定義響應式的 _route 對象 Vue.util.defineReactive(this, '_route', this._router.history.current) } } })
具體來說,首先判斷實例化時 options 是否包含 router,如果包含也就意味着是一個帶有路由配置的實例被創建了,此時才有必要繼續初始化路由相關邏輯。然后給當前實例賦值_router,這樣在訪問原型上的 $router 的時候就可以得到 router 了。
下邊來看里邊兩個關鍵:router.init 和 定義響應式的 _route 對象。
2.3執行 router.init(vm)
router.init
然后來看 router 的 init 方法就干了哪些事情
(注冊了對地址變更的監聽,history.setupListeners())
依舊是在 src/index.js 中:
/* @flow */ import { install } from './install' import { createMatcher } from './create-matcher' import { HashHistory, getHash } from './history/hash' import { HTML5History, getLocation } from './history/html5' import { AbstractHistory } from './history/abstract' import { inBrowser, supportsHistory } from './util/dom' import { assert } from './util/warn' export default class VueRouter { // ... init (app: any /* Vue component instance */) { // ... this.app = app const history = this.history if (history instanceof HTML5History) { history.transitionTo(getLocation(history.base)) } else if (history instanceof HashHistory) { history.transitionTo(getHash(), () => { window.addEventListener('hashchange', () => { history.onHashChange() }) }) } history.listen(route => { this.app._route = route }) } // ... } // ...
可以看到初始化主要就是給 app 賦值,針對於 HTML5History 和 HashHistory 特殊處理,因為在這兩種模式下才有可能存在進入時候的不是默認頁,需要根據當前瀏覽器地址欄里的 path 或者 hash 來激活對應的路由,此時就是通過調用 transitionTo 來達到目的;
而且此時還有個注意點是針對於 HashHistory 有特殊處理,
2.3.1為什么不直接在初始化 HashHistory 的時候監聽 hashchange 事件呢?
答:這個是為了修復vuejs/vue-router#725這個 bug 而這樣做的,
原因是因為在初始化的時候如果此時的 hash 值不是以 / 開頭的話就會補上 #/,這個過程會觸發 hashchange 事件,所以會再走一次生命周期鈎子,也就意味着會再次調用 beforeEnter 鈎子函數。即beforeEnter 鈎子就會被觸發兩次。
2.3.2transitionTo 方法的大概邏輯
在 src/history/base.js 中:
/* @flow */ import type VueRouter from '../index' import { warn } from '../util/warn' import { inBrowser } from '../util/dom' import { runQueue } from '../util/async' import { START, isSameRoute } from '../util/route' import { _Vue } from '../install' export class History { // ... transitionTo (location: RawLocation, cb?: Function) { // 調用 match 得到匹配的 route 對象 const route = this.router.match(location, this.current) // 確認過渡 this.confirmTransition(route, () => { // 更新當前 route 對象 this.updateRoute(route) cb && cb(route) // 子類實現的更新url地址 // 對於 hash 模式的話 就是更新 hash 的值 // 對於 history 模式的話 就是利用 pushstate / replacestate 來更新 // 瀏覽器地址 this.ensureURL() }) }
2.hashchange 或者路由跳轉時,執行 history.transitionTo(...),在這個過程中,會進行地址匹配,得到一個對應當前地址的 route,然后將其設置到對應的 vm._route上。updateRoute.
只是進行了賦值,
2.3.3為什么 vm 就可以感知到路由的改變了呢?
答案在 vue-router 注入 Vue 的 beforeCreate 鈎子函數中:
Vue.util.defineReactive(this, '_route', this._router.history.current)
采用與 Vue 本身數據相同的“數據劫持”方式,這樣對 vm._route 的賦值會被 Vue 攔截到,並且觸發 Vue 組件的更新渲染流程。
2.4視圖更新
地址變更如何同步視圖更新?
接上一步,vm._route 已經接收到路由的變更,從而觸發視圖更新。而當視圖更新進一步調用到 <router-view> 的 render() 時,即進入了 <router-view> 的處理。
<router-view> 的 render() 采用函數調用(h())模式,而非通過模板生成。這也是 Vue2 支持的定義組件渲染邏輯的方式,類似 React 的 render()。
采用這種模式的好處是
可以完全利用 JavaScript 的能力來編寫邏輯,不必受制於 Vue 的類 HTML 模板語法。
這里的主要處理邏輯是從根組件中取出當前的路由對象(parent.$route),然后取得該路由下對應的組件,然后交由該組件進行渲染:
return h(component, data, children)
這其中還涉及 <router-view> 嵌套的處理,不過主要邏輯就是這樣了。
小結
vue-router 以插件方式“侵入”Vue,從而支持一個額外的 router 屬性,以提供監聽並改變組件路由數據的能力。這樣每次路由發生改變后,可以同步到數據,進而“響應式”地觸發組件的更新。
<router-view> 作為根組件下的子組件,從根組件那里可以獲取到當前的路由對象,進而根據路由對象的組件配置,取出組件並用其替換當前位置的內容。這樣,也就完成整個路由變更到視圖變更的過程。
① 路由變更到視圖變更的過程整理為:
hashchange
-->
match route
-->
set vm._route
-->
<router-view> render()
-->
render matched component
② 實現過程中的技術點包括:
Vue 插件機制
響應式數據機制
Vue 渲染機制
地址變更監聽
————————————————
版權聲明:本文為CSDN博主「chenzeze0707」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/chenzeze0707/article/details/90287466