一个pinia plugin 实现数据本地缓存

5,200 阅读2分钟

这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战

背景

可能大家都知道SPA应用,页面刷新会导致数据丢失。多数人解决的方式可能是把数据存放在localStoragesessionStorage,在应用加载的时候去获取本地缓存的值,在赋给store。本插件的原理也是这样,但是插件的意义就是增加我们的功能,而且重要的是简化了我们的代码。

前置知识:

start

如果你阅读过pinia的插件机制,我们知道plugin的第一个参数,我们可以拿到store的值。于是我们可以对store.options做手脚,规范我们的缓存格式,以便缓存的使用

// pinia.d.ts
declare module 'pinia' {
  export interface PersistStrategy {
    key?: string
    storage?: Storage
    paths?: string[]
  }

  export interface PersistOptions {
    enabled: true // 决定是否开启使用缓存
    strategies?: PersistStrategy[]
  }

  export interface DefineStoreOptionsBase<S, Store> {
    persist?: PersistOptions
  }
}

于是我们使用缓存的格式为:

export const useUserStore = defineStore({
  id: 'storeUser',
  state () {
    return {
      firstName: 'S',
      lastName: 'L',
      accessToken: 'xxxxxxxxxxxxx',
    }
  },
  persist: {
    enabled: true,
    strategies: [
      { storage: sessionStorage, paths: ['firstName', 'lastName'] },
      { storage: localStorage, paths: ['accessToken'] },
    ],
  },
})

const defaultStrat: PersistStrategy[] = [
  {
    key: store.$id,
    storage: sessionStorage
  }
]

const strategies = options.persist.strategies?.length
  ? options.persist.strategies
  : defaultStrat

因为我们的strategies是个可选项,于是我们增加一个默认配置,keystoreid,默认使用sessionStorage进行缓存。

接下来,我们对我们的缓存策略进行循环实现

strategies.forEach((strategy) => {
  const storageKey = strategy.key || store.$id
  const storage = strategy.storage || sessionStorage
  const storageResult = storage.getItem(storageKey)
  if (storageResult) {
    store.$patch(JSON.parse(storageResult))
    // ...进行缓存
  }
})

因为我们SPA刷新需要对缓存的数据进行回填,于是我们需要对storage进行读取,再$patchstore进行更新。

cache

如果你有注意到strategies的类型是个数组,并带有path选择。这意味着,我们可以对sotrestate中的key,进行不同环境的缓存。

于是有

const storage = strategy.storage || sessionStorage
const storeKey = strategy.key || store.$id

if (strategy.paths) {
    const partialState = strategy.paths.reduce((finalObj, key) => {
      finalObj[key] = store.$state[key]
      return finalObj
    }, {} as PartialState)
    storage.setItem(storeKey, JSON.stringify(partialState))
} else {
    storage.setItem(storeKey, JSON.stringify(store.$state))
}

我们对strategy.paths进行循环,将store.state进行策略的划分,再进行不同环境的缓存。

update

上面已经基本解决了数据缓存的问题,但是store.state数据更新,我们怎么通知我插件去重新触发新一轮的缓存呢?

于是我们可以使用watch api,当store.state更新,我们直接拿到最新的state,重新缓存一遍state就好了。

于是我们发现,一开始,插件触发了一次缓存更新,每次state改变,也会触发一次缓存更新。那么我们可以将缓存的方法进行抽离封装。

function updateStorage(strategy: PersistStrategy, store: Store) {
  const storage = strategy.storage || sessionStorage
  const storeKey = strategy.key || store.$id

  if (strategy.paths) {
    const partialState = strategy.paths.reduce((finalObj, key) => {
      finalObj[key] = store.$state[key]
      return finalObj
    }, {} as PartialState)

    storage.setItem(storeKey, JSON.stringify(partialState))
  } else {
    storage.setItem(storeKey, JSON.stringify(store.$state))
  }
}

