使用面向对象以及设计模式的思维敲了一个Vue插件-VueStorage数据存储

691 阅读6分钟

在开发Vue的项目过程中, 我们难免会经常会把一些数据储存到我们的浏览器中,可能localStorageSessionStorage一下,亦或存

储到内存cache当中。但在存或取数据的过程,还得每次都要进行非空判断,以及JSON的序列化以及反序列化等等,不难打开控制台一

image-20220104113619182.png

窜窜红色的字符映入眼帘。有时还需要给存储的数据加上一个过期时间, 但这在WebStorage API 都没有怎么很好的实现。因此要使用弄一个Vue的储存插件,需要用的时候使用引入这个插件就行了。

Vue的插件机制

插件是自包含的代码,通常向 Vue 添加全局级功能。它可以是公开 install() 方法的 object,也可以是 function

插件的功能范围没有严格的限制——一般有下面几种:

  1. 添加全局方法或者 property。如:vue-custom-element
  2. 添加全局资源:指令/过渡等。如:vue-touch
  3. 通过全局 mixin 来添加一些组件选项。(如vue-router)
  4. 添加全局实例方法,通过把它们添加到 config.globalProperties 上实现。
  5. 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如 vue-router

VueStorage

这里呢,我们就是选择上面的第5点, 自己写一个库,然后暴露一个install()方法的object

该库要实现的功能点:

  • 可支持WebStorage API (localStorage和SessionStorage)

  • 可支持缓存到内存当中,程序运行环境不支持WebStorage API的时候该机制生效

  • 可支持设置数据缓存的有效期

  • 可支持缓存数据的命名空间(key的加个前缀),方便用户查看与操作

  • 可支持监听缓存数据的更改,暴露更改回调方法

  • 可支持Vue实例直接调用,实现Vue插件实现机制

项目搭建

  • 使用vite搭建一个vue项目

  • 命令窗口输入yarn crate vite

  • 选择对应的模板,创建成功出现如下图提示

image-20220104180243397.png

  • 输入cd vue-storage 以及 在VScode中打开该项目code ./ 如下图所示

image-20220104143847841.png

  • 在vue-storage文件夹下的新建一个example文件夹,把刚生成的所有文件放到该目录下,该文件的作用就是使用的自己写好的库的一个例子,也为了开发方式或者测试时方便调试

  • 在vue-storage文件下新建一个src文件夹, 在src文件夹下新建一个index.js文件以及一个storage文件夹

插件实现

src/index.js文件

  • src文件夹下的index.js需要执行一下操作

    1. 定义一个Storage对象并暴露一个install()方法

    2. 接受Vue插件实例传过来的参数并进行健壮性判断以及初始化调用

    3. 将Vue插件实例调用的存储实例挂载到Vue实例上

    4. Storage对象挂载到全局对象上(window或global)

  • index.js 具体代码

// 获取全局变量 - window或Node环境
const _global = typeof window !== 'undefined' ? window : global ?? {}

// 存储实现类型
const storageType = ['cache', 'session', 'local']

// 定义一个Storage对象
const Storage = {
  install: (Vue, options) => {
    // 接入插件传过来的参数以及配置默认参数
    const _options = {
      ...options,
      storage: options.storage || 'local',
      name: options.name || 'ls',
    }
    intercept(Vue, _options)
  },
}

// 将该对象挂载到全局对象上, 方便调用
_global.VueStorage = Storage

// 将该Storage对象暴露出去
export default Storage



src/interception.js文件

  • 在这个文件里面主要是实现一下功能

    1. 进行简单用户参数如数判断并给予相应的提示

    2. 根据用户输入的参数调用对应的储存API

    3. 当前程序运行环境不支持WebStorage API时,设置默认调用缓存到内存的存储API

    4. 将存储实例挂载到Vue的实例以及原型链上

  • src文件下新建constant.js文件用来定义一些常量以及interception.js文件

  • constant.js具体代码

// 获取全局变量 - window或Node环境
export const _global = typeof window !== 'undefined' ? window : global ?? {}

// 存储实现类型 cache: 存储内存中  session: SessionStorage  local: LocalStorage
export const storageType = ['cache', 'session', 'local']

export default {
  _global,
  storageType,
}

  • src/index.js做对应的修改
+ import { _global } from './constant'
+ import intercept from './interception'

- // 获取全局变量 - window或Node环境
- const _global = typeof window !== 'undefined' ? window : global ?? {}

- // 存储实现类型
- const storageType = ['cache', 'session', 'local']

