天天看點

keep-alive多級路由緩存最佳實踐

大廠技術  堅持周更  精選好文

在我們的業務中,我們常常會有清單頁跳轉詳情頁,詳情頁可能還會繼續跳轉下一級頁面,下一級頁面還會跳轉下一級頁面,當我們傳回上一級頁面時,我想保持前一次的所有查詢條件以及頁面的目前狀态。一想到頁面緩存,在​

​vue​

​​中我們就想到​

​keep-alive​

​​這個​

​vue​

​​的内置元件,在​

​keep-alive​

​​這個内置元件提供了一個​

​include​

​​的接口,隻要路由​

​name​

​比對上就會緩存目前元件。你或多或少看到不少很多處理這種業務代碼,本文是一篇筆者關于緩存多頁面的解決實踐方案,希望看完在業務中有所思考和幫助。

正文開始...

業務目标

首先我們需要确定需求,假設​

​A​

​​是清單頁,​

​A-1​

​​是詳情頁,​

​A-1-1​

​​,​

​A-1-2​

​​是詳情頁的子級頁面,​

​B​

​是其他路由頁面

我們用一個圖來梳理一下需求

keep-alive多級路由緩存最佳實踐

大概就是這樣的,一圖勝千言

然後我們開始,首頁面大概就是下面這樣

​pages/list/index.vue​

​​我們暫且把這個當成​

​A​

​頁面子產品吧

<template>
  <div class="list-app">
    <div><a href="javascript:void(0)" @click="handleToHello">to hello</a></div>
    <el-form ref="form" :model="condition" label-width="80px" inline>
      <el-form-item label="姓名">
        <el-input
          v-model="condition.name"
          clearable
          placeholder="請輸入搜尋姓名"
        ></el-input>
      </el-form-item>
      <el-form-item label="位址">
        <el-select v-model="condition.address" placeholder="請選擇位址">
          <el-option
            v-for="item in tableData"
            :key="item.name"
            :label="item.address"
            :value="item.address"
          >
          </el-option>
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button @click="featchList">重新整理</el-button>
      </el-form-item>
    </el-form>
    <el-table
      :data="tableData"
      style="width: 100%"
      row-key="id"
      border
      lazy
      :load="load"
      :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
    >
      <el-table-column prop="date" label="日期"> </el-table-column>
      <el-table-column prop="name" label="姓名"> </el-table-column>
      <el-table-column prop="address" label="位址"> </el-table-column>
      <el-table-column prop="options" label="操作">
        <template slot-scope="scope">
          <a href="javascript:void(0);" @click="handleView">檢視詳情</a>
          <a href="javascript:void(0);" @click="handleEdit(scope.row)">編輯</a>
        </template>
      </el-table-column>
    </el-table>
    <!--分頁-->
    <el-pagination
      @current-change="handleChangePage"
      background
      layout="prev, pager, next"
      :total="100"
    >
    </el-pagination>
    <!--彈框-->
    <list-modal
      title="編輯"
      width="50%"
      v-model="formParams"
      :visible.sync="dialogVisible"
      @refresh="featchList"
    ></list-modal>
  </div>
</template>      

我們再看下對應頁面的業務​

​js​

<!--pages/list/index.vue-->
<script>
import { sourceDataMock } from '@/mock';
import ListModal from './ListModal';


export default {
  name: 'list',
  components: {
    ListModal,
  },
  data() {
    return {
      tableData: [],
      cacheData: [], // 緩存資料
      condition: {
        name: '',
        address: '',
        page: 1,
      },
      dialogVisible: false,
      formParams: {
        date: '',
        name: '',
        address: '',
      },
    };
  },
  watch: {
    // eslint-disable-next-line func-names
    'condition.name': function (val) {
      if (val === '') {
        this.tableData = this.cacheData;
      } else {
        this.tableData = this.cacheData.filter(v => v.name.indexOf(val) > -1);
      }
    },
  },
  created() {
    this.featchList();
  },
  methods: {
    handleToHello() {
      this.$router.push('/hello-world');
    },
    handleChangePage(val) {
      this.condition.page = val;
      this.featchList();
    },
    handleSure() {
      this.dialogVisible = false;
    },
    load(tree, treeNode, resolve) {
      setTimeout(() => {
        resolve(sourceDataMock().list);
      }, 1000);
    },
    handleView() {
      this.$router.push('/detail');
    },
    handleEdit(row) {
      this.formParams = { ...row };
      this.dialogVisible = true;
      console.log(row);
    },
    featchList() {
      console.log('----start load data----', this.condition);
      const list = sourceDataMock().list;
      // 深拷貝一份資料
      this.cacheData = JSON.parse(JSON.stringify(list));
      this.tableData = list;
    },
  },
};
</script>      