我们监听state的改变,每次去调用updateStorage就可以了

watch(
  () => store.$state,
  () => {
    strategies.forEach((strategy) => {
      updateStorage(strategy, store)
    })
  },
  { immediate: true, deep: true }
)

注意{ immediate: true, deep: true }

当我程序一开始,storage没有数据时,storageResult便是null,所以需要立即进行一次缓存。 而且我们需要深度监听state的值。

typescript

pinia的初衷不就是更好的结合typescript嘛。

为了项目开发pinia的友好智能提示和推导,我们需要对pinia的类型进行加强。

于是我们重新定义pinia module

// pinia.d.ts
import 'pinia'

declare module 'pinia' {
  export interface PersistStrategy {
    key?: string
    storage?: Storage
    paths?: string[]
  }

  export interface PersistOptions {
    enabled: true
    strategies?: PersistStrategy[]
  }

  export interface DefineStoreOptionsBase<S, Store> {
    persist?: PersistOptions
  }
}

思考

上面的概述已经完全可以在项目中跑起来了,但是如果你对代码有也许的敏感度。

你会发现

watch(
  () => store.$state,
  () => {
    strategies.forEach((strategy) => {
      updateStorage(strategy, store)
    })
  },
  { immediate: true, deep: true }
)

store.state每次改变,该节点数便是全量更新,多少性能有一丢丢的影响。

为什么是全量更新,因为我们可以在strategies配置策略,可以往不同storage里面存放数据。

假如我们store中的状态,分别往sessionStoragelocalStorage,而我更新的确实部分节点的数据缓存在localStorage,而此次的缓存却同时将sessionStorage进行更新。

这是没必要的。所以每次的updateStorage需要通过diff找到最新的更新节点,去进行少量更新,按理来说也会加快了数据读取的速度。

当然了,这是个 TODO Task,大家也可以充分发挥自己的思想,进行代码加强。

源码🤭。

// pinia.d.ts
import 'pinia'

declare module 'pinia' {
  export interface PersistStrategy {
    key?: string
    storage?: Storage
    paths?: string[]
  }

  export interface PersistOptions {
    enabled: true
    strategies?: PersistStrategy[]
  }

  export interface DefineStoreOptionsBase<S, Store> {
    persist?: PersistOptions
  }
}
// pinia-plugin-persist.ts
import { PiniaPluginContext, PersistStrategy } from 'pinia'
import { watch } from 'vue'

type Store = PiniaPluginContext['store']
type PartialState = Partial<Store['$state']>

function updateStorage(strategy: PersistStrategy, store: Store) {
  const storage = strategy.storage || sessionStorage
  const storeKey = strategy.key || store.$id

  if (strategy.paths) {
    const partialState = strategy.paths.reduce((finalObj, key) => {
      finalObj[key] = store.$state[key]
      return finalObj
    }, {} as PartialState)

    storage.setItem(storeKey, JSON.stringify(partialState))
  } else {
    storage.setItem(storeKey, JSON.stringify(store.$state))
  }
}

export const piniaPluginPersist = ({
  options,
  store
}: PiniaPluginContext): void => {
  if (options.persist?.enabled) {
    const defaultStrat: PersistStrategy[] = [
      {
        key: store.$id,
        storage: sessionStorage
      }
    ]

    const strategies = options.persist.strategies?.length
      ? options.persist.strategies
      : defaultStrat

    strategies.forEach((strategy) => {
      const storageKey = strategy.key || store.$id
      const storage = strategy.storage || sessionStorage
      const storageResult = storage.getItem(storageKey)
      if (storageResult) {
        store.$patch(JSON.parse(storageResult))
        updateStorage(strategy, store)
      }
    })

    watch(
      () => store.$state,
      () => {
        strategies.forEach((strategy) => {
          updateStorage(strategy, store)
        })
      },
      { immediate: true, deep: true }
    )
  }
}