DIY 一個 Vuex 持久化外掛

Jrain發表於2019-04-02

DIY 一個 Vuex 持久化外掛

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

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

這個持久化外掛主要有2個功能:

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

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

Gist地址:gist.github.com/jrainlau/36… 線上體驗地址:codepen.io/jrainlau/pe…

一、學習寫一個 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 的支援。

新建一個工具函式 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 的功能。

四、案例測試

我們可以寫一個案例,來測試下這個外掛的執行情況。

線上體驗:codepen.io/jrainlau/pe…

DIY 一個 Vuex 持久化外掛

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 的情況。有需要的同學可以在此基礎上進行擴充套件,就不再展開討論了。如果發現文章有任何錯誤或不完善的地方,歡迎留言和我一同探討。

相關文章