以上業務代碼主要做了以下幾件事情

1、用​

​mockjs​

​模拟了一份清單資料

2、根據條件篩選對應的資料,分頁操作

3、從目前頁面跳轉子頁面,或者跳轉其他頁面,還有打開編輯彈框

首先我們要确認幾個問題,目前頁面的幾個特殊條件:

1、目前頁面的條件變化,頁面要更新

2、分頁器切換,頁面就需要更新

3、點選編輯彈框修改資料也是要更新

當我從清單去詳情頁,我從詳情頁傳回時,此時要緩存目前頁的所有資料以及頁面狀态,那要該怎麼做呢?

我們先看下首頁面

keep-alive多級路由緩存最佳實踐

大概需求已經明白,其實就是需要緩存條件以及分頁狀态,還有我展開子樹也需要緩存

我的大概思路就是,首先在路由檔案的裡放入一個辨別​

​cache​

​​,這個​

​cache​

​​裝載的就是目前的路由​

​name​

import Vue from 'vue';
import Router from 'vue-router';
import HelloWorld from '@/components/HelloWorld';
import List from '@/pages/list';
import Detail from '@/pages/detail';
Vue.use(Router);


export default new Router({
  routes: [
    {
      path: '/hello-world',
      name: 'HelloWorld',
      component: HelloWorld,
    },
    {
      path: '/',
      name: 'list',
      component: List,
      meta: {
        cache: ['list'],
      },
    },
    {
      path: '/detail',
      name: 'detail',
      component: Detail,
      meta: {
        cache: [],
      },
    },
  ],
});      

然後我們在​

​App.vue​

​​中的​

​router-view​

​​中加入​

​keep-alive​

​​,并且​

​include​

​指定對應路由頁面

<template>
  <div id="app">
    cache Page:{{ cachePage }}
    <keep-alive :include="cachePage">
      <router-view />
    </keep-alive>
  </div>
</template>      

我們看下​

​cachePage​

​​是從哪裡來的,我們通常把這種公用的變量放在全局​

​store​

​中管理

import store from '@/store';
export default {
  name: 'App',
  computed: {
    cachePage() {
      return store.state.global.cachePage;
    },
  },
};      

當我們進入這個頁面時就要根據​

​路由上設定的meta​

​​去确認目前頁面是否有緩存的​

​name​

​​,是以本質上也就成了,我如何設定​

​keep-alive​

​​中的​

​include​

​值

import store from '@/store';
export default {
  ...
  methods: {
    cacheCurrentRouter() {
      const { meta } = this.$route;
      if (meta) {
        if (meta.cache) {
          store.commit('global/setGlobalState', {
            cachePage: [
              ...new Set(store.state.global.cachePage.concat(meta.cache)),
            ],
          });
        } else {
          store.commit('global/setGlobalState', {
            cachePage: [],
          });
        }
      }
    },
  },
  created() {
    this.cacheCurrentRouter();
    this.$watch('$route', () => {
      this.cacheCurrentRouter();
    });
  },
};      

我們注意到,我們是根據​

​$route​

​​的​

​meta.cache​

​​然後去修改​

​store​

​​中的​

​cachePage​

​的

然後我們去​

​store/index.js​

​看下

// store/index.js
import Vue from 'vue';
import Vuex from 'vuex';
import { gloablMoudle } from './modules';
Vue.use(Vuex);
const initState = {};
const store = new Vuex.Store({
  state: initState,
  modules: {
    global: gloablMoudle,
  },
});
export default store;      

我們繼續找到最終設定​

​cachePage​

​​的​

​modules/global/index.js​

// modules/global/index.js
export const gloablMoudle = {
  namespaced: true,
  state: {
    cachePage: [],
  },
  mutations: {
    setGlobalState(state, payload) {
      Object.keys(payload).forEach((key) => {
        if (Reflect.has(state, key)) {
          state[key] = payload[key];
        }
      });
    },
  },
};      

是以我們可以看到​

​mutations​

​​有這樣的一段設定​

​state​

​​的操作​

​setGlobalState​

這塊代碼可以給大家分享下,為什麼我要循環​

​payload​

​​擷取對應的​

​key​

