寫一個支持Vue響應式的localStorage插件

前端的平常開發中,常常須要使用到localStorage存儲,在使用Vue做爲開發框架時,但願能與Vue的響應式系統集成到一塊兒,能夠像vue-router/vuex相似的模式使用。前端

效果

響應式設計

vue的響應式原理,是爲一個對象添加特定的屬性描述符,劫持它全部屬性的getter/setter。 在這裏,咱們定義一個對象_storage,遍歷它的全部屬性,經過官方暴露出的方法——Vue.util.defineReactive,把_storage變成observable了,那麼只要修改_storage某個key的值,依賴(模板/數據)就能實時更新。vue

接下來,咱們須要把_storage對象與本地的localStorage作實時同步。粗糙的辦法是每次更新_storage時手動更新localStorage,這就不符合咱們作這個插件的初衷了,咱們的願景是自動同步。git

咱們也像Vue那樣使用Object.defineProperty的功能,在getter/setter裏調用對應的localStorage api不就能解決問題了?但是這樣的話_storage的每個屬性都被Object.defineProperty兩次了,那麼前面一次就會被覆蓋,沒法生效啊!github

查看源碼能夠看到, Vue.util.defineReactive會保存前一次的 getter/setter(若是有的話),在將來每一次的 getter/setter中都會執行一次,在這裏咱們就能夠調用相關的 localStorage api了。

Vue插件api設計

1. 初始化配置

配置項web

  • 定義全部在localStorage中使用到的key-value,且須要指定數據類型,由於保存在本地存儲的數據只能是字符串,因此取本地web存儲數據時要根據原來的數據類型進行解析,例如對象須要JSON.parse
  • key的公共前綴namespace,方便標識某個項目使用的web存儲
const storage = Vue.use(Storage, 'my-namespace', {
  string: {
    type: String,
    default: 'test'
  },
  number: {
    type: Number
  },
  object: {
    type: Object,
    default: {
      hello: 'world'
    }
  }
})

class Storage {
  static install (Vue, nameSpace, options) {

    if (typeof nameSpace === 'object') {
      options = nameSpace
      nameSpace = 'vue-storage'
    }

    return new Storage(Vue, nameSpace, options)
  }
}
複製代碼

插件定義Storage類,且提供一個install方法給Vue進行註冊。key/value提供typedefault指定類型和默認值。vue-router

class Storage {
  constructor (Vue, nameSpace, options = {}) {
    const self = this
    this.Vue = Vue
    this.nameSpace = `${nameSpace}-`
    this.options = options

    // 刷新頁面時,把本地storage從新取出來
    const cacheStorage = Object.keys(window.localStorage)
      .filter(key => new RegExp(`^${this.nameSpace}`).test(key))
      .reduce((acc, key) => Object.assign(acc, {
        [ key.replace(this.nameSpace, '') ]: window.localStorage[key]
      }), {})

    // 每種數據類型的默認值
    const keyMap = [
      [ String, '' ],
      [ Boolean, '' ],
      [ Number, '' ],
      [ Array, [] ],
      [ Object, {} ],
    ]
    const map = this.typeMap = new Map(keyMap)

    let _storage = this.storage = {
      ...(
        Object.keys(options).reduce((acc, key) => {

          const { type, default: val } = options[key]
          if (!type) {
            Vue.util.warn(`type of the field 'key' is required`)
            return acc
          }

          return Object.assign(acc, {
            [key]: val === undefined ? map.get(type) : val
          })
        }, {})
      ),
      ...cacheStorage
    }
}
複製代碼

Storage實例化的過程,先把本地web存儲的數據提取出來,合併到配置的key/value中,實現頁面刷新不丟失數據。vuex

考慮到數據沒有定義默認值,經過keyMap爲每種數據類型定義一個默認值。最終獲得一個_storage對象,且賦值到this.storage供外部實例調用。api

2. 代理數據 getter/setter

class Storage {
  
  constructor () {
    
    Object.keys(_storage).forEach(key => {
  
      try {
    
        const val = _storage[key]
        this.set(key, val)
    
      } catch (e) {
    
        Vue.util.warn('vue-storage-error', e)
      }
    
      // 把_storage中key對應的value取值代理到localStorage中去
      Object.defineProperty(_storage, key, {
        get: () => self.get(key),
        set: (val) => self.set(key, val),
        configurable: true
      })
    
      // 定義可觀察對象
      Vue.util.defineReactive(_storage, key, _storage[key])
    })
  }

  get (key) {

    let val = window.localStorage.getItem(this._getKey(key))
    val = this._parse(key, val)
    return val
  }

  set (key, val) {

    try {

      val = typeof val === 'object' ? JSON.stringify(val) : val
      window.localStorage.setItem(this._getKey(key), val)

    } catch (e) {
      Vue.util.warn(`storage setting fail, please check the value`)
    }
  }
}
複製代碼

經過Object.defineProperty_storage中key對應的value的讀取/寫入實際是在localStorage中去讀取/寫入。markdown

3.暴露屬性接口

class Storage {
  
  constructor () {
    
    const self = this

    Object.defineProperty(Vue.prototype, '$storage', {
      get: () => _storage
    })

    // 代理Storage實例
    Object.defineProperty(Vue.prototype, '$storager', {
      get: () => self
    })

    Vue.storage = _storage
    Vue.storager = self

  }
}
複製代碼

參照vue-router那樣,對Vue/Vue實例暴露_storage可觀察數據和Storage實例,實如今組件內能夠經過this.$storage獲取_storage框架

4.提供插件公共方法

class Storage {
  
  get (key) {
    let val = window.localStorage.getItem(this._getKey(key))
    val = this._parse(key, val)
    return val
  }

  set (key, val) {

    try {

      val = typeof val === 'object' ? JSON.stringify(val) : val
      window.localStorage.setItem(this._getKey(key), val)

    } catch (e) {
      Vue.util.warn(`storage setting fail, please check the value`)
    }
  }

  remove (key) {
    window.localStorage.removeItem(this._getKey(key))
  }

  clear () {

    Object.keys(this.storage).forEach(key => {
      const lsKey = this._getKey(key)

      if (window.localStorage.hasOwnProperty(lsKey)) {
        window.localStorage.removeItem(lsKey)
      }
    })
  }
}
複製代碼

提供get/set/remove/clear方法操做localStorage。相似於getItem/setItem/removeItem/clear

參考

相關文章
相關標籤/搜索