在开发Vue的项目过程中, 我们难免会经常会把一些数据储存到我们的浏览器中,可能localStorage或SessionStorage一下,亦或存
储到内存cache当中。但在存或取数据的过程,还得每次都要进行非空判断,以及JSON的序列化以及反序列化等等,不难打开控制台一
窜窜红色的字符映入眼帘。有时还需要给存储的数据加上一个过期时间, 但这在WebStorage API 都没有怎么很好的实现。因此要使用弄一个Vue的储存插件,需要用的时候使用引入这个插件就行了。
Vue的插件机制
插件是自包含的代码,通常向 Vue 添加全局级功能。它可以是公开 install() 方法的 object,也可以是 function
插件的功能范围没有严格的限制——一般有下面几种:
- 添加全局方法或者 property。如:vue-custom-element
- 添加全局资源:指令/过渡等。如:vue-touch)
- 通过全局 mixin 来添加一些组件选项。(如vue-router)
- 添加全局实例方法,通过把它们添加到
config.globalProperties上实现。 - 一个库,提供自己的 API,同时提供上面提到的一个或多个功能。如 vue-router
VueStorage
这里呢,我们就是选择上面的第5点, 自己写一个库,然后暴露一个install()方法的object。
该库要实现的功能点:
-
可支持WebStorage API (localStorage和SessionStorage)
-
可支持缓存到内存当中,程序运行环境不支持WebStorage API的时候该机制生效
-
可支持设置数据缓存的有效期
-
可支持缓存数据的命名空间(key的加个前缀),方便用户查看与操作
-
可支持监听缓存数据的更改,暴露更改回调方法
-
可支持Vue实例直接调用,实现Vue插件实现机制
项目搭建
-
使用
vite搭建一个vue项目 -
命令窗口输入
yarn crate vite -
选择对应的模板,创建成功出现如下图提示
- 输入
cd vue-storage以及 在VScode中打开该项目code ./如下图所示
-
在vue-storage文件夹下的新建一个example文件夹,把刚生成的所有文件放到该目录下,该文件的作用就是使用的自己写好的库的一个例子,也为了开发方式或者测试时方便调试
-
在vue-storage文件下新建一个
src文件夹, 在src文件夹下新建一个index.js文件以及一个storage文件夹
插件实现
src/index.js文件
-
在
src文件夹下的index.js需要执行一下操作-
定义一个
Storage对象并暴露一个install()方法 -
接受Vue插件实例传过来的参数并进行健壮性判断以及初始化调用
-
将Vue插件实例调用的存储实例挂载到Vue实例上
-
将
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文件
-
在这个文件里面主要是实现一下功能
-
进行简单用户参数如数判断并给予相应的提示
-
根据用户输入的参数调用对应的储存API
-
当前程序运行环境不支持WebStorage API时,设置默认调用缓存到内存的存储API
-
将存储实例挂载到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.js、CacheStorage.js、WebStorage.js、StorageEvent.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文件
- 这文件的主要作用如下:
-
定义一个存储实例,初始化接收到的参数
-
为实例新增length属性,也是为清空功能做铺垫
-
给window全局对象进行事件绑定
-
实例参数的修改功能:
setOptions() -
sessionStorage以及localStorage的增删改查的功能:
set()、get()、remove() -
根据用户设置的namespace清空存储的对应数据:
clear() -
实现对存储数据的变动做一个监听并触发相应的回调机制:
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文件
- 这个文件的主要实现功能:
-
定义一个存储事件实例
Storage -
基于设计模式的发布-订阅模式来实现,事件订阅方法
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
- 该文件的主要做如下:
-
当WebStorage API是用不了的是默认调用该存储对象进行数据的存储
-
主要实现增删改查的功能:
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>
最终成果
项目Git仓库:github.com/ZhengMaster…