這篇文章也發在我的部落格,歡迎圍觀?
寫在前面
一個web app的實際使用場景中,有一些情景的互動要求,是記錄使用者的瀏覽狀態的。最常見的就是在列表頁進入詳情頁之後,再返回到列表頁,使用者希望返回到進入詳情頁之前的狀態繼續操作。但是有些使用場景,使用者又是希望能夠獲取最新的資料,例如同級列表頁之間切換的時候。
如此,針對上述兩種使用場景,需要實現按需讀取頁面快取。由於SPA應用的路由邏輯也是在前端實現的,因此可以在前端對路由的邏輯進行設定以實現所需效果。
使用技術
- Vue.js作為主要框架
- Vue-router作為前端路由管理器
- Vuex作為狀態管理工具
總體思路
keep-alive判斷當前元件是否讀取快取的節點,在整個生命週期裡面非常靠後,在afterEach之後,基本在元件例項建立之前。(因此在此之前對當前元件是否讀取快取進行處理都是可行的,我選擇在全域性前置守衛進行處理)
而判斷當前元件是否快取的節點,則早於元件的beforeRouteLeave鉤子。
基於上述邏輯,本方案解決的邏輯是,對當前開啟的頁面進行判斷,動態生成需要keepAlive的元件陣列配置,對有可能需要快取的先行進行快取,然後在每次路由切換的時候,再進行判斷,按需讀取頁面快取。
- 使用kepp-alive進行快取,使用include屬性對需要快取的頁面進行配置。
- 由於需要快取的頁面配置系動態生成,所以使用vuex儲存該配置。
- 在路由元資訊中寫入兩個配置,一是該路由是否需要快取,二是從相關路由進入時才進行快取的特定路由陣列。
- 在beforeEach進行設定,每次進入路由之前,對進入的路由及其所有父級路由進行判斷,若需要快取且命中特定路由陣列,則將相關路由新增至快取配置檔案中;若不符合,則將相關路由刪除。(此步驟實現了路由切換時,需要則讀取快取,不需要則重新獲取資料。)
- 使用全域性mixin,進入相關元件之前,對當前路由進行判斷,如果需要快取的則將該路由新增至快取配置中。(此步驟實現了快取當前開啟的需要快取的頁面。)
具體實現
1. 使用include屬性控制路由快取
此處需要注意的是,include匹配首先檢查元件自身的 name 選項,如果 name 選項不可用,則匹配它的區域性註冊名稱 (父元件 components 選項的鍵值)。匿名元件不能被匹配。
但是vue-router的環境下,是沒有區域性註冊名稱的,只能為元件補全name屬性。
因此,請務必給元件新增 name 選項,否則匿名元件將全部應用快取。
<keep-alive :include="$store.state.cachedRouteNames">
<router-view />
</keep-alive>
複製程式碼
2. 新增全域性路由快取配置
// store/index.js
const store = new vuex.Store({
state: {
// 快取的路由列表
cachedRouteNames: [],
},
mutations: {
UPDATE_CACHEDROUTENAMES(state,{ action, route }) {
const methods = {
'add': () => {
state.cachedRouteNames.push(route)
},
'delete': () => {
state.cachedRouteNames.splice(state.cachedRouteNames.findIndex((e) => { return e === route}),1)
}
}
methods[action]()
}
}
})
複製程式碼
3. 配置路由元資訊,對需要快取的路由進行配置
keepAlive表明路由需要被快取,必須,否則不快取
cacheWhenFromRoutes為陣列,非必須,若為falsy值,則任何時候均快取;若為空陣列,則任何時候均不快取
// router/index.js
{
path: '/productslist',
name: 'ProductsList',
component: ProductsList,
meta: {
keepAlive: true,
cacheWhenFromRoutes: ['ProductDetail'] // 此處配置的是路由的name
}
},
複製程式碼
4. 配置全域性前置守衛,按需讀取快取
// routeControl.js
// 需要快取的路由名稱陣列
const cachedRouteNames = store.state.cachedRouteNames;
// 定義新增快取元件name函式,設定的是元件的name
const addRoutes = (route) => {
const routeName = route.components.default.name
if (routeName && cachedRouteNames.indexOf(routeName) === -1) {
store.commit('UPDATE_CACHEDROUTENAMES', { action: 'add', route: routeName })
}
}
// 定義刪除快取元件name函式,設定的是元件的name
const deleteRoutes = (route) => {
const routeName = route.components.default.name
if (routeName && cachedRouteNames.indexOf(routeName) !== -1) {
store.commit('UPDATE_CACHEDROUTENAMES', { action: 'delete', route: routeName })
}
}
router.beforeEach((to, from, next) => {
// 處理快取路由開始
// 在讀取快取之前,先對該元件是否讀取快取進行處理
to.matched.forEach((item, index) => {
const routes = item.meta.cacheWhenFromRoutes;
/**
* 此處有幾種情況
* 1. 沒有配置cacheWhenFromRoutes, 則一直快取;
* 2. 配置了cacheWhenFromRoutes,但是首次開啟此web app,則from.name為空,此時應該將該頁面元件的name新增到快取配置檔案中
* 3. 配置了cacheWhenFromRoutes,from.name不為空,若命中cacheWhenFromRoutes,則新增該頁面元件的name到快取配置檔案中,否則刪除。
*
**/
if (item.meta.keepAlive && (!routes || (routes && (!from.name || routes.indexOf(from.name) !== -1)))) {
addRoutes(item)
} else {
deleteRoutes(item)
}
})
// 處理快取路由結束
new Promise(( resolve, reject ) => {
// ..other codes
}).then( res => {
if ( res ) {
next(res)
} else {
next()
}
})
})
// 全域性混入。此步驟的目的是在該元件被解析之後,若是屬於需要快取的元件,先將其新增到快取配置中,進行快取。
// 導航守衛的最後一個步驟就是呼叫 beforeRouteEnter 守衛中傳給 next 的回撥函式,此時整個元件已經被解析,DOM也已經更新。
Vue.mixin({
beforeRouteEnter(to, from, next) {
next(vm => {
to.matched.forEach((item) => {
const routeName = item.components.default.name
if (to.meta.keepAlive && routeName && cachedRouteNames.indexOf(routeName) === -1) {
store.commit('UPDATE_CACHEDROUTENAMES', { action: 'add', route: routeName })
}
})
})
},
})
複製程式碼
寫在最後
坑點
- 此方案涉及兩個name,一個是設定特定路由時,使用路由的name。另一個是動態生成快取配置檔案時,使用的是頁面元件的name。
- 務必給元件新增name屬性,便於include屬性的使用,也方便除錯跟蹤。如果元件缺少name屬性,將會預設使用快取。
- 動態處理快取配置時,一定要對to.matched進行遍歷,否則巢狀路由的父級路由的快取就無法生效,將導致子路由的快取也無法生效。
- 全域性混入有一定危險性,慎用...
以上是實踐過程中摸索出來的一種解決方案,我相信存在更加優雅高效的解決方式。如果你正好實踐過相關方法,煩請指正,謝謝。