vue-router 深刻學習

概述

使用 vue 構建 單頁面應用 時,離不開 vue-router 的配合。經過 vue-router, 咱們能夠創建 路由和組件(頁面)之間的映射關係。當 切換路由 時,將 匹配的組件(頁面) 渲染到對應的位置。html

雖然在工做中能夠很熟練的使用 vue-router,但在使用過程當中經常會出現一些疑問。好比:vue

  1. vue-router怎麼安裝? 安裝過程當中作了什麼?node

  2. vue-router 是怎麼工做的? 原理是什麼?webpack

  3. hash模式和history模式有什麼區別?web

  4. router-view 是怎樣渲染成當前路由對應的組件(頁面)的?vue-router

  5. 嵌套路由是怎麼工做的?數組

  6. 路由懶加載是怎麼工做的?promise

  7. 各個導航守衛(鉤子函數) 分別在什麼狀況下會觸發?瀏覽器

針對這些問題,本文會結合一個小例子,一一解答。緩存

示例

咱們先經過一個簡單的小例子,來回顧一下 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-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);
}
複製代碼

安裝 過程當中,有一些 關鍵步驟

  1. 全局 註冊 beforeCreatedestroyed 鉤子函數。

    在建立 vue實例(根vue實例/組件vue實例) 時觸發 beforeCreate 鉤子函數。

    若是是 根vue實例,根據傳入的 router實例 定義 _router(路由實例對象) 屬性, 而後對 router實例 進行初始化。在 router實例 初始化過程當中,會爲 根vue實例 定義 _route(路由信息對象) 屬性。 接下來,會將 根vue實例_route 屬性設置爲 響應式屬性切換路由 會致使 _route 屬性 更新, 而後 觸發界面從新渲染

    若是是 組件vue實例,爲 組件vue實例 定義 _routerRoot 屬性, 指向 根vue實例

    切換路由 致使 原路由頁面對應的vue實例 須要 銷燬 時, 觸發 destroyed 鉤子函數。

  2. Vue.prototype 定義 $router$route 屬性, 設置對應的 getter

    vue實例 經過 $router$route 屬性訪問 路由實例對象路由信息對象 時, 實際訪問的是 根vue實例_router_route 屬性。

    全部的 vue實例 訪問的 $router($route) 都是 同一個

  3. 註冊全局組件: RouterViewRouterLink

    渲染頁面 的過程當中, 若是遇到 router-linkrouter-view, 會使用 全局註冊 生成的 構造函數

vue-router工做原理

HTML5 中引入了 window.history.pushStatewindow.history.replaceState 方法, 它們能夠分別 添加和修改瀏覽器的歷史記錄不須要從新加載頁面

pushStatereplaceState 方法需配合 window.onpopstate 使用。咱們能夠經過 瀏覽器的前進、回退按鈕 或者 window.history.backwindow.history.gowindow.history.forward 方法, 激活瀏覽器的某個歷史記錄。 若是 激活的歷史記錄 是經過 pushState方法添加 或者 被replaceState方法修改,會觸發註冊的 popstate 事件。

pushStatereplaceState 方法不會觸發 popstate 事件。

若是瀏覽器不支持 pushStatereplaceState 方法, 咱們也能夠經過 window.location.hash = 'xxx' 或者 window.location.replace 的方式 添加和修改瀏覽器的歷史記錄, 而 不須要從新加載頁面

window.location.hashwindow.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.backwindow.history.gowindow.history.forwardthis.$router.backthis.$router.gothis.$router.forward 方法 激活某個歷史記錄時, 觸發註冊的 popstate(hashchange) 事件, 而後 從新渲染頁面

使用 vue-router單頁面應用 進行 頁面跳轉控制 的流程以下:

  1. 安裝 vue-router

  2. 使用 VueRouter構造函數 構建 router 實例;

    在這個過程當中,主要操做以下:

    • 遍歷 routeConfig.routes, 創建 路由path(name)組件(component) 之間的 映射關係

    • 根據 routeConfig.mode,爲 router實例 構建相應的 history屬性(mode: hash => HashHistory, mode: history => Html5History)

  3. 使用 router實例 構建 根vue實例, 觸發 beforeCreate 鉤子函數,對 router實例 進行 初始化;

    初始化 過程當中, 主要操做以下:

    • 根據當前 url,爲 根vue實例 添加 _route(路由信息對象)屬性

    • window 對象註冊 popstate(hashchange) 事件;

    初始化 完成之後, 將 根vue實例_route(路由信息對象)屬性 設置爲 響應式屬性。只要 更新_route屬性,就會 觸發界面更新

    首次界面渲染, 若是遇到 router-view 標籤,會使用 當前路由對應的組件 進行渲染。

  4. 經過 vm.$router.push 或者 vm.$router.replace 進行 頁面跳轉

    在此過程當中, 會在瀏覽器中 新增一個歷史記錄 或者 修改當前歷史記錄, 而後 更新根vue實例的_route(路由信息對象)屬性, 觸發 界面更新

    界面從新渲染 的時候, 遇到 router-view 標籤, 會使用 新路由對應的組件 進行渲染。

  5. 當使用 瀏覽器的前進、回退按鈕 或者 vm.$router.govm.$router.backvm.$router.forward 方法 激活某個瀏覽器歷史記錄時, 觸發註冊的 popstatue(hashchange) 事件, 更新根vue實例的_route(路由信息對象)屬性, 觸發 界面更新

    界面從新渲染 的時候, 遇到 router-view 標籤, 會使用 新路由對應的組件 進行渲染。

