keep-alive:元件級快取

大肥凱發表於2018-07-07

title: keep-alive:元件級快取 tags:

  • keep-alive
  • Vue
  • vue-router

頁面快取

在Vue構建的單頁面應用(SPA)中,路由模組一般使用vue-router。vue-router不儲存被切換元件的狀態,它進行push或者replace時,舊元件會被銷燬,而新元件會被新建,走一遍完整的生命週期。

但有時候,我們有一些需求,比如跳轉到詳情頁面時,需要保持列表頁的滾動條的深度,等返回的時候依然在這個位置,這樣可以提高使用者體驗。在Vue中,對於這種“頁面快取”的需求,我們可以使用keep-alive元件來解決這個需求。

使用方式

keep-alive是個抽象元件(或稱為功能型元件),實際上不會被渲染在DOM樹中。它的作用是在記憶體中快取元件(不讓元件銷燬),等到下次再渲染的時候,還會保持其中的所有狀態,並且會觸發activated鉤子函式。因為快取的需要通常出現在頁面切換時,所以常與router-view一起出現:

<keep-alive>
	<router-view />
</keep-alive>
複製程式碼

如此一來,每一個在router-view中渲染的元件,都會被快取起來。

如果只想渲染某一些頁面/元件,可以使用keep-alive元件的include/exclude屬性。include屬性表示要快取的元件名(即元件定義時的name屬性),接收的型別為string、RegExp或string陣列;exclude屬性有著相反的作用,匹配到的元件不會被快取。假如可能出現在同一router-view的N個頁面中,我只想快取列表頁和詳情頁,那麼可以這樣寫:

<keep-alive :include="['ListView', 'DetailView']">
  <router-view />
</keep-alive>
複製程式碼

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

上面include的寫法不是常用的,因為它固定了哪幾個頁面快取或不快取,假如有下面這個場景:

  • 現有頁面:首頁(A)、列表頁(B)、詳情頁(C),一般可以從:A->B->C;
  • B到C再返回B時,B要保持列表滾動的距離;
  • B返回A再進入B時,B不需要保持狀態,是全新的。

很明顯,這個例子中,B是“條件快取”的,C->B時保持快取,A->B時放棄快取。其實解決方案也不難,只需要將B動態地從include陣列中增加/刪除就行了。具體步驟是:

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

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)
    }
  }
}
複製程式碼
  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
    })
  }
}
複製程式碼
  1. 快取:在路由配置頁中,約定使用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)
  }
})
複製程式碼
  1. 取消快取的時機:對快取元件使用路由的元件層鉤子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中,這樣不至於讓元件之間的跳轉關係變得混亂。

  1. 一個需要注意的細節:因為keep-alive元件的include陣列操作的物件是元件名、而不是路由名,因此我們定義每一個元件時,都要顯式宣告name屬性,否則快取不起作用。而且,一個顯式的name對Vue devtools有提示作用。

另一種方式?

網上看到實現條件快取的另一種方式很類似,但它是在父元件中,使用兩個router-view並進行條件渲染:

// 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>
複製程式碼

在GitHub的issue中(見參考連結)有一些爭議,本人也還沒有比較全面的驗證過,所以也暫不知道效果如何。

Q&A

Q:元件名已經加入全域性include陣列了,為什麼頁面還是沒有快取到?

A:如果按照上面的思路來做就沒有問題,注意第5點,即很有可能你忘記給元件宣告一個name屬性了。

Q:元件能夠快取,但是滾動條並沒有快取、比如還是會回到頂部?

A:滾動條這個問題跟元件的HTML結構有關。淺顯而言,keep-alive功能快取的是父元素相對於元件的scrollTop,所以如果你的元件/頁面設定了height:100%、滾動條在元件內部的,看到的滾動條就是沒有快取的。當然,關於這個,還有待進入原始碼深究,給自己留個坑吧。

參考

  • 文件

在動態元件上使用-keep-alive

API:keep-alive

  • 應用

vue-router 之 keep-alive

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

  • 討論

https://github.com/vuejs/vue-router/issues/811

相關文章