// 定义一个Storage对象
const Storage = {
  install: (Vue, options) => {
    // 接入插件传过来的参数以及配置默认参数
    const _options = {
      ...options,
      storage: options.storage || 'local',
      name: options.name || 'ls',
    }
    intercept(Vue, _options)
  },
}

// 将该对象挂载到全局对象上, 方便调用
_global.VueStorage = Storage

// 将该Storage对象暴露出去
export default Storage


  • src/storage文件下新建四个文件:index.jsCacheStorage.jsWebStorage.jsStorageEvent.js

  • src/storage/index.js 新增以下代码

export * from './CacheStorage';
export * from './WebStorage';
export * from './StorageEvent';
  • interception.js具体代码
import { _global, storageType } from './constant'
import { CacheStorage, WebStorage } from './storage'

export const intercept = (Vue, options) => {
  const { storage, name } = options
  // 接收参数判断
  if (!storageType.includes(storage)) {
    throw new Error(`vue-storage: ${storage} 储存类型不支持`)
  }

  // 匹配到合适的存储实现API
  const { localStorage, sessionStorage } = _global ?? {}
  const storeMapping = {
    local: localStorage,
    session: sessionStorage,
    cache: CacheStorage,
  }

  const store = storeMapping[storage]
  // 设置默认储存方式
  if (!store) {
    store = CacheStorage
    console.error(
      `vue-storage: 你当前系统暂不${storage}该存储方式, 请使用cache储存方式`
    )
  }

  // 根据用户输入存储参数实例化一个对应的存储实例
  const entity = new WebStorage(store)

  // 参数合并
  entity.setOptions(
    Object.assign(entity.options, { namespace: '' }, options ?? {})
  )

  // 将本次存储对象挂载于Vue的根属性上
  Vue[name] = entity

  // 挂在原型上
  const { version = '2.x' } = Vue ?? {}
  const prototype =
    +version?.split('.')[0] > 2 ? Vue.config.globalProperties : Vue.prototype
  Object.defineProperty(prototype, `$${name}`, {
    get() {
      return entity
    },
  })
}

export default intercept

src/storage/WebStorage.js文件

  • 这文件的主要作用如下:
    1. 定义一个存储实例,初始化接收到的参数

    2. 为实例新增length属性,也是为清空功能做铺垫

    3. 给window全局对象进行事件绑定

    4. 实例参数的修改功能:setOptions()

    5. sessionStorage以及localStorage的增删改查的功能: set()get()remove()

    6. 根据用户设置的namespace清空存储的对应数据: clear()

    7. 实现对存储数据的变动做一个监听并触发相应的回调机制:on()off()

  • WebStorage.js具体代码
import { StorageEvent } from './StorageEvent'

export class WebStorage {
  constructor(storage) {
    this.storage = storage
    this.options = {
      namespace: '',
      events: ['storage'],
    }

    // 为实例新增length属性
    Object.defineProperty(this, 'length', {
      get() {
        return this.storage.length
      },
    })

    // 事件注册
    if (typeof window !== 'undefined') {
      for (const i in this.options.events) {
        // 非IE
        if (window.addEventListener) {
          window.addEventListener(
            this.options.events[i],
            StorageEvent.emit,
            false
          )
        } else if (window.attachEvent) {
          // IE 放弃吧 bro
          window.attachEvent(`on${this.options.events[i]}`, StorageEvent.emit)
        } else {
          window[`on${this.options.events[i]}`] = StorageEvent.emit
        }
      }
    }
  }

  // 合并options参数
  setOptions(options = {}) {
    this.options = Object.assign(this.options, options)
  }

  // 实现localStorage以及sessionStorage的写功能
  set(name, value, expire = null) {
    const stringifyNameData = JSON.stringify({
      value,
      // 数据保留时长
      expire: expire !== null ? new Date().getTime() + expire : null,
    })

    this.storage.setItem(`${this.options.namespace}${name}`, stringifyNameData)
  }

  // 实现localStorage以及sessionStorage的读功能
  get(name, defaultValue = null) {
    const nameData = this.storage.getItem(`${this.options.namespace}${name}`)

    if (nameData !== null) {
      const { value, expire } = JSON.parse(nameData)
      if (expire === null || expire >= new Date().getTime()) return value
      this.remove(name)
    }

    return defaultValue
  }

  // 通过索引编号来获取对应的值
  key(index) {
    return this.storage.key(index)
  }

  // 实现移除某个属性功能
  remove(name) {
    return this.storage.removeItem(`${this.options.namespace}${name}`)
  }