基本流程圖以下

hash / history

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.assignwindow.location.replace 方式來跳轉頁面。 每次跳轉,都會 從新加載頁面

使用 history 模式須要 服務端支持,要在服務端增長一個 覆蓋全部狀況的候選資源:若是 URL 匹配不到任何靜態資源,則應該返回同一個 index.html 頁面,這個頁面就是應用依賴的頁面。 不然會出現 404 異常。

http請求 中不會包含 hash 值。

router-view

vue應用 中, 一個 vue組件(component)template標籤最後渲染爲實際dom樹結構,經歷的主要過程以下:

  1. vue組件 對應的 template標籤 被解析成一個 vnode節點對象

    在解析過程當中,會根據 vue組件 對應的 options配置項({data, props, methods...}), 經過 Vue.extend 方法生成 vue組件 對應的構造方法 VueComponent

  2. 執行 vue組件 對應的構造方法 VueComponent,生成 vue組件實例,並進行 初始化

  3. 執行 vue組件實例render 方法,將 vue組件template模板 解析爲 vnode節點樹

  4. vue組件 對應的 vnode節點樹 渲染爲 實際的dom節點樹

router-viewtemplate標籤實際的頁面,經歷的過程和 普通vue組件 基本相同, 稍微的區別是在第一步 - 標籤轉化爲vnode節點

遇到 router-view標籤時,經過執行 全局註冊的RouterViewrender 方法,可轉化爲 vnode節點, 主要過程以下:

  1. 獲取 當前路由 所匹配的 組件對應的options配置項({data, props, methods...})

  2. 根據 組件options配置項, 經過 Vue.extend方法生成 vue組件 對應的構造方法 VueComponent

  3. 生成組件對應的 vnode節點

綜上, router-viewtemplate標籤最後的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 pathroute recordroute nameroute record 的映射關係, 即 pathMapnameMap。 其中, route record 是一個 路由記錄對象, 會包含 pathnamecomponentsparamsquery 等信息。

遍歷 routes配置項 的時候, 若是遇到了 嵌套路由, 會繼續遍歷 route.children, 將 子路由record子路由path、name映射關係 分別添加到 pathMapnameMap 中。 同時,在 子路由record 中添加 parent 屬性,指向 父路由record, 創建 父子路由record 之間的 關聯關係

當咱們切換到 某個路由 時, 會根據 path(或name)pathMap(或nameMap) 中尋找 匹配的路由record,將 匹配的路由record 添加到一個 數組 中。 若是 路由recordparent 屬性不爲 undefined, 那麼將 parent record 經過 unshift 的方式添加到 數組 中。 遞歸 處理 路由recordparent 屬性,直到屬性值爲 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, 渲染頁面 pageApageA 頁面中沒有 router-view,路由切換處理完畢。

路由切換到 '/pathB', 匹配到的 路由record 只有 一個。先處理外層 router-view, 渲染頁面 pageBpageB 中有 router-view 須要處理,可是 record列表 中已經 沒有匹配的record,只能作 空處理

路由切換到 '/pathB/pathC', 匹配到的 路由record兩個。 先處理外層 router-view, 使用 record列表 中的 第一個record 渲染頁面 pageBpageB 中有 router-view 須要處理,使用 record列表 中的 第二個record 渲染頁面 pageCpageC 頁面中沒有 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 配置項構建新路由對應的組件實例渲染頁面

路由懶加載 具體的工做流程以下:

  1. 定義 路由組件, 每個組件都是一個 函數, 用於 動態異步加載組件

    const Foo = () => import(/* webpackChunkName: 'Foo'*/'./Foo')
    複製代碼
  2. 構建 router實例, 使用 pathMap(nameMap) 收集 路由path(路由name)路由record 之間的映射關係。 路由record 中的 組件 是一個 函數,用於 異步獲取構建組件須要的配置項(data、methods、render 等)

  3. 初次加載頁面或路由切換激活某個路由 時, 獲取 激活路由匹配的路由record

  4. 因爲 路由record 中的 組件 是一個函數, 執行函數, 經過 動態添加script的方式加載組件, 返回一個 promise,狀態爲 pending

  5. 激活新路由動做中止,等待組件加載完成。 組件加載完成之後, 執行 js代碼, 獲取 組件對應的配置項(data、methods、render等), 將 步驟4 中的 promise 的狀態置爲 resolve。 觸發 promise 經過 then 方法註冊的 onResolve, 將 路由record中的組件更新爲配置項對象激活新路由動做繼續

  6. 經過 pushState(replaceState、go、back、forward) 激活新路由,觸發 頁面更新使用獲取到的 options 配置項構建新路由對應的組件實例渲染頁面

