likes
comments
collection
share

pinia-plugin-persistedstate 如何实现的持久化存储?`pinia-plugin-persist

作者站长头像
站长
· 阅读数 23

pinia-plugin-persistedstate 如何实现的持久化存储?`pinia-plugin-persist

先废话一句,今天看了动漫 《斩神之凡尘神域》第 03 集。说句实话,看哭了,热泪盈眶了.... 看弹幕,应该不止我一个人这样......

感兴趣的小伙伴、或者喜欢动漫的小伙伴,也可以去看看...

接下来,进入本文正题吧!(其实我是想发上述这个图,才决定写这一篇的。😄 😂)

背景

期间有个小伙伴评论了:

pinia-plugin-persistedstate 如何实现的持久化存储?`pinia-plugin-persist

瞬间让我想起了 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 状态持久化到 localStoragesessionStorage,从而在页面刷新或重新加载后保留状态。

源码分析

pinia-plugin-persistedstate 功能挺简洁的,源码应该也不是很多,那我们就一起看看...

pinia-plugin-persistedstate 如何实现的持久化存储?`pinia-plugin-persist

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
    }
  }
}
  1. 该函数的目的是解析持久化选项并生成特定的持久化配置。具体来说,它返回一个函数,该函数接受一个持久化选项对象并返回一个 Persistence 对象或 null
  2. parsePersistence 函数接收两个参数:
    • factoryOptions: 全局的持久化配置选项。
    • store: 当前的 Pinia Store 实例。
  3. try 语句块中,从 o(即 PersistedStateOptions)对象中解构出各个属性,并为它们设置默认值:
    • storage: 持久化的存储方式,默认是 localStorage
    • beforeRestore: 在恢复之前调用的函数,默认是 undefined
    • afterRestore: 在恢复之后调用的函数,默认是 undefined
    • serializer: 序列化和反序列化的方式,默认使用 JSON.stringifyJSON.parse
    • key: 存储的键,默认是 Store 的 id
    • paths: 要持久化的状态路径,默认是 null
    • debug: 调试模式,默认是 false
  4. 返回一个 Persistence 对象,该对象包含了解构出来的属性。特别地,对 key 属性进行处理,如果 key 是一个函数,则调用该函数生成键名(灵活的键名生成)。
  5. 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)
  }
}
  1. hydrateStore 函数的目的是从持久化存储中恢复状态到 Pinia Store 中。
  2. 参数 1:store: Store: Pinia 的 Store 实例,表示当前的 Store 对象。
  3. 参数 2:{ storage, serializer, key, debug }: 从 Persistence 对象中解构出来的属性。
    • storage: 持久化存储的方式,通常是 localStoragesessionStorage
    • serializer: 序列化和反序列化的方法,通常包含 serializedeserialize 两个方法。
    • key: 用于存储状态的键名。
    • debug: 是否开启调试模式。
  4. try 语句块中,尝试从 storage 中获取键名为 key 的数据。
  5. 如果 fromStorage 存在(即存储中有数据),则使用 serializerdeserialize 方法将其反序列化,并使用 Pinia 的 $patch 方法将反序列化后的数据合并到当前的 Store 状态中。
  6. 错误处理。

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)
  }
}
  1. persistState 函数的目的是将 Pinia Store 的状态持久化存储到指定的存储位置
  2. 参数 1:state: StateTree: Pinia Store 的状态树。
  3. 参数 2:{ storage, serializer, key, paths, debug }: 从 Persistence 对象中解构出来的属性。具体就不重复说了。
  4. 如果 paths 是一个数组,则使用 pick 函数从 state 中选择指定的路径;否则,将整个 state 用于存储。(下边再说一下 pick 函数)。
  5. 将要存储的状态 toStore 进行序列化(使用 serializer.serialize 方法),并使用指定的 key 将其存储到 storage 中。
  6. 错误处理。

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,
        },
      )
    })
  }
}
  1. createPersistedState 算是是一个用语函数。
  2. 参数:factoryOptions: 全局的持久化配置选项,默认是一个空对象。
  3. 返回一个接收 PiniaPluginContext 的函数,结合使用方式,这个函数会在 Store 初始化时调用。
  4. auto: 全局持久化配置中的自动持久化选项,默认为 false
  5. persist: Store 局部配置中的持久化选项,如果没有配置,则使用全局配置中的 auto 选项。
  6. 如果 persistfalse,则不进行持久化,直接返回。
  7. 处理 HMR 情况下的 Store 恢复。如果 Store 是热替换创建的,则尝试恢复原始 Store 的持久化状态。
  8. normalizeOptions: 规范化持久化选项,确保每个选项都有合理的默认值。(下边会说) 9. parsePersistence: 解析持久化选项,生成 Persistence 对象。
  9. filter(Boolean): 过滤掉 nullundefinedPersistence 对象。
  10. 为 Store 添加 $persist 方法,用于手动持久化状态。
  11. 为 Store 添加 $hydrate 方法,用于手动恢复持久化状态。该方法支持在恢复前后运行钩子函数。
  12. 初始状态恢复和订阅状态变化。订阅 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)
      )
    },
  })
}
  1. normalizeOptions 函数的目的是规范化持久化配置选项,将全局配置与局部配置合并,并提供默认值。
  2. 竟然是这么来合并局部和全局配置。(该源码作者 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))
  }, {})
}
  1. get 函数用于从状态树 (StateTree) 中获取指定路径的值。它通过路径数组逐步访问嵌套的对象属性。
  2. set 函数用于在状态树中设置指定路径的值。它通过路径数组逐步创建嵌套的对象,并在指定位置设置值。
  3. pick 函数从状态树中提取指定路径的子状态树。它使用 get 函数获取值,使用 set 函数设置值,最终返回包含指定路径的子状态树。

