Vue3下使用pinia(store)实现状态管理(响应式全局变量)及自动持久化


首先感谢 小满zs大神的文章及其 B站课程。收益匪浅,强烈推荐。下面的pinia自动持久化插件,就是根据他的 视频编写的。在此声明本章代码并非完全原创。
插件为了通用性,有些繁琐,可以根据自己需求改写。为了与Vuex方法对比,内容参照 Vue3下使用Vuex(store)实现响应式全局变量

pinia较Vuex更容易上手,且实现部分更灵活。

本文内容在我实际使用过程中的功能修改、bug修复会随时同步更新。更希望大家评论区多指教、讨论。

1 安装

项目路径下,终端内执行

vue add pinia -S

或者

yarn install pinia -S

2 编写pinia自动持久化插件

持久化存储器支持local,session,cookie三种方式。

  • 为了满足不同store实例可以自定义不同持久化存储器类型,需要在实例属性中定义存储器类型,为此定义了StoreCommonData 类型,作为所有store实例的“基类”。
  • 持久化参数中keyPrefix-key值前缀的意义是为统一管理本系统定义存储器内属性。

插件文件 在/src/utils/store目录下。

2.1 ts类型定义

/src/utils/store/index.d.ts文件

// store存储器类型
export type StoreType = 'local' | 'session' | 'cookie';
/**
 * store基本/共有数据
 */
export type StoreCommonData = {
    // 存储类型
    readonly storageType: StoreType
}
// pinia存储器持久化插件配置
export type PiniaStorageOptions = {
    //key 前缀 该值默认值:__piniaDefaultKeyPrefix__
    keyPrefix?: string,
    //默认存储类型,如果store未设置,则使用默认存储类型。该值默认值:__defaultStoreType__
    defaultStoreType?: StoreType,
    //需要管理的store key(不含前缀),未定义时管理所有store
    manageStoreKeys?: string[]
    //不需要管理的store key(不含前缀),未定义时管理manageStoreKeys定义内容,
    // 有冲突时优先使用unManageStoreKeys定义内容
    unManageStoreKeys?: string[]
}

2.2 pinia自动持久化插件

/src/utils/store/index.ts文件

/**
 * pinia持久化存储
 * 更新日志:
 * 2024/6/4 添加key的加前缀和去前缀方法;移除指定存储器类型的数据方法,添加不移除keys的参数
 * @author MuYi
 * @date 2024/5/24
 * @version 1.0
 */
import {PiniaPluginContext} from 'pinia';
import {toRaw} from "vue";
import {logClientInfo} from "~/api/system/sys";
import {COOKIE_EXPIRES_HOURS} from "~/config/env";
import type {PiniaStorageOptions, StoreCommonData, StoreType} from "./index.d"

const __piniaDefaultKeyPrefix__: string = 'MUYI_';
const __defaultStoreType__ = 'local';
let PINIA_DEFAULT_KEY_PREFIX: string = __piniaDefaultKeyPrefix__;
export const piniaStorage = (storageOptions?: PiniaStorageOptions) => {
    PINIA_DEFAULT_KEY_PREFIX = `${storageOptions?.keyPrefix ?? __piniaDefaultKeyPrefix__}`
    return (context: PiniaPluginContext) => {
        const {store} = context;
        let key = store.$id;
        if (storageOptions?.unManageStoreKeys && storageOptions.unManageStoreKeys.includes(key)) return;
        if (storageOptions?.manageStoreKeys && !storageOptions.manageStoreKeys.includes(key)) return;
        const keyWithPrefix = getKeyWithPrefix(key);
        const storageType = getStorageType(toRaw(store.$state) as StoreCommonData, storageOptions);

        const data = getStorage(keyWithPrefix) as StoreCommonData;
        store.$subscribe(() => {
            try {
                setStorage(keyWithPrefix, toRaw(store.$state), storageType);
            } catch (error) {
                logClientInfo('Storage set operation failed:' + error);
            }
        })
        return {...data}
    }
}
/**
 * 删除local、session、cookie存储。未指定key时,删除所有PINIA_DEFAULT_KEY_PREFIX为前缀的key存储。
 * @param keysWithOutPrefix 存储key,不含前缀。
 */
