这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战
背景
可能大家都知道SPA
应用,页面刷新会导致数据丢失。多数人解决的方式可能是把数据存放在localStorage
或sessionStorage
,在应用加载的时候去获取本地缓存的值,在赋给store
。本插件的原理也是这样,但是插件的意义就是增加我们的功能,而且重要的是简化了我们的代码。
前置知识:
- pinia
- # 如何写一个pinia plugin 增强store的能力
- seb-l.github.io/pinia-plugi… 本篇文章算是对它的源码解析吧
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
是个可选项,于是我们增加一个默认配置,key
为store
的id
,默认使用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
进行读取,再$patch
对store
进行更新。
cache
如果你有注意到strategies
的类型是个数组,并带有path
选择。这意味着,我们可以对sotre
的state
中的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
中的状态,分别往sessionStorage
和localStorage
,而我更新的确实部分节点的数据缓存在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 }
)
}
}