至此,核心源码都看完了,基本原理也都清楚了!是否有种 拨开乌云见晴天 的感觉?!

源码感受

源码我们也看完了,原理也懂了,但是是否还有点额外的收获呢?

其实,我个人是有一点的,比如作者的一些编码技巧,编程思想等。

1. 代理对象(Proxy)和反射(Reflect)

  • 代理对象: 使用 Proxy 对象拦截对配置选项的访问,动态地合并局部配置和全局配置。
  • 反射: 使用 Reflect 提供一致的对象操作方法,提高代码的可读性和维护性。

我记得第一次看到是在 Vue3 的源码中,你看看该作者就运用起来了,学到好东西,用起来才更好。不过也不是强用,是说的在合适的场景中运用起来。

2. 函数式编程思想

  • 纯函数: getset 函数是纯函数,它们不改变输入对象的状态,而是返回新的值或状态。
  • 高阶函数: 使用 reduce 等高阶函数处理复杂的逻辑,简化代码结构。

只想说写的真不错!!!

3. 错误处理

  • 异常处理: 使用 try-catch 块捕获和处理异常,确保程序的健壮性和可调试性。
  • 调试选项: 提供 debug 配置选项,在调试模式下输出详细错误信息,方便开发人员排查问题。

代码中的异常处理,错误处理也是值得我们没有做好该方面的小伙伴们学习!还有就是作为插件,也还是debug 配置选项,Good !!

4. 防御性编程

防御性编程: 检查并防止对 __proto__ 属性的访问,避免原型污染,提高代码的安全性。

再上述的 pick 函数中的 set 函数运用了。

防御式编程(Defensive Programming) 是一种编程实践或编程范式,通过在代码中添加防御性的逻辑错误处理机制,来降低软件出现错误或异常的概率,从而提高软件的正确行、稳定性、安全性,让软件更健壮。

5. 模块化和代码复用

模块化设计: 函数之间职责明确, 比如:getsetpick 函数相互独立但又协同工作,增强代码的可维护性和可复用性。

6. 语义化命名

语义化命名: 代码中的函数和变量命名清晰, getsetpickparsePersistence 等函数名直观地表达了其功能,增强了代码的可读性(所以上述的源码读起来才那么顺畅!!!)。

7. 组合模式

组合模式: pick 函数结合 getset 函数,复用现有函数来实现更复杂的功能,遵循了组合优于继承的原则。

8. ES6+ 特性

  • 解构赋值: 广泛使用解构赋值简化变量的提取,提高代码的简洁性和可读性。
  • 箭头函数: 使用箭头函数简化函数定义,减少样板代码。
  • 可选链操作符: 使用 ?. 确保在访问嵌套对象属性时不会抛出错误,提高代码的健壮性。

9. 等等...

就不一一列举了,希望大家也能有不错的感受!

小结

由动漫 《斩神之凡尘神域》第 03 集,为导火索,以某一小伙伴的评论为背景,促使写了这篇文章。也算有所收获,哎,午饭还没吃呢,先去吃个饭,继续搬砖去喽。

转载自:https://juejin.cn/post/7399985723703672870
评论
请登录