保姆级带你手写Pinia插件:plugin-pinia-persistence

源码已发布至GitHub,需要的小伙伴请自取。如果觉得文章有帮助的话,求个Star⭐️

在上一篇中我们实现了一个基础版的持久化插件(在看本篇之前建议先复习一下上一篇是如何实现基础版的plugin-pinia-persistence)。 随着业务复杂度提升,基础版本逐渐无法满足实际需求。

所以,我们来添加新功能了!

功能升级清单

  1. 存储键名自定义 - 告别固定命名的key
  2. 存储介质扩展 - 支持localStorage/sessionStorage/自定义存储
  3. 数据筛选能力 - 按需持久化特定状态字段
  4. 生命周期钩子 - 添加beforeHydrateafterHydrate回调
  5. 错误处理机制 - 增加存储异常日志记录

需求分析:

首先我们看一下这些功能,不难发现,这些功能都是要求可以自定义的,那么就可以采用通过配置项的方式去完成。

在Pinia的官网介绍中提到,Pinia 插件是一个函数,它接收一个可选参数context ,其中的参数options可以定义传给 defineStore() 的 store 的可选对象。

在基础版中我们已经定义了一个options,即 persist: true ,用于判断是否需要持久化。因此在上面的功能中就可以根据具体的需求设置配置项来进行功能开发。

在配置项中,可以分为两种:1、全局配置;2、局部配置。 由于这两种配置可能会出现配置项的重复,因此在优先级上我们设定局部配置大于全局配置。

需求开发:

在基础版中,我们创建了一个函数 createPersistedState 并导出插件给createPinia()使用,在这里我们可以传递一些全局配置,例如:

js 复制代码
const store = createPinia()
store.use(
    createPersistedState({
        key: (id) => id,
        storage: localStorage,
        debug: true
    })
)

因此我们需要修改函数 createPersistedState 来接收全局配置项

js 复制代码
const createPersistedState = (config = {}) => {
    return (context) => {
        createPersisted(context, config)
    }
}

const createPersisted = (context, config) => {
    // to do ...
}

一、存储键名自定义 - 告别固定命名的key

分析一下:

在自定义key的设置中,我们既可以配置全局的key,也可以针对各个模块设置key。

局部配置key:

js 复制代码
// --- store/userStore.js ---
const useUserStore = defineStore({
    id: 'user',
    state: () => ({
        name: ''
    }),
    persist: {
        key: 'my-user'
    }
})

// --- store/plugin-pinia-persistence.js ---
const createPersisted = (context, config) => {
    const { store, options: { persist = true }} = context
    // 如果不需要持久化,直接返回
    if (!persist) return
    
    // 定义一个配置项,判断persist中key是否存在,不存在则使用默认
    const persistenceOptions = {
        key: persist.key ?? store.$id
    }
    
    // 将数据从存储中恢复状态到store
    hydrateStore(store, persistenceOptions, context)

    // 数据变化时,持久化store的状态
    store.$subscribe(
        (_mutation, state) => {
            persistState(state, persistenceOptions)
        },
        { detached: true }
    )
}
// 从存储中恢复状态到store
const hydrateStore = (store, persist, context) => {
    const { key } = persist // 将key结构出来后使用
    const fromStorage = localstorage.getItem(key)
    // ...
}

// 将store的状态持久化到存储中
const persistState = (state, persist) => {
    const { key } = persist
    // 将key结构出来后使用
    localstorage.setItem(key, toStorage)
}

全局配置key:

由于pinia中支持分模块,比如我们有userStore、shopStore两个模块,那么在配置全局key时,如果直接设置字符串,会导致两个模块存储数据的key值重复导致数据覆盖。因此我们在配置全局key时只能对模块设置的key进行扩展和补充。

js 复制代码
// --- store/index.js ---
const store = createPinia()
store.use(
    createPersistedState({
        // 配置全局的key,传入一个函数并接受一个参数,用来做扩展,比如加前缀
        key: (id) => `_persisted_${id}`
    })
)

// --- store/plugin-pinia-persistence.js ---
const createPersisted = (context, config) => {
    const { store, options: { persist = true }} = context
    
    // 定义一个配置项,判断全局的key是否存在且为函数,执行函数后返回对应的字符串。
    const persistenceOptions = {
        key: (config.key && typeof config.key === "function" ? config.key : (x) => x)(p.key ?? store.$id)
    }
    
    // ...
}

二、存储介质扩展 - 支持localStorage/sessionStorage/自定义存储

局部配置:

js 复制代码
// --- store/userStore.js ---
const useUserStore = defineStore({
    id: 'user',
    state: () => ({
        name: ''
    }),
    persist: {
        key: 'my-user',
        storage: sessionstorage
    }
})

// --- store/plugin-pinia-persistence.js ---
const createPersisted = (context, config) => {
    const { store, options: { persist = true }} = context
    if (!persist) return
    
    // 配置项中接收storage
    const persistenceOptions = {
        key: persist.key ?? store.$id,
        // 将定义的storage传入
        storage: persist.storage ?? window.localstorage
    }
    // ...
}
// 从存储中恢复状态到store
const hydrateStore = (store, persist, context) => {
    const { key, storage } = persist // 将storage解构出来
    // 修改
    const fromStorage = storage.getItem(key)
    // ...
}

// 将store的状态持久化到存储中
const persistState = (state, persist) => {
    const { key, storage } = persist   // 将storage解构出来后使用
    
    // 修改
    storage.setItem(key, toStorage)
}

全局配置:

如果嫌麻烦我们可以在全局进行配置storage,如果在模块中也配置了storage,那按照规则局部配置大于全局配置。

js 复制代码
// --- store/index.js ---
const store = createPinia()
store.use(
    createPersistedState({
        storage: localStorage  // 配置storage
    })
)

// --- store/plugin-pinia-persistence.js ---
const createPersisted = (context, config) => {
    const { store, options: { persist = true }} = context
    if (!persist) return
    
    // 配置项中接收storage
    const persistenceOptions = {
        key: persist.key ?? store.$id,
        // 只需要在配置项中配置全局配置的storage即可。
        storage: persist.storage ?? config.storage ?? window.localstorage
    }
}

三、数据筛选能力 - 按需持久化特定状态字段

使用deep-pick-omit库,用来深度选择(pick)或排除(omit)嵌套对象的属性

js 复制代码
// --- store/userStore.js ---
const useUserStore = defineStore({
    id: 'user',
    state: () => ({
        name: 'Tony',
        a: { b: "B" },
	      c: { d: 'D' }
    }),
    persist: {
        key: 'my-user',
        storage: sessionstorage,
        pick: ['name', 'a', 'c.d'], // pick表示数组中的进行存储
        omit: ['a.b'], // omit 表示数组中的不存储,其他的进行存储
    }
})

// --- store/plugin-pinia-persistence.js ---
// 引入deep-pick-omit
import { deepOmitUnsafe, deepPickUnsafe } from 'deep-pick-omit'
const createPersisted = (context, config) => {
    const { store, options: { persist = true }} = context
    if (!persist) return
    
    // 配置项中接收storage
    const persistenceOptions = {
        key: persist.key ?? store.$id,
        storage: persist.storage ?? config.storage ?? window.localstorage,
        pick: persist.pick, // 传入配置
        omit: persist.omit // 传入配置
    }
    // ...
}

// 从存储中恢复状态到store
const hydrateStore = (store, persist, context) => {
    const { key, storage, pick, omit } = persist // 将pick, omit解构出来
    // 修改
    const fromStorage = storage.getItem(key)
    if (fromStorage) {
        // 反序列化,解析出数据
        const deserialized = serializer.deserialize(fromStorage)
        // 筛选出哪些是需要存储的
        const picked = pick ? deepPickUnsafe(deserialized, pick) : deserialized
        // 排除出去不需要存储的
        const omitted = omit ? deepOmitUnsafe(picked, omit) : picked
        store.$patch(omitted)
    }
    // ...
}

// 将store的状态持久化到存储中
const persistState = (state, persist) => {
    const { key, storage, pick, omit } = persist   // 将pick, omit解构出来后使用
    
    // 筛选出哪些是需要存储的
    const picked = pick ? deepPickUnsafe(state, pick) : state
    // 排除出去不需要存储的
    const omitted = omit ? deepOmitUnsafe(picked, omit) : picked
    // 序列化数据
    const toStorage = serializer.serialize(omitted)
    storage.setItem(key, toStorage)
}

这样就实现了数据的选择性存储,如果我们有这样一个需求,比如name字段需要存到localstorage中,token需要存在sessionStorage中,该怎么处理?

js 复制代码
// --- store/userStore.js ---
const useUserStore = defineStore({
    id: 'user',
    state: () => ({
        name: 'Tony',
        token: '123123123'
    }),
    persist: [  // 将结构改为数组的形式
        {
          pick: ['name']
          storage: localstorage
        },{
          pick: ['token']
          storage: sessionstorage
        }
    ]
})