export const removeStorage = (keysWithOutPrefix?: string[]) => {
    if (!keysWithOutPrefix || keysWithOutPrefix.length === 0) {
        const keysToRemove = [
            ...getLocalStorageKeys(),
            ...getSessionStorageKeys(),
            ...getCookieStorageKeys()
        ];

        keysToRemove.forEach(key => {
            try {
                localStorage.removeItem(key);
                sessionStorage.removeItem(key);
                setCookieExpires(key);
            } catch (error) {
                logClientInfo('Storage remove [‘+key+’] operation failed:' + error);
            }
        });
        return;
    }
    keysWithOutPrefix.forEach(key => {
        let k = getKeyWithPrefix(key)
        try {
            localStorage.removeItem(k);
            sessionStorage.removeItem(k);
            setCookieExpires(k);
        } catch (error) {
            logClientInfo('Storage remove [‘+k+’] operation failed:' + error);
        }
    })
}
/**
 * 移除指定类型的存储(带有PINIA_DEFAULT_KEY_PREFIX前缀的所有key)
 * @param type 存储器类型
 * @param unRemoveKeysWithOutPrefix 不删除的key(不包含PINIA_DEFAULT_KEY_PREFIX前缀)列表
 */
export const removeStorageByType = (type: StoreType, unRemoveKeysWithOutPrefix?: string[]) => {
    switch (type) {
        case 'local':
            removeSpeacialKeys(getLocalStorageKeys(), unRemoveKeysWithOutPrefix).forEach(key => {
                try {
                    localStorage.removeItem(key);
                } catch (error) {
                    logClientInfo('Local Storage remove [‘+key+’] operation failed:' + error);
                }
            })
            break;
        case 'session':
            removeSpeacialKeys(getSessionStorageKeys(), unRemoveKeysWithOutPrefix).forEach(key => {
                try {
                    sessionStorage.removeItem(key);
                } catch (error) {
                    logClientInfo('Session  Storage remove [‘+key+’] operation failed:' + error);
                }
            });
            break;
        case 'cookie':
            removeSpeacialKeys(getCookieStorageKeys(), unRemoveKeysWithOutPrefix).forEach(key => {
                setCookieExpires(key);
            });
            break;
    }
}
/**
 * 使cookie过期
 * @param key key
 */
const setCookieExpires = (key: string) => {
    // 获取当前时间并设置为过去的时间(确保立即过期)
    const now = new Date();
    now.setTime(now.getTime() - (1000 * 60 * 60 * 24));
    const expires = now.toUTCString();
    document.cookie = `${key}=; expires=${expires}; path=/;`
}
/**
 * 获得本地存储的所有具有PINIA_DEFAULT_KEY_PREFIX前缀的key
 */
const getLocalStorageKeys = () => {
    return Object.keys(localStorage).filter(key => key.startsWith(PINIA_DEFAULT_KEY_PREFIX));
}
const removeSpeacialKeys = (sourceList: string[], unIncludeKeysWithOutPrefix?: string[]) => {
    if (sourceList && sourceList.length > 0 && unIncludeKeysWithOutPrefix && unIncludeKeysWithOutPrefix.length > 0) {
        return sourceList.filter(key => !unIncludeKeysWithOutPrefix.includes(getKeyWithOutPrefix(key)));
    } else return sourceList;
}
const getKeyWithPrefix = (keyWithOutPrefix: string) => {
    return `${PINIA_DEFAULT_KEY_PREFIX}${keyWithOutPrefix}`
}
const getKeyWithOutPrefix = (getKeyWithPrefix: string) => {
    return getKeyWithPrefix.replace(PINIA_DEFAULT_KEY_PREFIX, '')
}
/**
 * 获得session存储的所有具有PINIA_DEFAULT_KEY_PREFIX前缀的key
 */
const getSessionStorageKeys = () => {
    return Object.keys(sessionStorage).filter(key => key.startsWith(PINIA_DEFAULT_KEY_PREFIX));
}
/**
 * 获得cookie存储的所有具有PINIA_DEFAULT_KEY_PREFIX前缀的key
 */
const getCookieStorageKeys = () => {
    return Object.keys(document.cookie).filter(key => key.startsWith(PINIA_DEFAULT_KEY_PREFIX));
}

const getStorageType = (value: StoreCommonData, storageOptions?: PiniaStorageOptions) => {
    return value.storageType ?? (storageOptions?.defaultStoreType ?? __defaultStoreType__);
}
const setStorage = (itemKey: string, value: any, storageType?: StoreType) => {
    let v = JSON.stringify(value);
    switch (storageType) {
        case 'local':
            localStorage.setItem(itemKey, v)
            break;
        case 'session':
            sessionStorage.setItem(itemKey, v)
            break;
        case 'cookie':
            const expirationDate = new Date(Date.now() + COOKIE_EXPIRES_HOURS * 60 * 60 * 1000); // 过期时间,单位为毫秒
            document.cookie = `${itemKey}=${v}; expires=${expirationDate.toUTCString()}; path=/;`;
            break;
    }
}
const getStorage = (itemKey: string, storageType: StoreType = __defaultStoreType__): {} => {
    let result: string | null = null;
    switch (storageType) {
        case 'local':
            result = localStorage.getItem(itemKey) as string
            break;
        case 'session':
            result = sessionStorage.getItem(itemKey) as string
            break;
        case 'cookie':
            let cookies = document.cookie.split('; ');
            for (let i = 0; i < cookies.length; i++) {
                let [name,] = cookies[i].split('=');
                if (name === itemKey) {
                    result = decodeURIComponent(name.split('=')[1]);
                    break;
                }
            }
            break;
    }
    return result ? JSON.parse(result) : {}
}
  • 其中的使用的COOKIE_EXPIRES_HOURS内容如下
