最近因為某些需求,所以需要把一些動作放在 Vue 外面,但是由於 Vue 整個生命週期的關係,所以有些事情沒辦法在元件當中操作。所以就把歪腦筋動到 vue-router 身上,後來發現,很多動作還是會有些意外發生。
我們就來聊一下那些看似合理的 意外!
引言
以下皆是在 Vue 2.3/Vue-router 2.5 含以上版本測試,如果你不是該版本或以上,請斟酌服用。
Lifecycle 與 Vue-router 2.5
一開始還是要請出官方的圖來打一點預防針,
接著來看一下 Vue-router 2.5 在 Navigation Guards 新加入的特性,
請留意最後的 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')
就會開始動作,所以這樣會造成什麼問題?
- 如果你的 Plugins 當中所觸發的傳輸,會取回關鍵性資料,例如 User ID
- 如果 1. 取回的資料需要在元件中,也就是
beforeRouteEnter
當中用到 - 在
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 那邊來插手或是干涉就有點詭異。
所以,真實的執行順序大概是這個樣子,
- Vuex Plugins 還是最優先執行
- 遇到
beforeEach
接著執行 - 遇到
beforeRouterEnter
接著執行 - 遇到
beforeResolve
接著執行 - 遇到
beforeRouteUpdate
接著執行 - 遇到
beforeRouteLeave
接著執行
接著如果瀏覽其他的路徑的話,只有幾項會被觸發,
- 最先執行
beforeEach
a. 如果元件有beforeRouteLeave
則先執行 - 遇到
beforeRouterEnter
接著執行
a. 如果父元件有beforeRouteUpdate
則先執行 - 遇到
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()
繼續下一步驟)。
只是這裡有些你要自己解決的事情,
- 因為 每次 都會呼叫
beforeEach
,例如登入檢查,應該避免同一時間重複檢查(使用者其實不知情,但是你的 API Server 會覺得你很煩(? - 如果
beforeEach
寫壞了,會整組 App 跟著壞掉,不可不慎(或者是很容易掉入 Router 的無窮迴圈)。 - 子元件的
beforeRouteEnter
會比父元件的beforeRouteUpdate
慢一步執行,所以必須確認兩者資料流得關連性,操作不慎是有可能造成髒資料的產出(例如污染了 Vuex 當中的 state)。 beforeEach
在父元件上有beforeRouteLeave
時會慢他一步執行,所以也是得確保資料流關連性問題,例如我故意在beforeRouteLeave
登出,然後beforeEach
就得再檢查一次登入(或更甚是如果使用meta
屬性檢查而被跳過,那麼邏輯判斷就是有漏洞了)。
結語
- 其實登入登出只是假議題。
- 我要做的其實是動態元件載入。
- 實際上可行,可以用 API 回應來控制 Vue Components。
- 剩下的不好說。
- 對於動態載入有興趣的我再另外寫一篇。
- 上述 5. 不知道什麼時候會寫。