中文 | English
基与 ES6 API Proxy 与 @vue/reactivity 实现, 像普通对象一样使用的 storage.
Prorage = Proxy + Storage
[Playground]: Stackblitz
- 可以像普通变量一样使用 Storage.
- 可定制化, 可以通过 Plugin 来实现大部分定制化需求.
- 无副作用, 支持 Tree Shaking.
- 基于
@vue/reactivity实现, 可以很好的配合 Vue 使用. - 更适合不使用 Vuex/Pinia 的 Vue 项目, 但也可以不配合 Vue 使用.
npm install @vue/reactivity
npm install prorage如果你已经安装了 Vue, 则不需要再安装 @vue/reactivity.
import { createStorage } from 'prorage'
const storage = createStorage()
storage.foo = 'foo'
delete foo
storage.bar = []
storage.bar.push('hello')import { createStorage, expiresPlugin } from 'prorage'
const storage = createStorage({
storage: localStorage,
stringify: JSON.stringify,
parse: JSON.parse,
saveFlush: 'async',
plugins: [expiresPlugin()],
prefix: 'prefix#',
})| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| storage | StorageLike | localStorage |
储存对象 |
| stringify | StringifyLike | JSON.stringify |
转换为 JSON 字符串的方法 |
| parse | ParseLike | JSON.parse |
解析 JSON 字符串的方法 |
| saveFlush | "sync" | "async" |
"async" |
保存的执行时机 |
| plugins | ProragePlugin[] | [] |
插件 |
| prefix | string | 储存键名前缀 |
StorageLike, 比如localStorage,sessionStorage. 应具有方法:getItem,setItem,removeItem. 其中getItem必须是同步方法.saveFlush为async时,多次操作会合并为一次保存,而sync时则会在每次操作后立即保存。plugins相关的内容, 请阅读后文的 内置 Plugin 与 Plugin 的开发.
清空数据.
storage.clear() // 清空所有数据(符合 preifx 的数据)
storage.clear('foo') // 清空 foo 命名空间下的数据重新读取数据.
storage.reload('foo')需要注意的是旧数据会脱离控制(被覆盖). 举个例子:
storage.test = { a: 1 }
const temp = storage.test
temp.a = 2 // 这是有效的
storage.reload('test')
temp.a = 3 // 这是无效的
temp === storage.test // false在调用 reload 后 storage.test 上的引用被替换为重新读取的数据, 原先储存的 temp 不再是 storage.test 的数据. 故再对 temp 对象进行修改, 不会再影响 storage.test.
addEventListener('storage', ({ key }) => storage.reload(key))主动保存数据, 通常不需要主动调用.
storage.save('foo')与 Vue 的 watch 使用方式相同, 但做了较多阉割. 该 API 主要面向不使用 Vue 的项目.
watch(
() => storage,
() => {
console.log('change')
}, {
deep: true,
flush: 'async'
}
)- 第一个参数
source, 仅支持函数形式. - 第二个参数
callback, 未提供oldValue与newValue. - 第三个参数
options,deep与immediate和 Vue 基本一致.flush仅支持sync与async,sync和 Vue 一致,async异步执行.
为数据增加附加属性. 作为一个基础 Plugin, 不需要声明使用.
import { createStorage, useExtra } from 'prorage'
const storage = createStorage()
storage.foo = useExtra('bar', {
test: 'hello world'
})
getExtra(storage, 'foo') // { test: 'hello world' }生成一个具有附加属性的数据.
function useExtra<T>(value: T, extra: Record<string, unknown>): T获得 target 对象上 key 键名绑定的附加属性.
function getExtra(target: object, key: string | symbol): Record<string, unknown>允许为数据设置有效期.
import { createStorage, expiresPlugin, useExpires } from 'prorage'
const storage = createStorage({
plugins: [
expiresPlugin({
checkInterval: 1000,
})
]
})
storage.foo = useExpires('bar', { days: 7 })| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| checkInterval | 'none' | 'raf' | number |
- 过期数据并不会立即删除, 而是下次被访问时才会被删除.
checkInterval为"raf"或number时, 会通过requestAnimationFrame/setTimeout定期检查数据是否过期. 只有被访问过的数据才会加入到检查队列中.- 使用
setTimeout时有补偿机制, 当数据过期时间 - Date.now()大于checkInterval, 则下次运行时间取数据过期时间 - Date.now().
function useExpires<T>(value: T, expires: ExpiresDate): T
type ExpiresDate = number | Date | ExpiresDateOptions
type ExpiresDateOptions = {
milliseconds?: number
seconds?: number
minutes?: number
hours?: number
days?: number
months?: number
years?: number
}expires为number, 时间戳, 作为过期的绝对时间.expires为Date, 作为过期的绝对时间.expires为ExpiresDateOptions, 作为过期的相对时间(相对当前时间). 其中months按自然月天数计算, 不等同于30天.
将数据转换为更适合储存的格式.
import { createStorage, translatePlugin } from 'prorage'
const storage = createStorage({
plugins: [translatePlugin()]
})
storage.foo = new Date()
storage.bar = 123n
storage.baz = /test/gi
storage.qux = Infinity默认支持的数据类型有: BigInt, NaN/Infinity/-Infinity, Date, RegExp.
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
| dictionary | TranslateDictionary[] | [] |
字典 |
import { createStorage, translatePlugin } from 'prorage'
const storage = createStorage({
plugins: [
translatePlugin({
dictionary: [
{
name: 'Symbol',
test: (value) => typeof value === 'symbol',
stringify: (value) => value.toString(),
parse: (value) => {
const _Symbol = (value) => {
try {
return new Function(`return ${value}`)()
} catch (e) {
return typeof value === 'symbol'
? value
: Symbol.for(String(value))
}
}
const _value = value.replace(/^Symbol\((.*)\)$/, '_Symbol("$1")')
return new Function('_Symbol', `return ${_value}`)(_Symbol)
},
}
]
})
]
})
storage.foo = Symbol.for('123')| 参数 | 类型 | 说明 |
|---|---|---|
| name | string | 唯一标识 |
| test | (value: unknown) => boolean | 判断数据是否由该字典进行处理 |
| stringify | (value: any) => any | 转换为储存格式 |
| parse | (value: any) => any | 还原数据 |
name需要唯一, 内置的标识有:BigInt,Number,Date,RegExp.- 按数组顺序进行
test(内置的追加在数组末尾), 匹配后该数据将不再进行其他转换操作.
键名支持和对象一样, 但 symbol 作为键名 JSON.stringify 时会被忽略.
键值支持情况如下.
| 数据类型 | 基础支持 | with translatePlugin |
|---|---|---|
| undefined | ✔️ | ✔️ |
| null | ✔️ | ✔️ |
| String | ✔️ | ✔️ |
| Boolean | ✔️ | ✔️ |
| Number | ✔️ | ✔️ |
| BigInt | ❌ | ✔️ |
| Symbol | ❌ | 可以支持 Symbol.for (需用户配置) |
| 数据类型 | 基础支持 | with translatePlugin | 说明 |
|---|---|---|---|
| 基础的 Object | ✔️ | ✔️ | |
| Array | ✔️ | ✔️ | |
| Date | ❌ | ✔️ | |
| RegExp | ❌ | ✔️ | |
| Function | ❌ | 可以勉强支持 (需用户配置) | 会丢失作用域 |
| Set | ❌ | ❌ | 实现成本与收益不匹配 |
| Map | ❌ | ❌ | 实现成本与收益不匹配 |
| WeakSet | ❌ | ❌ | 没有实现价值 |
| WeakMap | ❌ | ❌ | 没有实现价值 |
可以借助 flatted 之类的 JSON 库来解决循环引用的问题.
import { stringify, parse, } from 'flatted'
import { createStorage } from 'prorage'
const storage = createStorage({
stringify,
parse,
})
storage.test = {}
storage.test.circular = storage.testimport { createStorage } from 'prorage'
type MyStorage = {
foo: string
bar: number
}
const storage = createStorage<MyStorage>()就和 @vue/reactivity 在 React 中使用一样, 实现方式很多, 以下是一种简单的使用示例: Prorage With React - StackBlitz.
若使用的 vue.xxx.global.js 或是 vue.xxx-browser.js 版本的 Vue, 会导致与 prorage 所依赖的 @vue/reactivity 不是同一份代码, 使得两者 trigger 事件相互独立. 应避免使用这两类版本的 Vue.