pinia-plugin-persistedstate 如何实现的持久化存储?`pinia-plugin-persist
先废话一句,今天看了动漫 《斩神之凡尘神域》第 03 集。说句实话,看哭了,热泪盈眶了.... 看弹幕,应该不止我一个人这样......
感兴趣的小伙伴、或者喜欢动漫的小伙伴,也可以去看看...
接下来,进入本文正题吧!(其实我是想发上述这个图,才决定写这一篇的。😄 😂)
背景
期间有个小伙伴评论了:
瞬间让我想起了 pinia-plugin-persistedstate,之前做 Vue3 项目的时候,就是用的 pinia + pinia-plugin-persistedstate
。之前只是用了,并没有去了解其原理,真正怎么实现的?
那么接下来就一探究竟。
pinia-plugin-persistedstate
官方文档:prazdevs.github.io/pinia-plugi… github 源码:github.com/prazdevs/pi…
简介
pinia-plugin-persistedstate
是一个插件,用于在 Vue 3 应用中使用 Pinia 状态管理库时实现状态持久化。这个插件可以将 Pinia 状态持久化到 localStorage
或 sessionStorage
,从而在页面刷新或重新加载后保留状态。
源码分析
pinia-plugin-persistedstate
功能挺简洁的,源码应该也不是很多,那我们就一起看看...
1 说 parsePersistence
函数:
function parsePersistence(factoryOptions: PersistedStateFactoryOptions, store: Store) {
return (o: PersistedStateOptions): Persistence | null => {
try {
const {
storage = localStorage,
beforeRestore = undefined,
afterRestore = undefined,
serializer = {
serialize: JSON.stringify,
deserialize: JSON.parse,
},
key = store.$id,
paths = null,
debug = false,
} = o
return {
storage,
beforeRestore,
afterRestore,
serializer,
key: (factoryOptions.key ?? (k => k))(typeof key == 'string' ? key : key(store.$id)),
paths,
debug,
}
}
catch (e) {
if (o.debug)
console.error('[pinia-plugin-persistedstate]', e)
return null
}
}
}
- 该函数的目的是
解析持久化选项并生成特定的持久化配置
。具体来说,它返回一个函数,该函数接受一个持久化选项对象并返回一个Persistence
对象或null
。 parsePersistence
函数接收两个参数:factoryOptions
: 全局的持久化配置选项。store
: 当前的 Pinia Store 实例。
- 在
try
语句块中,从o
(即PersistedStateOptions
)对象中解构出各个属性,并为它们设置默认值:storage
: 持久化的存储方式,默认是localStorage
。beforeRestore
: 在恢复之前调用的函数,默认是undefined
。afterRestore
: 在恢复之后调用的函数,默认是undefined
。serializer
: 序列化和反序列化的方式,默认使用JSON.stringify
和JSON.parse
。key
: 存储的键,默认是 Store 的id
。paths
: 要持久化的状态路径,默认是null
。debug
: 调试模式,默认是false
。
- 返回一个
Persistence
对象,该对象包含了解构出来的属性。特别地,对key
属性进行处理,如果key
是一个函数,则调用该函数生成键名(灵活的键名生成)。 - 在
catch
块中,如果解析过程发生错误,并且debug
模式开启,则输出错误信息到控制台,并返回null
。(优秀的代码就是会处理好报错)
2 说 hydrateStore
函数
function hydrateStore(
store: Store,
{ storage, serializer, key, debug }: Persistence,
) {
try {
const fromStorage = storage?.getItem(key)
if (fromStorage)
store.$patch(serializer?.deserialize(fromStorage))
}
catch (e) {
if (debug)
console.error('[pinia-plugin-persistedstate]', e)
}
}
hydrateStore
函数的目的是从持久化存储中恢复状态到 Pinia Store 中。- 参数 1:
store: Store
: Pinia 的 Store 实例,表示当前的 Store 对象。 - 参数 2:
{ storage, serializer, key, debug }
: 从Persistence
对象中解构出来的属性。storage
: 持久化存储的方式,通常是localStorage
或sessionStorage
。serializer
: 序列化和反序列化的方法,通常包含serialize
和deserialize
两个方法。key
: 用于存储状态的键名。debug
: 是否开启调试模式。
- 在
try
语句块中,尝试从storage
中获取键名为key
的数据。 - 如果
fromStorage
存在(即存储中有数据),则使用serializer
的deserialize
方法将其反序列化,并使用 Pinia 的$patch
方法将反序列化后的数据合并到当前的 Store 状态中。 - 错误处理。
3 说 persistState
函数
function persistState(
state: StateTree,
{ storage, serializer, key, paths, debug }: Persistence,
) {
try {
const toStore = Array.isArray(paths) ? pick(state, paths) : state
storage!.setItem(key!, serializer!.serialize(toStore as StateTree))
}
catch (e) {
if (debug)
console.error('[pinia-plugin-persistedstate]', e)
}
}
persistState
函数的目的是将 Pinia Store 的状态持久化存储到指定的存储位置- 参数 1:
state: StateTree
: Pinia Store 的状态树。 - 参数 2:
{ storage, serializer, key, paths, debug }
: 从Persistence
对象中解构出来的属性。具体就不重复说了。 - 如果
paths
是一个数组,则使用pick
函数从state
中选择指定的路径;否则,将整个state
用于存储。(下边再说一下pick
函数)。 - 将要存储的状态
toStore
进行序列化(使用serializer.serialize
方法),并使用指定的key
将其存储到storage
中。 - 错误处理。
4 说 createPersistedState
函数
export function createPersistedState(
factoryOptions: PersistedStateFactoryOptions = {},
): PiniaPlugin {
return (context: PiniaPluginContext) => {
const { auto = false } = factoryOptions
const {
options: { persist = auto },
store,
pinia,
} = context
if (!persist)
return
// HMR handling, ignores stores created as "hot" stores
/* c8 ignore start */
if (!(store.$id in pinia.state.value)) {
// @ts-expect-error `_s is a stripped @internal`
const original_store = pinia._s.get(store.$id.replace('__hot:', ''))
if (original_store)
Promise.resolve().then(() => original_store.$persist())
return
}
/* c8 ignore stop */
const persistences = (
Array.isArray(persist)
? persist.map(p => normalizeOptions(p, factoryOptions))
: [normalizeOptions(persist, factoryOptions)]
).map(parsePersistence(factoryOptions, store)).filter(Boolean) as Persistence[]
store.$persist = () => {
persistences.forEach((persistence) => {
persistState(store.$state, persistence)
})
}
store.$hydrate = ({ runHooks = true } = {}) => {
persistences.forEach((persistence) => {
const { beforeRestore, afterRestore } = persistence
if (runHooks)
beforeRestore?.(context)
hydrateStore(store, persistence)
if (runHooks)
afterRestore?.(context)
})
}
persistences.forEach((persistence) => {
const { beforeRestore, afterRestore } = persistence
beforeRestore?.(context)
hydrateStore(store, persistence)
afterRestore?.(context)
store.$subscribe(
(
_mutation: SubscriptionCallbackMutation<StateTree>,
state: StateTree,
) => {
persistState(state, persistence)
},
{
detached: true,
},
)
})
}
}
createPersistedState
算是是一个用语函数。- 参数:
factoryOptions
: 全局的持久化配置选项,默认是一个空对象。 - 返回一个接收
PiniaPluginContext
的函数,结合使用方式,这个函数会在 Store 初始化时调用。 auto
: 全局持久化配置中的自动持久化选项,默认为false
。persist
: Store 局部配置中的持久化选项,如果没有配置,则使用全局配置中的auto
选项。- 如果
persist
为false
,则不进行持久化,直接返回。 - 处理 HMR 情况下的 Store 恢复。如果 Store 是热替换创建的,则尝试恢复原始 Store 的持久化状态。
normalizeOptions
: 规范化持久化选项,确保每个选项都有合理的默认值。(下边会说) 9.parsePersistence
: 解析持久化选项,生成Persistence
对象。filter(Boolean)
: 过滤掉null
或undefined
的Persistence
对象。- 为 Store 添加
$persist
方法,用于手动持久化状态。 - 为 Store 添加
$hydrate
方法,用于手动恢复持久化状态。该方法支持在恢复前后运行钩子函数。 - 初始状态恢复和订阅状态变化。订阅 Store 的状态变化,当状态变化时自动持久化。(发布订阅)
5 说 normalizeOptions
函数
export function normalizeOptions(
options: boolean | PersistedStateOptions | undefined,
factoryOptions: PersistedStateFactoryOptions,
): PersistedStateOptions {
options = isObject(options) ? options : Object.create(null)
return new Proxy(options as object, {
get(target, key, receiver) {
if (key === 'key')
return Reflect.get(target, key, receiver)
return (
Reflect.get(target, key, receiver)
|| Reflect.get(factoryOptions, key, receiver)
)
},
})
}
normalizeOptions
函数的目的是规范化持久化配置选项,将全局配置与局部配置合并,并提供默认值。- 竟然是这么来合并局部和全局配置。(该源码作者 Vue3 源码肯定学的不错!)
6 说 pick
函数
function get(state: StateTree, path: Array<string>): unknown {
return path.reduce((obj, p) => {
return obj?.[p]
}, state)
}
function set(state: StateTree, path: Array<string>, val: unknown): StateTree {
return (
(path.slice(0, -1).reduce((obj, p) => {
if (/^(__proto__)$/.test(p))
return {}
else return (obj[p] = obj[p] || {})
}, state)[path[path.length - 1]] = val),
state
)
}
export function pick(baseState: StateTree, paths: string[]): StateTree {
return paths.reduce<StateTree>((substate, path) => {
const pathArray = path.split('.')
return set(substate, pathArray, get(baseState, pathArray))
}, {})
}
get
函数用于从状态树 (StateTree
) 中获取指定路径的值。它通过路径数组逐步访问嵌套的对象属性。set
函数用于在状态树中设置指定路径的值。它通过路径数组逐步创建嵌套的对象,并在指定位置设置值。pick
函数从状态树中提取指定路径的子状态树。它使用get
函数获取值,使用set
函数设置值,最终返回包含指定路径的子状态树。
至此,核心源码都看完了,基本原理也都清楚了!是否有种 拨开乌云见晴天 的感觉?!
源码感受
源码我们也看完了,原理也懂了,但是是否还有点额外的收获呢?
其实,我个人是有一点的,比如作者的一些编码技巧,编程思想等。
1. 代理对象(Proxy)和反射(Reflect)
- 代理对象: 使用
Proxy
对象拦截对配置选项的访问,动态地合并局部配置和全局配置。 - 反射: 使用
Reflect
提供一致的对象操作方法,提高代码的可读性和维护性。
我记得第一次看到是在 Vue3 的源码中,你看看该作者就运用起来了,学到好东西,用起来才更好。不过也不是强用,是说的在合适的场景中运用起来。
2. 函数式编程思想
- 纯函数:
get
和set
函数是纯函数,它们不改变输入对象的状态,而是返回新的值或状态。 - 高阶函数: 使用
reduce
等高阶函数处理复杂的逻辑,简化代码结构。
只想说写的真不错!!!
3. 错误处理
- 异常处理: 使用
try-catch
块捕获和处理异常,确保程序的健壮性和可调试性。 - 调试选项: 提供
debug
配置选项,在调试模式下输出详细错误信息,方便开发人员排查问题。
代码中的异常处理,错误处理也是值得我们没有做好该方面的小伙伴们学习!还有就是作为插件,也还是debug
配置选项,Good !!
4. 防御性编程
防御性编程: 检查并防止对 __proto__
属性的访问,避免原型污染,提高代码的安全性。
再上述的 pick
函数中的 set
函数运用了。
防御式编程(Defensive Programming) 是一种编程实践或编程范式,通过在代码中添加防御性的逻辑
和错误处理机制
,来降低软件出现错误或异常的概率,从而提高软件的正确行、稳定性、安全性,让软件更健壮。
5. 模块化和代码复用
模块化设计: 函数之间职责明确, 比如:get
、set
和 pick
函数相互独立但又协同工作,增强代码的可维护性和可复用性。
6. 语义化命名
语义化命名: 代码中的函数和变量命名清晰, get
、set
、pick
、parsePersistence
等函数名直观地表达了其功能,增强了代码的可读性(所以上述的源码读起来才那么顺畅!!!)。
7. 组合模式
组合模式: pick
函数结合 get
和 set
函数,复用现有函数来实现更复杂的功能,遵循了组合优于继承的原则。
8. ES6+ 特性
- 解构赋值: 广泛使用解构赋值简化变量的提取,提高代码的简洁性和可读性。
- 箭头函数: 使用箭头函数简化函数定义,减少样板代码。
- 可选链操作符: 使用
?.
确保在访问嵌套对象属性时不会抛出错误,提高代码的健壮性。
9. 等等...
就不一一列举了,希望大家也能有不错的感受!
小结
由动漫 《斩神之凡尘神域》第 03 集,为导火索,以某一小伙伴的评论为背景,促使写了这篇文章。也算有所收获,哎,午饭还没吃呢,先去吃个饭,继续搬砖去喽。
转载自:https://juejin.cn/post/7399985723703672870