1. 程式人生 > 實用技巧 >keep-alive 元件級快取

keep-alive 元件級快取

前言

在Vue構建的單頁面應用(SPA)中,路由模組一般使用vue-router。vue-router不儲存被切換元件的狀態,

它進行push或者replace時,舊元件會被銷燬,而新元件會被新建,走一遍完整的生命週期。

但有時候,我們有一些需求,比如跳轉到詳情頁面時,需要保持列表頁的滾動條的深度,等返回的時候依然在這個位置,這樣可以提高使用者體驗。

在Vue中,對於這種“頁面快取”的需求,我們可以使用keep-alive元件來解決這個需求。

keep-alive

keep-alive是個抽象元件(或稱為功能型元件),實際上不會被渲染在DOM樹中。 它的作用是在記憶體中快取元件(不讓元件銷燬),等到下次再渲染的時候,還會保持其中的所有狀態,並且會觸發activated鉤子函式。 因為快取的需要通常出現在頁面切換時,所以常與router-view一起出現:
<keep-alive>
    <router-view />
</keep-alive>
可以使用keep-alive元件的include/exclude屬性。 include屬性表示要快取的元件名(即元件定義時的name屬性), 接收的型別為string、RegExp或string陣列; exclude屬性有著相反的作用,匹配到的元件不會被快取。 假如可能出現在同一router-view的N個頁面中,我只想快取列表頁和詳情頁,那麼可以這樣寫:
<keep-alive :include="['Home', 'User']">
  <router-view />
</keep-alive>

vue實現前進重新整理,後退不重新整理

希望實現前進重新整理、後退不重新整理的效果。即載入過的介面能快取起來(返回不用重新載入),關閉的介面能被銷燬掉(再進入時重新載入)。

例如對a->b->c 前進(b,c)重新整理,c->b->a 後退(b,a)不重新整理

知道路由是前進還是後退就好了,

這樣的話我就能在後退的時候讓from路由的keepAlive置為false

to路由的keepAlive置為ture,就能在再次前進時,重新載入之前這個keepAlive被置為false的路由了

但是這個需要集合鈎子函式來是實現

// App.vue

<div class="app">
    <keep-alive>
      <router-view v-if="$route.meta.keepAlive"></router-view>
    </keep-alive>
    <router-view v-if="!$route.meta.keepAlive"></router-view>
</div>

下面在router/index.js即我們的路由檔案中,定義meta資訊:

// list是我們的搜尋結果頁面
{      
    path: '/list',  
    name: 'List',      
    component: resolve => require(['@/pages/list'], resolve),    
    meta: {        
        isUseCache: false,  // 這個欄位的意思稍後再說      
        keepAlive: true  // 通過此欄位判斷是否需要快取當前元件  
    }    
}

說這之前,先簡單說一下和快取相關的vue鉤子函式。

設定了keepAlive快取的元件:

第一次進入:beforeRouterEnter ->created->…->activated->…->deactivated

後續進入時:beforeRouterEnter ->activated->deactivated

可以看出,只有第一次進入該元件時,才會走created鉤子,而需要快取的元件中activated是每次都會走的鉤子函式。

所以,我們要在這個鉤子裡面去判斷,當前元件是需要使用快取的資料還是重新重新整理獲取資料。思路有了,下面我們來實現

// list組價的activated鉤子
 activated() {
    // isUseCache為false時才重新重新整理獲取資料
    // 因為對list使用keep-alive來快取元件,所以預設是會使用快取資料的         
    if(!this.$route.meta.isUseCache){            
        this.list = []; // 清空原有資料
        this.onLoad(); // 這是我們獲取資料的函式
    } 
}

這裡的isUseCache其實就是我們用來判斷是否需要使用快取資料的欄位,我們在list的路由的meta中已經預設設定為false,所以第一次進入list時是獲取資料的。

當我們從詳情頁返回時,我們把list頁面路由的isUseCache設定成true,這樣我們在返回list頁面時會使用快取資料

// 詳情頁面的beforeRouteLeave鉤子函式
beforeRouteLeave (to, from, next) {        
    if (to.name == 'List') {
        to.meta.isUseCache = true;    
    }        
    next();
}

我們這裡是在即將離開detail頁面前判斷是否返回的列表頁。

如果是返回list頁面,則把list頁面路由的isUseCache欄位設定成true。為什麼這樣設定呢?

因為我們對list元件使用的keep-alive進行快取元件,其預設就是使用快取的。

而我們又在list元件的actived鉤子函式中進行了判斷:

只有在list頁面的isUseCache==false時才會清空原有資料並重新獲取資料。

所以此處設定isUseCache為true,此時返會list頁面是不會重新獲取資料的,而是使用的快取資料。

detail返回list可以快取資料了,那麼search前往list頁面時怎麼讓list頁面不使用快取資料而是獲取新資料呢?我們重新回到list的activated鉤子中:

// list組價的activated鉤子
 activated() {
    // isUseCache為false時才重新重新整理獲取資料
    // 因為對list使用keep-alive來快取元件,所以預設是會使用快取資料的         
    if(!this.$route.meta.isUseCache){            
        this.list = []; // 清空原有資料
        this.onLoad(); // 這是我們獲取資料的函式
        this.$route.meta.isUseCache = false;    } 
}

我們加了一行this.$route.meta.isUseCache=false;也就是從detail返回list後,將list的isUseCache欄位為false,

而從detail返回list前,我們設定了list的isUseCache為true。

所以,只有從detail返回list才使用快取資料,而其他頁面進入list是重新重新整理資料的。

至此,一個前進重新整理、後退返回的功能基本完成了

場景還原實際

比如,如果這個詳情頁是個訂單詳情,那麼在訂單詳情頁可能會有刪除訂單的操作。 那麼刪除訂單操作後會返回訂單列表頁,是需要列表頁重新重新整理的。 那麼我們需要此時在訂單詳情頁進行是否要重新整理的判斷。簡單改造一下詳情頁:
data () {    
    return {
        isDel: false  // 是否進行了刪除訂單的操作       
    }
},
beforeRouteLeave (to, from, next) {        
    if (to.name == 'List') {
        // 根據是否刪除了訂單的狀態,進行判斷list是否需要使用快取資料
        to.meta.isUseCache = !this.isDel;                
    }        
    next();    
},
methods: {        
    deleteOrder () {       
        // 這裡是一些刪除訂單的操作

        // 將狀態變為已刪除訂單
        // 所以beforeRouteLeave鉤子中就會將list元件路由的isUseCache設定為false    
        // 所以此時再返回list時,list是會重新重新整理資料的 
        this.isDel = true; 
        this.$router.go(-1)
    }
}

另外用Vuex來實現後退功能

然後在一篇部落格中看到是用Vuex來寫的,所以這邊也自己demo了下:

就是下面的程式碼了:

實現條件快取:全域性的include陣列

只需要將B動態地從include陣列中增加/刪除就行了

在Vuex中定義一個全域性的快取陣列,待傳給include:

export default {
  namespaced: true,
  state: {
    keepAliveComponents: [] // 快取陣列
  },
  mutations: {
    keepAlive (state, component) {
      // 注:防止重複新增(當然也可以使用Set)
      !state.keepAliveComponents.includes(component) && 
        state.keepAliveComponents.push(component)
    },
    noKeepAlive (state, component) {
      const index = state.keepAliveComponents.indexOf(component)
      index !== -1 &&
        state.keepAliveComponents.splice(index, 1)
    }
  }
}

在父頁面中定義keep-alive,並傳入全域性的快取陣列:

// App.vue

<div class="app">
  <!--傳入include陣列-->
  <keep-alive :include="keepAliveComponents">
    <router-view></router-view>
  </keep-alive>
</div>

export default {
  computed: {
    ...mapState({
      keepAliveComponents: state => state.global.keepAliveComponents
    })
  }
}

快取:在路由配置頁中,約定使用meta屬性keepAlive,值為true表示元件需要快取。

在全域性路由鉤子beforeEach中對該屬性進行處理,這樣一來,每次進入該元件,都進行快取:

const router = new Router({
  routes: [
    {
      path: '/A/B',
      name: 'B',
      component: B,
      meta: {
        title: 'B頁面',
        keepAlive: true // 這裡指定B元件的快取性
      }
    }
  ]
})

router.beforeEach((to, from, next) => {
  // 在路由全域性鉤子beforeEach中,根據keepAlive屬性,統一設定頁面的快取性
  // 作用是每次進入該元件,就將它快取
  if (to.meta.keepAlive) {
    store.commit('global/keepAlive', to.name)
  }
})

取消快取的時機:對快取元件使用路由的元件層鉤子beforeRouteLeave。

因為B->A->B時不需要快取B,所以可以認為:當B的下一個頁面不是C時取消B的快取,那麼下次進入B元件時B就是全新的:

export default {
  name: 'B',
  created () {
      // ...設定滾動條在最頂部
  },
  beforeRouteLeave (to, from, next) {
    // 如果下一個頁面不是詳情頁(C),則取消列表頁(B)的快取
    if (to.name !== 'C') {
        this.$store.commit('global/noKeepAlive', from.name)
    }
    next()
  }
}

因為B的條件快取,是B自己的職責,所以最好把該業務邏輯寫在B的內部,而不是A中,這樣不至於讓元件之間的跳轉關係變得混亂。

一個需要注意的細節:因為keep-alive元件的include陣列操作的物件是元件名、而不是路由名,

因此我們定義每一個元件時,都要顯式宣告name屬性,否則快取不起作用。而且,一個顯式的name對Vue devtools有提示作用。