使用 vue 構建 單頁面應用 時,離不開 vue-router 的配合。經過 vue-router, 咱們能夠創建 路由和組件(頁面)之間的映射關係。當 切換路由 時,將 匹配的組件(頁面) 渲染到對應的位置。html
雖然在工做中能夠很熟練的使用 vue-router,但在使用過程當中經常會出現一些疑問。好比:vue
vue-router怎麼安裝? 安裝過程當中作了什麼?node
vue-router 是怎麼工做的? 原理是什麼?webpack
hash模式和history模式有什麼區別?web
router-view 是怎樣渲染成當前路由對應的組件(頁面)的?vue-router
嵌套路由是怎麼工做的?數組
路由懶加載是怎麼工做的?promise
各個導航守衛(鉤子函數) 分別在什麼狀況下會觸發?瀏覽器
針對這些問題,本文會結合一個小例子,一一解答。緩存
咱們先經過一個簡單的小例子,來回顧一下 vue-router 的使用。
// html
<div id="app">
<router-view></router-view>
</div>
複製代碼
// js
// 安裝 vue-router
Vue.use(VueRouter)
// router
var router = new VueRouter({
mode: 'history',
routes: [{
path: '/pageA',
name: 'pageA'
component: { template: '<div>pageA</div>'}
}, {
path: '/pageB',
name: 'pageB'
component: { template: '<div>pageB<router-view></router-view></div>'},
children: [{
path: '/pageC',
name: 'pageC'
component: { template: '<div>pageC</div>' }
}]
}]
})
// vue應用
var vm = new Vue({
el: '#app',
router
})
vm.$router.push('/pageA') // 跳轉 pageA
vm.$router.repalce({name: 'pageB'}) // 跳轉 pageB
vm.$router.push({path: '/pageB/pageC'}) // 跳轉 pageC
複製代碼
接下來,會結合上面 示例,對 概述中 提出的問題一一解答。
在使用 vue-router 開發 vue項目 的時候,須要先經過 Vue.use 方法來安裝 vue-router插件。
Vue.use 方法的 內部操做 以下:
Vue.use = function (plugin) {
// 判斷 plugin 是否已經安裝。 若是已安裝, 直接返回, 不需安裝。
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []));
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
// additional parameters
const args = toArray(arguments, 1);
args.unshift(this);
// 若是 plugin 是一個對象, 且提供 install 方法
// 執行 plugin 提供的 install 方法安裝 plugin
if (typeof plugin.install === 'function') {
plugin.install.apply(plugin, args);
} else if (typeof plugin === 'function') {
// 若是 plugin 是一個函數, 執行函數,安裝 plugin
plugin.apply(null, args);
}
// 緩存 已經安裝的 plugin
installedPlugins.push(plugin);
return this
};
複製代碼
Vue.use 方法在 安裝插件(plugin) 的時候, 會調用 插件(plugin) 的 install 方法。
待安裝的插件(plugin), 若是是一個 對象,必須 顯示提供install方法; 若是是一個 函數,則 自動做爲install方法。
vue-router 提供了 install 方法供 Vue.use 方法使用。 方法詳情 以下:
function install (Vue) {
...
Vue.mixin({
// beforeCreate 鉤子函數, 每個vue實例建立的時候, 都會執行
beforeCreate () {
// 只有建立 根vue 實例的時候,配置項中才會有 router 選項
if (isDef(this.$options.router)) {
// this -> 通常爲根vue實例
this._routerRoot = this;
// _router, 路由實例對象
this._router = this.$options.router;
// 初始化router實例
this._router.init(this);
Vue.util.defineReactive(this, '_route', this._router.history.current);
} else {
// this -> 通常爲組件實例
// _routerRoot,含有router的vue實例, 通常爲根vue實例
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this;
}
registerInstance(this, this);
},
// destroyed 鉤子函數,每一個vue實例銷燬時觸發
destroyed () {
registerInstance(this);
}
});
// this.$router, 返回根vue實例的_router屬性
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
});
// this.$route, 返回根vue實例的_route屬性
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
});
// 全局註冊 RouterView 組件
Vue.component('RouterView', View);
// 全局註冊 RouterLink 組件
Vue.component('RouterLink', Link);
}
複製代碼
安裝 過程當中,有一些 關鍵步驟:
全局 註冊 beforeCreate、destroyed 鉤子函數。
在建立 vue實例(根vue實例/組件vue實例) 時觸發 beforeCreate 鉤子函數。
若是是 根vue實例,根據傳入的 router實例 定義 _router(路由實例對象) 屬性, 而後對 router實例 進行初始化。在 router實例 初始化過程當中,會爲 根vue實例 定義 _route(路由信息對象) 屬性。 接下來,會將 根vue實例 的 _route 屬性設置爲 響應式屬性。 切換路由 會致使 _route 屬性 更新, 而後 觸發界面從新渲染。
若是是 組件vue實例,爲 組件vue實例 定義 _routerRoot 屬性, 指向 根vue實例。
當 切換路由 致使 原路由頁面對應的vue實例 須要 銷燬 時, 觸發 destroyed 鉤子函數。
給 Vue.prototype 定義 $router、$route 屬性, 設置對應的 getter。
當 vue實例 經過 $router、$route 屬性訪問 路由實例對象 和 路由信息對象 時, 實際訪問的是 根vue實例 的 _router、_route 屬性。
全部的 vue實例 訪問的 $router($route) 都是 同一個。
註冊全局組件: RouterView 、 RouterLink。
在 渲染頁面 的過程當中, 若是遇到 router-link、 router-view, 會使用 全局註冊 生成的 構造函數。
HTML5 中引入了 window.history.pushState 和 window.history.replaceState 方法, 它們能夠分別 添加和修改瀏覽器的歷史記錄 而 不須要從新加載頁面。
pushState、replaceState 方法需配合 window.onpopstate 使用。咱們能夠經過 瀏覽器的前進、回退按鈕 或者 window.history.back、 window.history.go、 window.history.forward 方法, 激活瀏覽器的某個歷史記錄。 若是 激活的歷史記錄 是經過 pushState方法添加 或者 被replaceState方法修改,會觸發註冊的 popstate 事件。
pushState、replaceState 方法不會觸發 popstate 事件。
若是瀏覽器不支持 pushState、replaceState 方法, 咱們也能夠經過 window.location.hash = 'xxx' 或者 window.location.replace 的方式 添加和修改瀏覽器的歷史記錄, 而 不須要從新加載頁面。
window.location.hash、 window.location.replace 需配合 window.onhashchange 使用。若是 激活的歷史記錄 是經過 window.location.hash方式添加 或者被 window.locaton.relace方法修改,會觸發註冊的 onhashchange 事件。
只要 url 中 # 後面的值發生變化, 就會觸發 hashchange 事件。
使用 vue-router 進行 單頁面應用頁面跳轉 是基於 上述原理 實現的。
當須要 跳轉頁面 時,經過 pushState(replaceState、window.location.hash、 window.location.replace) 方法 添加或修改歷史記錄, 而後 從新渲染頁面。 當經過 瀏覽器的前進、回退按鈕 或者 window.history.back、 window.history.go、 window.history.forward、this.$router.back、 this.$router.go、 this.$router.forward 方法 激活某個歷史記錄時, 觸發註冊的 popstate(hashchange) 事件, 而後 從新渲染頁面。
使用 vue-router 對 單頁面應用 進行 頁面跳轉控制 的流程以下:
安裝 vue-router;
使用 VueRouter構造函數 構建 router 實例;
在這個過程當中,主要操做以下:
遍歷 routeConfig.routes, 創建 路由path(name) 和 組件(component) 之間的 映射關係;
根據 routeConfig.mode,爲 router實例 構建相應的 history屬性(mode: hash => HashHistory, mode: history => Html5History)。
使用 router實例 構建 根vue實例, 觸發 beforeCreate 鉤子函數,對 router實例 進行 初始化;
在 初始化 過程當中, 主要操做以下:
根據當前 url,爲 根vue實例 添加 _route(路由信息對象)屬性;
爲 window 對象註冊 popstate(hashchange) 事件;
初始化 完成之後, 將 根vue實例 的 _route(路由信息對象)屬性 設置爲 響應式屬性。只要 更新_route屬性,就會 觸發界面更新。
首次界面渲染, 若是遇到 router-view 標籤,會使用 當前路由對應的組件 進行渲染。
經過 vm.$router.push 或者 vm.$router.replace 進行 頁面跳轉。
在此過程當中, 會在瀏覽器中 新增一個歷史記錄 或者 修改當前歷史記錄, 而後 更新根vue實例的_route(路由信息對象)屬性, 觸發 界面更新;
界面從新渲染 的時候, 遇到 router-view 標籤, 會使用 新路由對應的組件 進行渲染。
當使用 瀏覽器的前進、回退按鈕 或者 vm.$router.go、 vm.$router.back、 vm.$router.forward 方法 激活某個瀏覽器歷史記錄時, 觸發註冊的 popstatue(hashchange) 事件, 更新根vue實例的_route(路由信息對象)屬性, 觸發 界面更新;
界面從新渲染 的時候, 遇到 router-view 標籤, 會使用 新路由對應的組件 進行渲染。
基本流程圖以下:
vue-router 默認使用 hash 模式, 即 切換頁面 只修改 location.hash。
hash 模式下, 頁面 url 的格式爲: protocol://hostname: port/xxx/xxx/#/xxx。
hash 模式, 也是經過 pushState(replaceState) 在瀏覽器中 新增(修改)歷史記錄。 當經過 前進或者後退 方式激活某個歷史記錄時, 觸發 popstate 事件。 若是 瀏覽器不支持pushState,則經過 修改window.location.hash(window.location.replace) 在瀏覽器中 新增(修改)歷史記錄。 當經過 前進或者後退 方式激活某個歷史記錄時, 觸發 hashChange 事件。
若是選擇 history 模式, 切換頁面會修改 location.pathname。
history模式下,頁面url的格式爲 protocol://hostname: port/xxx/xxx, 更加美觀。
history 模式充分利用 pushState 來完成頁面跳轉。 若是 瀏覽器不支持pushState、replaceState,則經過 window.location.assign 、 window.location.replace 方式來跳轉頁面。 每次跳轉,都會 從新加載頁面。
使用 history 模式須要 服務端支持,要在服務端增長一個 覆蓋全部狀況的候選資源:若是 URL 匹配不到任何靜態資源,則應該返回同一個 index.html 頁面,這個頁面就是應用依賴的頁面。 不然會出現 404 異常。
http請求 中不會包含 hash 值。
在 vue應用 中, 一個 vue組件(component) 從 template標籤 到 最後渲染爲實際dom樹結構,經歷的主要過程以下:
vue組件 對應的 template標籤 被解析成一個 vnode節點對象。
在解析過程當中,會根據 vue組件 對應的 options配置項({data, props, methods...}), 經過 Vue.extend 方法生成 vue組件 對應的構造方法 VueComponent。
執行 vue組件 對應的構造方法 VueComponent,生成 vue組件實例,並進行 初始化。
執行 vue組件實例 的 render 方法,將 vue組件 的 template模板 解析爲 vnode節點樹。
將 vue組件 對應的 vnode節點樹 渲染爲 實際的dom節點樹。
router-view 從 template標籤 到 實際的頁面,經歷的過程和 普通vue組件 基本相同, 稍微的區別是在第一步 - 標籤轉化爲vnode節點。
遇到 router-view標籤時,經過執行 全局註冊的RouterView 的 render 方法,可轉化爲 vnode節點, 主要過程以下:
獲取 當前路由 所匹配的 組件對應的options配置項({data, props, methods...})。
根據 組件options配置項, 經過 Vue.extend方法生成 vue組件 對應的構造方法 VueComponent。
生成組件對應的 vnode節點。
綜上, router-view 從 template標籤 到 最後的dom節點樹 的過程以下:
注意: 上述流程僅適用於 兄弟路由 中間的切換,不適用於 父子路由切換 和使用 keep-alive。 父子路由切換 和使用 keep-alive 以後的路由切換會稍微 不同。
嵌套路由 的具體使用詳見 官網。
嵌套路由 會涉及到將 多個router-view 渲染爲對應的 頁面(或組件)。 在上面的例子中,路由切換到 /pageB 時,外層router-view 渲染爲頁面 pageB,內層router-view 渲染爲 空;當路由切換到 /pageB/pageC 時, 外層router-view 渲染爲頁面 pageB,內層router-view 渲染爲 pageC。
當咱們經過 new VueRouter(...) 建立 router實例 的時候, 會 遍歷routes配置項, 分別創建 route path 和 route record、 route name 和 route record 的映射關係, 即 pathMap、nameMap。 其中, route record 是一個 路由記錄對象, 會包含 path、name、components、params、query 等信息。
遍歷 routes配置項 的時候, 若是遇到了 嵌套路由, 會繼續遍歷 route.children, 將 子路由record 和 子路由path、name 的 映射關係 分別添加到 pathMap、nameMap 中。 同時,在 子路由record 中添加 parent 屬性,指向 父路由record, 創建 父子路由record 之間的 關聯關係。
當咱們切換到 某個路由 時, 會根據 path(或name) 從 pathMap(或nameMap) 中尋找 匹配的路由record,將 匹配的路由record 添加到一個 數組 中。 若是 路由record 的 parent 屬性不爲 undefined, 那麼將 parent record 經過 unshift 的方式添加到 數組 中。 遞歸 處理 路由record 的 parent 屬性,直到屬性值爲 undefined 爲止。 最後咱們會獲得一個 路由record列表,列表中會包含 祖先路由record、父路由record、當前路由record。
在上面的示例中,各個路由匹配的 record列表 以下:
'/pageA': [{path: '/pageA', components: ...}]
'/pageB': [{path: '/pageB', components: ...}]
'/pageB/pageC': [
{path: '/pageB', component: ...},
{path: '/pageB/pageC', components: ...},
]
複製代碼
路由切換 會觸發 頁面從新渲染。 頁面從新渲染的時候會從 最外層的router-view 開始,而後 逐級處理 頁面內部的 router-view。
處理 router-view標籤 的時候, 會從當前路由的 record列表 中查找 匹配的record,而後將 record.components 渲染爲 實際頁面。 vue-router會保證每個 router-view 都能匹配到對應的 record。
當 路由切換到 '/pathA', 匹配到的 路由record 只有 一個。先處理外層 router-view, 渲染頁面 pageA。 pageA 頁面中沒有 router-view,路由切換處理完畢。
當 路由切換到 '/pathB', 匹配到的 路由record 只有 一個。先處理外層 router-view, 渲染頁面 pageB。 pageB 中有 router-view 須要處理,可是 record列表 中已經 沒有匹配的record,只能作 空處理。
當 路由切換到 '/pathB/pathC', 匹配到的 路由record 有 兩個。 先處理外層 router-view, 使用 record列表 中的 第一個record 渲染頁面 pageB。 pageB 中有 router-view 須要處理,使用 record列表 中的 第二個record 渲染頁面 pageC。pageC 頁面中沒有 router-view,路由切換處理完畢。
嵌套路由 之間 相互切換 時,父頁面組件會更新。 更新時,先執行 父頁面組件 對應的 render 方法生成 vnode節點樹。 將 vnode節點樹 渲染爲 dom節點樹 以前, 會將 新vnode節點樹 和 原vnode節點樹 作比較。 因爲 新舊vnode節點樹沒有變化, 因此 父頁面不會有任何dom操做。
父路由 切換到 子路由,父頁面不變, 將 子頁面對應的dom樹 添加到 父頁面的dom樹 中。
子路由 切換到 父路由,父頁面不變, 將 子頁面對應的dom樹 從 父頁面的dom樹 中刪除。
子路由間相互切換,父頁面不變, 將 上一個子頁面對應的dom樹 從 父頁面的dom樹 中刪除,將 下一個子頁面對應的dom樹 添加到 父頁面的dom樹 中。
路由懶加載 的具體使用詳見 官網。
上面的 示例 中沒有用到 路由懶加載,在經過 new VueRouter 構建 router實例 的時候, routes 配置項中的每個 route 中的 component 都是一個 普通對象,構建組件所須要的各個配置項(data、props、methods等) 都已獲取。
在實際的 vue單頁面應用 中,路由懶加載 被普遍應用。 路由懶加載 的前提是使用 webpack打包源代碼。使用 webpack打包源代碼 之後,每一個頁面 都會被分離成一個 單獨的chunk,只有在 vue應用 須要時纔會 動態加載(經過動態添加script元素的方法從服務端獲取頁面js文件,渲染頁面)。
使用 路由懶加載 後, 在構建 router實例 的時候, routes 配置項中的每個 route 中的 component 都是一個 function, 用於 動態從服務端加載頁面源代碼,獲取 構建組件所須要的各個配置項(data、props、methods等)。
在 切換路由 的時候, 會先 經過動態添加script元素的方式 從 服務端 加載 頁面js文件, 獲取 構建頁面組件須要的options配置項(data、props、render、methods等)。 而後 激活新路由(pushState 將新路由添加到瀏覽器歷史記錄中), 觸發 頁面更新,使用獲取到的 options 配置項構建新路由對應的組件實例,渲染頁面。
路由懶加載 具體的工做流程以下:
定義 路由組件, 每個組件都是一個 函數, 用於 動態異步加載組件。
const Foo = () => import(/* webpackChunkName: 'Foo'*/'./Foo')
複製代碼
構建 router實例, 使用 pathMap(nameMap) 收集 路由path(路由name) 和 路由record 之間的映射關係。 路由record 中的 組件 是一個 函數,用於 異步獲取構建組件須要的配置項(data、methods、render 等)。
當 初次加載頁面或路由切換激活某個路由 時, 獲取 激活路由匹配的路由record。
因爲 路由record 中的 組件 是一個函數, 執行函數, 經過 動態添加script的方式加載組件, 返回一個 promise,狀態爲 pending。
激活新路由動做中止,等待組件加載完成。 組件加載完成之後, 執行 js代碼, 獲取 組件對應的配置項(data、methods、render等), 將 步驟4 中的 promise 的狀態置爲 resolve。 觸發 promise 經過 then 方法註冊的 onResolve, 將 路由record中的組件更新爲配置項對象。 激活新路由動做繼續。
經過 pushState(replaceState、go、back、forward) 激活新路由,觸發 頁面更新, 使用獲取到的 options 配置項構建新路由對應的組件實例,渲染頁面。
導航守衛 的具體用法詳見 官網。
咱們仍是經過文章開始提供的 示例 來講明 各個路由守衛 在何時觸發。
當路由從 '/pageA' 切換到 '/pageB' 時,要經歷以下流程:
從 pathMap(nameMap) 中獲取 新路由對應的 route record, 構建 新路由對應的路由信息對象 - route。
若是使用了 路由懶加載, route record 中 components 中的組件不是一個 普通對象,而是一個 函數,構建 組件實例 須要的 options配置項(data、methods、props等) 尚未獲取到。
此時,新路由尚未被激活,仍是 原路由頁面。
觸發 pageA 註冊的 組件級路由守衛 - beforeRouteLeave。 此時,新路由尚未被激活,仍是 原路由頁面。
觸發 全局守衛 - beforeEach。 此時,新路由尚未被激活,仍是 原路由頁面。
觸發 新路由('/pageB') 註冊的 路由獨享守衛 - beforeEnter。 此時,新路由尚未被激活,仍是 原路由頁面。
若是使用了 路由懶加載,會經過 動態添加script元素的方式 從 服務端 加載 頁面源文件,獲取 新路由頁面對應的組件配置項。 此時 新路由 對應的 route record 中 components 中的 組件 更新爲一個 普通對象。
此時, 新路由依舊沒有被激活,仍是 原路由頁面。
觸發 pageB 註冊的 組件級路由守衛 - beforeRouteEnter。此時, 新路由依舊沒有被激活,仍是 原路由頁面。
觸發 全局守衛 - beforeResolve, 表明 構建異步路由組件所需的 options 已經獲取。此時, 新路由依舊沒有被激活,仍是 原路由頁面。
更新 根vue實例 的 _route 屬性(路由信息對象),觸發頁面異步更新(觸發_route的setter)。此時, 新路由依舊沒有被激活,仍是 原路由頁面。
觸發 全局守衛 - afterEach。 此時, 新路由依舊沒有被激活,仍是 原路由頁面。
激活新路由,即經過 pushState 將 新路由 添加到 瀏覽器歷史記錄中。 此時, 仍是 原路由頁面。
頁面更新開始, 渲染 pageB。
觸發 組件pageB 的 beforeCreate、created、beforeMount。
銷燬頁面pageA,觸發 組件pageA 的 beforeDestory、destroyed。
觸發 組件pageB的 mounted,pageB 渲染完成。
嵌套路由間的切換會有稍許不一樣, 當 '/pageB' 和 '/pageB/pageC' 之間 來回切換時, 流程以下:
等同上面。
若是從 '/pageB/pageC' 切換到 '/pageB', 觸發 pageC 註冊的 組件級路由守衛 - beforeRouteLeave。
若是從 '/pageB' 切換到 '/pageB/pageC', 不會 觸發 pageB 註冊的 組件級路由守衛 - beforeRouteLeave。
觸發 beforeEach。
觸發 組件pageB 註冊的 組件級路由守衛 - beforeRouteUpdate。
若是從 '/pageB' 切換到 '/pageB/pageC', 觸發 /pageB/pageC 註冊的 路由級路由守衛 - beforeEnter。
若是從 '/pageB/pageC' 切換到 '/pageB', 不會觸發beforeEnter。
若是使用了 路由懶加載,須要經過 動態添加script元素的方式 從 服務端 加載 頁面源文件,獲取 新路由頁面對應的組件配置項。
若是從 '/pageB' 切換到 '/pageB/pageC', 觸發 組件pageC 註冊的 組件級路由守衛 - beforeRouterEnter。
若是從 '/pageB/pageC' 切換到 '/pageB', 不會觸發beforeRouterEnter。
觸發 beforeResolve。
更新 根vue實例 的 _route 屬性(路由信息對象),觸發頁面異步更新(觸發_route的setter)。
激活新路由,即經過 pushState 將 新路由 添加到 瀏覽器歷史記錄中。
頁面更新開始, 渲染新頁面。
若是從 '/pageB' 切換到 '/pageB/pageC', 觸發 組件pageB 的 beforeUpdate, 觸發 組件pageC 的 beforeCreate、created、beforeMount、mounted,再觸發 組件pageB 的 updated。
若是從 '/pageB/pageC' 切換到 '/pageB', 銷燬 頁面pageC,觸發 組件pageB 的 beforeUpdate, 觸發 組件pageC 的 beforeDestory、destroyed,再觸發 組件pageB 的 updated。
當 vue-router 使用 histroy模式 時,若是 瀏覽器不支持history.pushState, vue-router 會自動變爲 hash模式, 使用 window.location.hash = xxx 和 hashchange 實現 單頁面應用。
若是爲 history模式, 且 router.fallback 設置爲 false 時, 若是 瀏覽器不支持history.pushState, vue-router 不會變爲 hash模式。此時,只能經過 location.assign、 location.replace 方法 從新加載頁面。
未完待續...