写一个支持Vue响应式的localStorage插件

2,730 阅读3分钟

前端的日常开发中,经常需要使用到localStorage存储,在使用Vue作为开发框架时,希望能与Vue的响应式系统集成到一起,可以像vue-router/vuex类似的模式使用。

效果

响应式设计

vue的响应式原理,是为一个对象添加特定的属性描述符,劫持它所有属性的getter/setter。 在这里,我们定义一个对象_storage,遍历它的所有属性,通过官方暴露出的方法——Vue.util.defineReactive,把_storage变成observable了,那么只要修改_storage某个key的值,依赖(模板/数据)就能实时更新。

接下来,我们需要把_storage对象与本地的localStorage做实时同步。粗糙的办法是每次更新_storage时手动更新localStorage,这就不符合我们做这个插件的初衷了,我们的愿景是自动同步。

我们也像Vue那样使用Object.defineProperty的功能,在getter/setter里调用对应的localStorage api不就能解决问题了?可是这样的话_storage的每一个属性都被Object.defineProperty两次了,那么前面一次就会被覆盖,无法生效啊!

查看源码可以看到,Vue.util.defineReactive会保存前一次的getter/setter(如果有的话),在未来每一次的getter/setter中都会执行一次,在这里我们就可以调用相关的localStorage api了。

Vue插件api设计

1. 初始化配置

配置项

  • 定义所有在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指定类型和默认值。

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中,实现页面刷新不丢失数据。

考虑到数据没有定义默认值,通过keyMap为每种数据类型定义一个默认值。最终得到一个_storage对象,且赋值到this.storage供外部实例调用。

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中去读取/写入。

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

参考