​​,然後再從​

​state​

​​中判斷是否有​

​key​

​,最後再指派?

在業務中我們看到不少這樣的代碼

export const gloablMoudle = {
  namespaced: true,
  state: {
    a: [],
    b: []
  },
  mutations: {
    seta(state, payload) {
      state.a = payload
    },
    setb(state, payload) {
      state.b = payload
    },
    ...
  },
  actions: {
    actA({commit, state}, payload) {
        commit('seta', payload)
    },
    actB({commit, state}, payload) {
        commit('setb', payload)
    }
    ...
  }
  ...
};      

在具體業務中大概就下面這樣

store.dispatch('actA', {})
store.dispatch('actB', {})      

是以你會看到如此重複的代碼,寫多了,貌似會越來越多,有沒有可以一勞永逸呢?

是以上面一塊代碼,你可以優化成下面這樣

export const gloablMoudle = {
  namespaced: true,
  state: {
    a: [],
    b: []
  },
  mutations: {
    setState(state, payload) {
       Object.keys(payload).forEach(key => {
           if (Reflect.has(state, key)) {
               state[key] = payload[key]
            }
       })
    },
  },
  actions: {
    setActionState({commit, state}, payload) {
      commit('setState', payload)  
    }
  }
};      

在業務代碼裡你就這樣做

store.dispatch('setActionState', {a: [1,2,3]})
store.dispatch('setActionState', {b: [1,2,3]})      

或者是下面這樣

store.commit('setState', {a: [1,2,3]})
store.commit('setState', {b: [1,2,3]})      

是以你會看到我這個檔案會非常的小,同樣達到目的,而且維護成本會降低很多,達到了我們代碼設計的高内聚,低耦合,一勞永逸的抽象思想。

回到正題,我們已經設定的全局​

​store​

​​的​

​cachePage​

我們注意到在​

​created​

​​裡面我們除了有去更新​

​cachePage​

​​,還有去監聽路由的變化,當我們切換路由去詳情頁面,我們是要根據路由辨別更新​

​cachePage​

​的。

import store from '@/store';
export default {
   ...
  methods: {
    cacheCurrentRouter() {
      const { meta } = this.$route;
      if (meta) {
        if (meta.cache) {
          store.commit('global/setGlobalState', {
            cachePage: [
              ...new Set(store.state.global.cachePage.concat(meta.cache)),
            ],
          });
        } else {
          store.commit('global/setGlobalState', {
            cachePage: [],
          });
        }
      }
    },
  },
  created() {
    this.cacheCurrentRouter();
    // 監聽路由,根據路由判斷目前是否應該要緩存
    this.$watch('$route', () => {
      this.cacheCurrentRouter();
    });
  },
};      

我們看下最終的效果

當我們從目前頁面切換到​

​tohello​

​頁面時,再回來,目前頁面就會重新被激活,然後重新再次緩存

如果我需要​

​detial/index.vue​

​也需要緩存,那麼我隻需要在路由檔案新增目前路由名稱即可

export default new Router({
  routes: [
    {
      path: '/hello-world',
      name: 'HelloWorld',
      component: HelloWorld,
    },
    {
      path: '/',
      name: 'list',
      component: List,
      meta: {
        cache: ['list'],
      },
    },
    {
      path: '/detail',
      name: 'detail',
      component: Detail,
      meta: {
        cache: ['detail'], // 這裡的名稱就是目前路由的名稱
      },
    },
  ],
});      

是以無論多少級頁面,跳轉哪些頁面,都可以輕松做到緩存,而且核心代碼非常簡單

keep-alive揭秘

最後我們看下​

​vue​

​​中這個内置元件​

​keep-alive​

​有什麼特征,以及他是如何實作緩存路由元件的

從官方文檔知道[1],當一個元件被​

​keep-alive​

​緩存時

1、該元件不會重新渲染

2、不會觸發​

​created​

​​,​

​mounted​

​鈎子函數

3、提供了一個可觸發的鈎子函數​

​activated​

​函數【目前元件緩存時會激活該鈎子】

4、​

​deactivated​

​離開目前緩存元件時觸發

我們注意到​

​keep-alive​

​​提供了3個接口​

​props​

  • include,被比對到的路由元件名(注意必須時元件的​

    ​name​

    ​)
  • exclude,排序不需要緩存的元件
  • max 提供最大緩存元件執行個體,設定這個可以限制緩存元件執行個體

不過我們注意,​

​keep-alive​

