DIY 一個 Vuex 持久化插件

在作 Vue 相關項目的時候,總會遇到由於頁面刷新致使 Store 內容丟失的狀況。複雜的項目每每涉及大量的狀態須要管理,若是僅由於一次刷新就須要所有從新獲取,代價也未免太大了。javascript

那麼咱們能不能對這些狀態進行本地的持久化呢?答案是能夠的,社區裏也提供了很多的解決方案,如 vuex-persistedstatevuex-localstorage 等插件,這些插件都提供了相對完善的功能。固然除了直接使用第三方插件之外,咱們本身來 DIY 一個也是很是容易的。html

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

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

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

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

1、學習寫一個 Vuex 插件

引用 Vuex 官網 的例子:git

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

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

而後像這樣使用:vuex

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

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

2、容許用戶選擇須要被持久化的數據

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

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

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

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 值將支持嵌套的形式,整個插件將會更加靈活。

3、從本地讀取持久化數據並更新至 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 的功能。

4、案例測試

咱們能夠寫一個案例,來測試下這個插件的運行狀況。

在線體驗: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 的狀況。有須要的同窗能夠在此基礎上進行擴展,就再也不展開討論了。若是發現文章有任何錯誤或不完善的地方,歡迎留言和我一同探討。

相關文章
相關標籤/搜索