[VueJS] 關於 vue-router 外面的兩三事

最近因為某些需求,所以需要把一些動作放在 Vue 外面,但是由於 Vue 整個生命週期的關係,所以有些事情沒辦法在元件當中操作。所以就把歪腦筋動到 vue-router 身上,後來發現,很多動作還是會有些意外發生。

我們就來聊一下那些看似合理的 意外


引言

以下皆是在 Vue 2.3/Vue-router 2.5 含以上版本測試,如果你不是該版本或以上,請斟酌服用。

Lifecycle 與 Vue-router 2.5

一開始還是要請出官方的圖來打一點預防針,

https://vuejs.org/v2/guide/instance.html#Lifecycle-Diagram

接著來看一下 Vue-router 2.5 在 Navigation Guards 新加入的特性,

https://router.vuejs.org/en/advanced/navigation-guards.html

請留意最後的 The Full Navigation Resolution Flow 的順序就好了。

非同步載入

我之前有一篇文章提到了 Vuex 的 Plugins 可以做的事情,文末有個但書,

如果是在 App 的最上層,以上的作法會出現例外。

是的,當我把 store 在 App 的最上層呼叫的時候,會出現例外。舉個例子來說,

import store from '@/vuex'

export default {
  beforeRouteEnter (to, from, next) {
    store.dispatch('fetch/something')
      .then(() => {
        next()
      })
  }
}

在這種時候,即便你的 Plugins 當中的動作會先執行,你也無法預期非同步載入在什麼時候會拿到結果,緊接著 store.dispatch('fetch/something') 就會開始動作,所以這樣會造成什麼問題?

  1. 如果你的 Plugins 當中所觸發的傳輸,會取回關鍵性資料,例如 User ID
  2. 如果 1. 取回的資料需要在元件中,也就是 beforeRouteEnter 當中用到
  3. beforeRouteEnter 是有機會拿不到資料的

所以,在這種多重非同步載入的情況下,確實是有可能造成元件異常。然而,這一點在 NuxtJS 貌似有比較好的處理,不過我沒有深入研究他,所以這一塊暫時不放上來比較。

或許你會說,使用 Promise.all 應該可以解決,不過那又是另一件事情了。後面有時間的話再提出來說明一下。

真實的動作

在一整個 Vue 的環境中,Vue-Router, Vuex 目前來說大抵上是基本配備,當然如果你不需要的話,可以通篇略過是沒有問題的。

回想一下剛剛 Vue 官方的 Lifecycle Diagram 與 Vue-Router 的 Navigation Guards,接著在思考一下 Vuex 的 Plugins,然後或許你會覺得,

誰叫你在 Component 外面(或是 App 底層)就把 store 叫進來用?

對不起。

迫於需要,所以我也只能這麼做,Vuex 的 Plugins 沒有不好,只是因為 Router 的需要,所以必須要在 Router 運行之前就拿到一些關鍵資訊,例如登入狀態。然而,雖然我可以在 Plugins 當中就取得登入狀態,但是在整個 Component 開始渲染的同時,登入狀態可能會處於 不明 的情況,這種情況不允許發生。

爾或者說,如果登入狀態檢查失效,則必須要被強制轉到登入頁面,這個動作全權由 Router 控制,Vuex 那邊來插手或是干涉就有點詭異。

所以,真實的執行順序大概是這個樣子,

  1. Vuex Plugins 還是最優先執行
  2. 遇到 beforeEach 接著執行
  3. 遇到 beforeRouterEnter 接著執行
  4. 遇到 beforeResolve 接著執行
  5. 遇到 beforeRouteUpdate 接著執行
  6. 遇到 beforeRouteLeave 接著執行

接著如果瀏覽其他的路徑的話,只有幾項會被觸發,

  1. 最先執行 beforeEach
    a. 如果元件有 beforeRouteLeave 則先執行
  2. 遇到 beforeRouterEnter 接著執行
    a. 如果父元件有 beforeRouteUpdate 則先執行
  3. 遇到 beforeResolve 接著執行

這大概是完整的 Vue-Router 執行順序,而 Vuex 的 Plugins 在這裡並不會發生作用,這是當然,他只會在整個 App 第一次啟動的時候會被執行而已。

確保載入狀態

剛剛有提及了 Promise.all 這件事,事實上是可以做到的,舉例來說,

import store from '@/vuex'

export default {
  beforeRouteEnter (to, from, next) {
    Promise.all([
      store.dispatch('fetch/something')
    ]).then(() => {
      next()
    })
  }
}

先決條件是,你的 store.dispatch('fetch/something') 必須要回傳一個 Promise 物件。這樣做的確能確保資料流的正確性,不過相對的除錯起來比較困難,就如同 awaiy/async 的除錯方式一樣。

然而,在每次 beforeEach 一定會最優先被執行的情況下(扣除 App 第一次被載入,會低於 Vuex Plugins 的情況),我們確實可以把某些非同步的資料放在 beforeEach 當中去執行,確認資料無誤後才放行(呼叫 next() 繼續下一步驟)。

只是這裡有些你要自己解決的事情,

  1. 因為 每次 都會呼叫 beforeEach,例如登入檢查,應該避免同一時間重複檢查(使用者其實不知情,但是你的 API Server 會覺得你很煩(?
  2. 如果 beforeEach 寫壞了,會整組 App 跟著壞掉,不可不慎(或者是很容易掉入 Router 的無窮迴圈)。
  3. 子元件的 beforeRouteEnter 會比父元件的 beforeRouteUpdate 慢一步執行,所以必須確認兩者資料流得關連性,操作不慎是有可能造成髒資料的產出(例如污染了 Vuex 當中的 state)。
  4. beforeEach 在父元件上有 beforeRouteLeave 時會慢他一步執行,所以也是得確保資料流關連性問題,例如我故意在 beforeRouteLeave 登出,然後 beforeEach 就得再檢查一次登入(或更甚是如果使用 meta 屬性檢查而被跳過,那麼邏輯判斷就是有漏洞了)。

結語

  1. 其實登入登出只是假議題。
  2. 我要做的其實是動態元件載入。
  3. 實際上可行,可以用 API 回應來控制 Vue Components。
  4. 剩下的不好說。
  5. 對於動態載入有興趣的我再另外寫一篇。
  6. 上述 5. 不知道什麼時候會寫。
Hina Chen
偏執與強迫症的患者,算不上是無可救藥,只是我已經遇上我的良醫了。
Taipei