// --- store/plugin-pinia-persistence.js ---
const createPersisted = (context, config) => {
    const { store, options: { persist = true }} = context
    if (!persist) return
    
    // 由于要适配数组,所以如果不是数组的话要转化成数组的形式
    const persistenceOptions = Array.isArray(persist) ? persist : persist === true ? [{}] : [persist]
													    
    // 进行遍历处理配置项
    const persistences = persistenceOptions.map((p) => {
        return {
            key: (config.key && typeof config.key === "function" ? config.key : (x) => x)(p.key ?? store.$id),
            storage: p.storage ?? config.storage ?? window.localStorage,
            pick: p.pick,
            omit: p.omit
        }
    })
    
    // 循环调用
    persistences.forEach((persist) => {
        // 将数据从存储中恢复状态到store
        hydrateStore(store, persist, context)

        // 数据变化时,持久化store的状态
        store.$subscribe(
            (_mutation, state) => {
                persistState(state, persist)
            },
            { detached: true }
        )
    })
}

四、生命周期钩子 + 错误处理机制

js 复制代码
// --- store/userStore.js ---
const useUserStore = defineStore({
    id: 'user',
    state: () => ({
        name: 'Tony'
    }),
    persist: {
        key: 'my-user',
        storage: sessionstorage,
        pick: [],
        omit: [],
        beforeHydrate: (ctx) => {}, 
        afterHydrate: (ctx) => {}, 
        debug: true
    }
})

// --- store/plugin-pinia-persistence.js ---
const createPersisted = (context, config) => {
    const { store, options: { persist = true }} = context
    if (!persist) return
		
    // ...

    const persistences = persistenceOptions.map((p) => {
        return {
            key: (config.key && typeof config.key === "function" ? config.key : (x) => x)(p.key ?? store.$id),
            storage: p.storage ?? config.storage ?? window.localStorage,
            pick: p.pick,
            omit: p.omit,
            beforeHydrate: p.beforeHydrate, // 配置钩子函数
            afterHydrate: p.afterHydrate // 配置钩子函数
            debug: p.debug ?? config.debug ?? false // 配置debug
        }
    })
    
    // ...
}
// 从存储中恢复状态到store
const hydrateStore = (store, persist, context) => {
    const { key, storage, pick, omit, beforeHydrate, afterHydrate, debug } = persist

    try {
        // 钩子函数在用持久化数据激活 store state 之前运行
        beforeHydrate?.(context)

        // ...

        // 钩子函数在用持久化数据激活 store state 之后运行
        afterHydrate?.(context)
    } catch (error) {
        // 如果debug配置了,则使用console.error捕获错误
        if (debug) {
            console.error('持久化错误' + error)
        }
    }
}

// 将store的状态持久化到存储中
const persistState = (state, persist) => {
    const { key, storage, pick, omit, beforeHydrate, afterHydrate, debug } = persist
    try {
        // ...
    } catch (err) {
        if (debug) {
            console.error('保存错误' + err)
        }
    }
}

至此,plugin-pinia-persistence的大部分功能已经实现了,我再官网上看了看应该还缺少强制 hydration、强制持久化 ,应该就是将 hydrateStorepersistState 方法挂载到store上,然后就可以调用了。

希望本文对你了解Pinia以及如何写一个plugin有所帮助。

源码已发布至GitHub,需要的小伙伴可以取用。如果觉得文章有帮助的话,求个Star⭐️

相关推荐
apcipot_rain2 小时前
【应用密码学】实验五 公钥密码2——ECC
前端·数据库·python
ShallowLin2 小时前
vue3学习——组合式 API:生命周期钩子
前端·javascript·vue.js
Nejosi_念旧3 小时前
Vue API 、element-plus自动导入插件
前端·javascript·vue.js
互联网搬砖老肖3 小时前
Web 架构之攻击应急方案
前端·架构
pixle03 小时前
Vue3 Echarts 3D饼图(3D环形图)实现讲解附带源码
前端·3d·echarts
麻芝汤圆4 小时前
MapReduce 入门实战:WordCount 程序
大数据·前端·javascript·ajax·spark·mapreduce
juruiyuan1116 小时前
FFmpeg3.4 libavcodec协议框架增加新的decode协议
前端
Peter 谭6 小时前
React Hooks 实现原理深度解析:从基础到源码级理解
前端·javascript·react.js·前端框架·ecmascript
LuckyLay7 小时前
React百日学习计划——Deepseek版
前端·学习·react.js
gxn_mmf8 小时前
典籍知识问答重新生成和消息修改Bug修改
前端·bug