保姆级带你手写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⭐️

相关推荐
汪子熙15 分钟前
走进 Fundamental NGX Platform:从 SAP 设计体系到高生产力组件层
前端·javascript·面试
拉不动的猪28 分钟前
单点登录全流程小姐
前端·javascript·面试
菜鸟小九35 分钟前
html、css(javaweb第一天)
前端·css·html
y东施效颦1 小时前
uni-app页面发布测试环境出现连接服务器超时,点击屏幕重试解决方案
前端·javascript·vue.js·uni-app·vue
大熊程序猿1 小时前
《开篇:课程目录》
前端·c#
摸鱼仙人~2 小时前
React中子传父组件通信操作指南
前端·javascript·react.js
程序员阿超的博客2 小时前
React事件处理:如何给按钮绑定onClick点击事件?
前端·javascript·react.js
前端小咸鱼一条2 小时前
Vue中渲染函数的使用
javascript·vue.js·ecmascript
沉香亭北2 小时前
vue+vite 全局主题
前端
郑州小张2 小时前
前端解析PDF文件目录以及点击目录实现对应内容预览
前端·javascript