// cookie过期时间。单位小时。默认2天
export const COOKIE_EXPIRES_HOURS = 24 * 2; //  (以小时计)
  • logClientInfo方法向后台传送错误日志,可以删除该方法。不影响插件功能
  • removeStorage方法用于在系统注销或需要时,删除存储器内容

2.3 加载引用插件

main.ts文件中添加如下代码。

import {piniaStorage} from "~/utils/store";
import {Stores} from "~/stores/type";
pinia.use(piniaStorage({
    manageStoreKeys: [Stores.appInfo]
}))
app.use(pinia);

a. app是vue的实例
b. piniaStorage传入的参数按自己需求填写,也可以使用默认值,即不输入参数。

3 vue store实例

3.1 目录及文件结构

按惯例所有store实例应存储在stores目录下,其中具体方案按自己喜好。
在这里插入图片描述

3.2 store实例命名统一管理

为了统一管理store实例,建议编写如下type.ts文件

/**
 *  本系统包含的状态管理类型
 * @author MuYi
 * @date 2024/5/24
 * @version 1.0
 */
export enum Stores{
    //app注册及版权信息
    appInfo='appInfo',
    //用户信息
    userInfo='userInfo',
    //主题信息
    theme='theme'
}

3.3 编写store实例

/**
 * app注册及版权信息,同步在session中,退出则被清除
 *@author MuYi
 *@date 2022/3/21 8:58
 *@version 1.0
 */
import {defineStore} from 'pinia';
import {useUserOperateStore} from "./userOperate";
import type {StoreCommonData} from "~/utils/store/index.d";
import {Stores} from "~/stores/type";


/**
 * app信息数据结构
 */
export type  StateAppInfo = StoreCommonData & {
    registerCompany: string,
    appName: string,
    appTitle: string,
    appTitleEng: string,
    version: string,
    copyright: string,
    copyrightYear: string,
    author: string, 
}

const getDefaultValue = (): StateAppInfo => {
    return {
        registerCompany: "请联系xxxxx注册您的公司",
        appName: "MUYI",
        appTitle: "WinTown Software",
        appTitleEng: "WinTown Software",
        version: "非合法版",
        copyright: "WinTown Software studio All rights reserved.",
        copyrightYear: "©请使用正版",
        author: "Mobile:xxxx; Email:xxx@qq.com", 
        storageType: 'local'
    }
}


export const useAppInfoStore = defineStore(Stores.appInfo, {
    state: (): StateAppInfo => {
        return getDefaultValue();
    },
    getters: {
        docTitle: (state: StateAppInfo) => {
            let title ;
            try {
                const userOperateStore = useUserOperateStore();
                title = userOperateStore.activeTagValue.title;
            } catch {
                title = "";
            }
            return state.appName + "-" + title;
        },
    },
    actions: {
        /**
         * 保存/设置app信息
         * @param appInfo{StateAppInfo}
         */
        saveAppInfo(appInfo: StateAppInfo) {
            if (appInfo && appInfo.registerCompany) {
                this.$state = appInfo;
            } else {
                this.$reset();
            }
        },

        /**
         * 清空程序信息
         */
        clearAppInfo() {
            this.$reset();
        },

    }
});

注意:

  • 实例属性类型声明时,需引用StoreCommonData
  • storageType属性用来定义存储器类型

3.3 使用store实例

示例1:

import {StateAppInfo, useAppInfoStore} from "~/stores/appInfo"

/**
 * 获取app软件信息
 * @return {Promise<boolean>}
 */
const getAppInfo = async () => {
    const appInfoStore = useAppInfoStore();
    try {
        let url = "/system/getAppInfo";
        let result = await request.get<StateAppInfo>(url)
        if (result.data && result.success) {
            appInfoStore.saveAppInfo(result.data);
            return true;
        }
        return false;
    } catch (e) {
        appInfoStore.clearAppInfo();
        logClientInfo("获取AppInfo出错。" + e)
        return false;
    }
}

示例2:

/**
 * 清除用户及程序必要本地信息
 */
export function clearStorageInfo() {
    removeStorageByType('local',[Stores.userOperate]);
    removeStorageByType('session');
}
  • 12
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

muyi517

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值