DIY 一個 Vuex 持久化插件

在做 Vue 相關項目的時候,總會遇到因爲頁面刷新導致 Store 內容丟失的情況。複雜的項目往往涉及大量的狀態需要管理,如果僅因爲一次刷新就需要全部重新獲取,代價也未免太大了。

那麼我們能不能對這些狀態進行本地的持久化呢?答案是可以的,社區裏也提供了不少的解決方案,如 vuex-persistedstatevuex-localstorage 等插件,這些插件都提供了相對完善的功能。當然除了直接使用第三方插件以外,我們自己來 DIY 一個也是非常容易的。

這個持久化插件主要有2個功能:

  1. 能夠選擇需要被持久化的數據。
  2. 能夠從本地讀取持久化數據並更新至 Store。

接下來我們會從上述兩個功能點出發,完成一個 Vuex 持久化插件。

Gist地址:https://gist.github.com/jrain...
在線體驗地址:https://codepen.io/jrainlau/p...

一、學習寫一個 Vuex 插件

引用 Vuex 官網 的例子:

Vuex 的 store 接受 plugins 選項,這個選項暴露出每次 mutation 的鉤子。Vuex 插件就是一個函數,它接收 store 作爲唯一參數:

const myPlugin = store => {
  // 當 store 初始化後調用
  store.subscribe((mutation, state) => {
    // 每次 mutation 之後調用
    // mutation 的格式爲 { type, payload }
  })
}

然後像這樣使用:

const store = new Vuex.Store({
  // ...
  plugins: [myPlugin]
})

一切如此簡單,關鍵的一點就是在插件內部通過 store.subscribe() 來監聽 mutation。在我們的持久化插件中,就是在這個函數內部對數據進行持久化操作。

二、允許用戶選擇需要被持久化的數據

首選初始化一個插件的主體函數:

const VuexLastingPlugin = function ({
  watch: '*',
  storageKey: 'VuexLastingData'
}) {
  return store => {}
}

插件當中的 watch 默認爲全選符號 *,允許傳入一個數組,數組的內容爲需要被持久化的數據的 key 值,如 ['key1', 'key2'] 等。接着便可以去 store.subscribe() 裏面對數據進行持久化操作了。

const VuexLastingPlugin = function ({
  watch: '*'
}) {
  return store => {
    store.subscribe((mutation, state) => {
      let watchedDatas = {}
      // 如果爲全選,則持久化整個 state 
      // 否則將只持久化被列出的 state
      if (watch === '*') {
        watchedDatas = state
      } else {
        watch.forEach(key => {
          watchedDatas[key] = state[key]
        })
      }
      // 通過 localStorage 持久化
      localStorage && localStorage.setItem(storageKey, JSON.stringify(watchedDatas))
    })
  }
}

按照 Vuex 的規範,有且只有通過 mutation 才能夠修改 state,於是按照上面的步驟,我們便完成了對數據進行實時持久化的工作。

這裏也有一個小問題,就是寫入 watch 參數的數組元素必須是 state 當中的最外層 key ,不支持形如 a.b.c 這樣的嵌套 key。這樣的功能顯然不夠完善,所以我們希望可以增加對嵌套 key 的支持。

新建一個工具函數 getObjDeepValue()

function getObjDeepValue (obj, keysArr) {
  let val = obj
  keysArr.forEach(key => {
    val = val[key]
  })
  return val
}

該函數接收一個對象和一個 key 值數組, 返回對應的值,我們來驗證一下:

var obj = {
  a: {
    name: 'aaa',
    b: {
      name: 'bbb',
      c: {
        name: 'ccc'
      }
    }
  }
}

getObjDeepValue(obj, 'a.b.c'.split('.'))

// => { name: "ccc" }

驗證成功以後,便可以把這個工具函數也放進 store.subscribe() 裏使用了:

    store.subscribe((mutation, state) => {
      let watchedDatas = {}
      if (watch === '*') {
        watchedDatas = state
      } else {
        watch.forEach(key => {
          // 形如 a.b.c 這樣的 key 會被保存爲 deep_a.b.c 的形式
          if (data.split('.').length > 1) {
            watchedDatas[`deep_${key}`] = getObjDeepValue(state, key.split('.'))
          } else {
            watchedDatas[key] = state[key]
          }
        })
      }
      
      localStorage && localStorage.setItem(storageKey, JSON.stringify(watchedDatas))
    })