  // 实现清空功能
  clear() {
    if (this.length === 0) {
      return
    }

    const removedKeys = []

    for (let i = 0; i < this.length; i++) {
      const key = this.storage.key(i)
      // 筛选出储存的数据的key是以namespace开头的相关数据
      const regexp = new RegExp(`^${this.options.namespace}.+`, 'i')
      if (regexp.test(key) === false) {
        continue
      }

      removedKeys.push(key)
    }

    for (const key in removedKeys) {
      this.storage.removeItem(removedKeys[key])
    }
  }

  // 事件订阅
  on(name, callback) {
    StorageEvent.on(`${this.options.namespace}${name}`, callback)
  }

  // 取消事件订阅
  off(name, callback) {
    StorageEvent.on(`${this.options.namespace}${name}`, callback)
  }
}

src/storage/StorageEvent.js文件

  • 这个文件的主要实现功能:
    1. 定义一个存储事件实例 Storage

    2. 基于设计模式的发布-订阅模式来实现,事件订阅方法on(), 事件发布方式emit(), 取消事件订阅方法off()

  • StorageEvent.js具体源码
// 使用设计模式中的发布-订阅模式
// 订阅者:每个属性代表着一个事件对象, 每个事件对象存储着回调事件列表
const listeners = {}

export class StorageEvent {
  // 订阅一个回调方法
  static on(name, callback) {
    // 默认为[name] 属性赋值为空数组
    if (typeof listeners[name] === 'undefined') listeners[name] = []
    listeners[name].push(callback)
  }

  // 取消订阅回调事件列表中指定事件
  static off(name, callback) {
    if (listeners[name].length) {
      listeners[name].splice(listeners[name].indexOf(callback), 1)
    } else {
      listeners[name] = []
    }
  }

  // 发布事件
  static emit(event) {
    const evt = event ?? window.event

    if (typeof evt === 'undefined' || typeof e.key === 'undefined') return

    const getStorageData = (data) => {
      // 可能JSON.parse 会报错, so try catch
      try {
        return JSON.parse(data)
      } catch (error) {
        console.log('error', error)
        return data
      }
    }

    const trigger = (listener) => {
      const newValue = getStorageData(evt.newValue)
      const oldValue = getStorageData(evt.oldValue)
      listener(newValue, oldValue, e.url || e.uri)
    }

    const callbackList = listeners[e.key]
    callbackList?.length && callbackList.forEach(trigger)
  }
}


src/storage/CacheStorage.js

  • 该文件的主要做如下:
    1. 当WebStorage API是用不了的是默认调用该存储对象进行数据的存储

    2. 主要实现增删改查的功能:setItem()getItem()removeItem()clear()key()

  • CacheStorage.js的源代码如下:
let cache = {}

class CacheStorageInterface {
  constructor() {
    Object.defineProperty(this, 'length', {
      get() {
        return Object.keys(ls).length
      },
    })
  }

  getItem(name) {
    return cache[name] ?? null
  }

  setItem(name, value) {
    cache[name] = value
    return true
  }

  removeItem(name) {
    if (name in cache) {
      return delete cache[name]
    }
    return false
  }

  clear() {
    cache = {}
    return true
  }

  key(index) {
    const keys = Object.keys(cache)
    return keys[index] ?? null
  }
}

const CacheStorage = new CacheStorageInterface()

export { CacheStorage }

验证一下

  • example/src/main.js 引入我们的插件VueStorage并进行注册
import { createApp } from 'vue'
import App from './App.vue'
+ import VueStorage from '../../src'

createApp(App)
+  .use(VueStorage, { namespace: 'VueStorage__', name: 'vst', storage: 'local' })
  .mount('#app')

  • example/src/App.vue 进行设置存储的数据
<script setup>
// This starter template is using Vue 3 <script setup> SFCs
// Check out https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup
import HelloWorld from './components/HelloWorld.vue'

+ import { getCurrentInstance } from 'vue'

+ const { proxy } = getCurrentInstance()

+ proxy.$vst.set('testCount', { count: 80 })
</script>

<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <HelloWorld msg="Hello Vue 3 + Vite" />
</template>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>

  • example/src/components/HelloWord.vue获取存储的数据
<script setup>
+ import { ref, getCurrentInstance } from 'vue'

defineProps({
  msg: String,
})

+ const { proxy } = getCurrentInstance()

+ const initCount = proxy.$vst.get('testCount').count

+ const count = ref(initCount)
</script>


最终成果

image-20220104191257020.png

项目Git仓库:github.com/ZhengMaster…