導航守衛

導航守衛 的具體用法詳見 官網

咱們仍是經過文章開始提供的 示例 來講明 各個路由守衛 在何時觸發。

當路由從 '/pageA' 切換到 '/pageB' 時,要經歷以下流程:

  1. pathMap(nameMap) 中獲取 新路由對應的 route record, 構建 新路由對應的路由信息對象 - route

    若是使用了 路由懶加載route recordcomponents 中的組件不是一個 普通對象,而是一個 函數,構建 組件實例 須要的 options配置項(data、methods、props等) 尚未獲取到。

    此時,新路由尚未被激活,仍是 原路由頁面

  2. 觸發 pageA 註冊的 組件級路由守衛 - beforeRouteLeave。 此時,新路由尚未被激活,仍是 原路由頁面

  3. 觸發 全局守衛 - beforeEach。 此時,新路由尚未被激活,仍是 原路由頁面

  4. 觸發 新路由('/pageB') 註冊的 路由獨享守衛 - beforeEnter。 此時,新路由尚未被激活,仍是 原路由頁面

  5. 若是使用了 路由懶加載,會經過 動態添加script元素的方式服務端 加載 頁面源文件,獲取 新路由頁面對應的組件配置項。 此時 新路由 對應的 route recordcomponents 中的 組件 更新爲一個 普通對象

    此時, 新路由依舊沒有被激活,仍是 原路由頁面

  6. 觸發 pageB 註冊的 組件級路由守衛 - beforeRouteEnter。此時, 新路由依舊沒有被激活,仍是 原路由頁面

  7. 觸發 全局守衛 - beforeResolve, 表明 構建異步路由組件所需的 options 已經獲取。此時, 新路由依舊沒有被激活,仍是 原路由頁面

  8. 更新 根vue實例_route 屬性(路由信息對象)觸發頁面異步更新(觸發_route的setter)。此時, 新路由依舊沒有被激活,仍是 原路由頁面

  9. 觸發 全局守衛 - afterEach。 此時, 新路由依舊沒有被激活,仍是 原路由頁面

  10. 激活新路由,即經過 pushState新路由 添加到 瀏覽器歷史記錄中。 此時, 仍是 原路由頁面

  11. 頁面更新開始渲染 pageB

    觸發 組件pageBbeforeCreatecreatedbeforeMount

  12. 銷燬頁面pageA,觸發 組件pageAbeforeDestorydestroyed

  13. 觸發 組件pageBmountedpageB 渲染完成。

嵌套路由間的切換會有稍許不一樣, 當 '/pageB''/pageB/pageC' 之間 來回切換時, 流程以下:

  1. 等同上面。

  2. 若是從 '/pageB/pageC' 切換到 '/pageB', 觸發 pageC 註冊的 組件級路由守衛 - beforeRouteLeave

    若是從 '/pageB' 切換到 '/pageB/pageC', 不會 觸發 pageB 註冊的 組件級路由守衛 - beforeRouteLeave

  3. 觸發 beforeEach

  4. 觸發 組件pageB 註冊的 組件級路由守衛 - beforeRouteUpdate

  5. 若是從 '/pageB' 切換到 '/pageB/pageC', 觸發 /pageB/pageC 註冊的 路由級路由守衛 - beforeEnter

    若是從 '/pageB/pageC' 切換到 '/pageB'不會觸發beforeEnter

  6. 若是使用了 路由懶加載,須要經過 動態添加script元素的方式服務端 加載 頁面源文件,獲取 新路由頁面對應的組件配置項

  7. 若是從 '/pageB' 切換到 '/pageB/pageC', 觸發 組件pageC 註冊的 組件級路由守衛 - beforeRouterEnter

    若是從 '/pageB/pageC' 切換到 '/pageB'不會觸發beforeRouterEnter

  8. 觸發 beforeResolve

  9. 更新 根vue實例_route 屬性(路由信息對象)觸發頁面異步更新(觸發_route的setter)

  10. 激活新路由,即經過 pushState新路由 添加到 瀏覽器歷史記錄中

  11. 頁面更新開始渲染新頁面

    若是從 '/pageB' 切換到 '/pageB/pageC', 觸發 組件pageBbeforeUpdate, 觸發 組件pageCbeforeCreatecreatedbeforeMountmounted,再觸發 組件pageBupdated

    若是從 '/pageB/pageC' 切換到 '/pageB', 銷燬 頁面pageC,觸發 組件pageBbeforeUpdate, 觸發 組件pageCbeforeDestorydestroyed,再觸發 組件pageBupdated

其餘

  1. vue-router 使用 histroy模式 時,若是 瀏覽器不支持history.pushState, vue-router 會自動變爲 hash模式, 使用 window.location.hash = xxxhashchange 實現 單頁面應用

    若是爲 history模式, 且 router.fallback 設置爲 false 時, 若是 瀏覽器不支持history.pushState, vue-router 不會變爲 hash模式。此時,只能經過 location.assignlocation.replace 方法 從新加載頁面

  未完待續...

相關文章
相關標籤/搜索