經過這一改造,通過 watch 寫入的 key 值將支持嵌套的形式,整個插件將會更加靈活。

三、從本地讀取持久化數據並更新至 Store

從上面的步驟我們已經能夠靈活監聽 store 裏的數據並持久化它們了,接下來的工作就是完成如何在瀏覽器刷新之後去讀取本地持久化數據,並把它們更新到 store。

爲插件添加一個默認爲 true 的選項 autoInit,作爲是否自動讀取並更新 store 的開關。從功能上來說,刷新瀏覽器之後插件應該自動讀取 localStorage 裏面所保存的數據,然後把它們更新到當前的 store。關鍵的點就是如何把 deep_${key} 的值正確賦值到對應的地方,所以我們需要再新建一個工具函數 setObjDeepValue()

function setObjDeepValue (obj, keysArr, value) {
  let key = keysArr.shift()
  if (keysArr.length) {
    setObjDeepValue(obj[key], keysArr, value)
  } else {
    obj[key] = value
  }
}

該函數接收一個對象,一個 key 值數組,和一個 value ,設置對象對應 key 的值,我們來驗證一下:

var obj = {
  a: {
    name: 'aaa',
    b: {
      name: 'bbb',
      c: {
        name: 'ccc'
      }
    }
  }
}

setObjDeepValue(obj, ['a', 'b', 'c'], 12345)

/**
obj = {
  a: {
    name: 'aaa',
    b: {
      name: 'bbb',
      c: 12345
    }
  }
}
*/

有了這個工具方法,就可以正式操作 store 了。

    if (autoInit) {
      const localState = JSON.parse(storage && storage.getItem(storageKey))
      const storeState = store.state
      if (localState) {
        Object.keys(localState).forEach(key => {
          // 形如 deep_a.b.c 形式的值會被賦值到 state.a.b.c 中
          if (key.includes('deep_')) {
            let keysArr = key.replace('deep_', '').split('.')
            setObjDeepValue(storeState, keysArr, localState[key])
            delete localState[key]
          }
        })
        // 通過 Vuex 內置的 store.replaceState 方法修改 store.state
        store.replaceState({ ...storeState, ...localState })
      }
    }

上面這段代碼會在頁面初始化的時候讀取 storage 的值,然後把形如 deep_a.b.c 的值提取並賦值到 store.state.a.b.c 當中,最後通過 store.replaceState() 方法更新整個 store.state 的值。這樣便完成了從本地讀取持久化數據並更新至 Store 的功能。

四、案例測試

我們可以寫一個案例,來測試下這個插件的運行情況。

在線體驗:https://codepen.io/jrainlau/p...

圖片描述

App.vue

<template>
  <div id="app">
    <pre>{{$store.state}}</pre>

    <button @click="updateA">updateA</button>
    <button @click="updateX">UpdateX</button>
  </div>
</template>

<script>
export default {
  name: 'app',
  methods: {
    updateA () {
      let random = Math.random()
      this.$store.commit('updateA', {
        name: 'aaa' + random,
        b: {
          name: 'bbb' + random,
          c: {
            name: 'ccc' + random
          }
        }
      })
    },
    updateX () {
      this.$store.commit('updateX', { name: Math.random() })
    }
  }
}
</script>

store.js

import Vue from 'vue'
import Vuex from 'vuex'
import VuexPlugin from './vuexPlugin'

Vue.use(Vuex)

export default new Vuex.Store({
  plugins: [VuexPlugin({
    watch: ['a.b.c', 'x']
  })],
  state: {
    a: {
      name: 'aaa',
      b: {
        name: 'bbb',
        c: {
          name: 'ccc'
        }
      }
    },
    x: {
      name: 'xxx'
    }
  },
  mutations: {
    updateA (state, val) {
      state.a = val
    },
    updateX (state, val) {
      state.x = val
    }
  }
})

從案例可以看出,我們針對 state.a.b.c 和 state.x 進行了數據持久化。在整個 state.a 都被修改的情況下,僅僅只有 state.a.b.c 被存入了 localStorage ,數據恢復的時候也只修改了這個屬性。而 state.x 則整個被監聽,所以任何對於 state.x 的改動都會被持久化並能夠被恢復。

尾聲

這個 Vuex 插件僅在瀏覽器環境生效,未曾考慮到 SSR 的情況。有需要的同學可以在此基礎上進行擴展,就不再展開討論了。如果發現文章有任何錯誤或不完善的地方,歡迎留言和我一同探討。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章