​​并不能緩在函數式元件裡使用,也就是是申明的​

​純函數元件​

​不會有作用

我們看下​

​keep-alive​

​這個内置元件是怎麼緩存元件的

在​

​vue2.0​

​​源碼目錄裡看到​

​/core/components/keep-alive.js​

首先我們看到,在​

​created​

​​鈎子裡綁定了兩個變量​

​cache​

​​,​

​keys​

created () {
    this.cache = Object.create(null)
    this.keys = []
  },      

然後我們會看到有在​

​mounted​

​​和​

​updated​

​​裡面有去調用​

​cacheVNode​

...
mounted () {
    this.cacheVNode()
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
},      

我們可以看到首先在​

​mounted​

​​裡就是​

​cacheVNode()​

​​,然後就是監聽​

​props​

​的變化

methods: {
    cacheVNode() {
      const { cache, keys, vnodeToCache, keyToCache } = this
      if (vnodeToCache) {
        const { tag, componentInstance, componentOptions } = vnodeToCache
        cache[keyToCache] = {
          name: getComponentName(componentOptions),
          tag,
          componentInstance,
        }
        keys.push(keyToCache)
        // prune oldest entry
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
        this.vnodeToCache = null
      }
    }
  },      

上面一段代碼大的大意就是,如果有​

​vnodeToCache​

​​存在,那麼就會将元件添加到​

​cache​

​​對象中,并且如果有​

​max​

​,則會對多餘的元件進行銷毀

在​

​render​

​​裡,我們看到會擷取預設的​

​slot​

​​,然後會根據​

​slot​

​擷取根元件

首先會判斷路由根元件上的是否有​

​name​

​​,沒有就不緩存,直接傳回​

​vnode​

render () {
    const slot = this.$slots.default
    const vnode: VNode = getFirstComponentChild(slot)
    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) {
      // check pattern
      const name: ?string = getComponentName(componentOptions)
      const { include, exclude } = this
      if (
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }
    ...
  }      

當再次通路時,就會從目前緩存對象裡去找,直接執行

​vnode.componentInstance = cache[key].componentInstance​

​​,元件執行個體會從​

​cache​

​對象中尋找

render () {
    const slot = this.$slots.default
    const vnode: VNode = getFirstComponentChild(slot)
    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
      const { cache, keys } = this
      const key: ?string = vnode.key == null
        // same constructor may get registered as different local components
        // so cid alone is not enough (#3269)
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key
      if (cache[key]) {
        // vnode.componentInstance 從cache對象中尋找
        vnode.componentInstance = cache[key].componentInstance
        // make current key freshest
        remove(keys, key)
        // 在删除的時候會有用到keys
        keys.push(key)
      } else {
        // delay setting the cache until update
        this.vnodeToCache = vnode
        this.keyToCache = key
      }
      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0])
  }      

總結

  • ​keep-alive​

    ​​緩存多級路由,主要思路根據路由的​

    ​meta​

    ​辨別,然後在​

    ​App.vue​

    ​元件中​

    ​keep-alive​

    ​包裹​

    ​router-view​

    ​路由标簽,我們通過全局​

    ​store​

    ​變量去控制​

    ​includes​

    ​判斷目前路由是否該被緩存,同時需要監聽路由判斷是否有需要緩存,通過設定全局​

    ​cachePage​

    ​去控制路由的緩存
  • 優化​

    ​store​

    ​資料流代碼,可以減少代碼,提高的代碼子產品的複用度
  • 當一個元件被緩存時,加載該緩存元件時是會觸發​

    ​activated​

    ​鈎子,當從一個緩存元件離開時,會觸發​

    ​deactivated​

    ​,在特殊場景可以在這兩個鈎子函數上做些事情
  • 簡略剖析​

    ​keep-alive​

    ​實作原理,從預設插槽中擷取元件執行個體,然後會根據是否有​

    ​name​

    ​,​

    ​include​

    ​以及​

    ​exclude​

    ​,判斷是否每次傳回​

    ​vnode​

    ​,如果​

    ​include​

    ​有需要緩存的元件,則會從​

    ​cache​

    ​對象中擷取執行個體對​

    ​vnode.componentInstance​

    ​進行重新指派優先從緩存對象中擷取
  • 本文示例 code example[2]

​​

keep-alive多級路由緩存最佳實踐

​​

參考資料

[1]從官方文檔知道:​​ https://v2.cn.vuejs.org/v2/api/#keep-alive​​