查看: 285|回复: 0

Vue3 Pinia Store重置初始状态两种实现方案

[复制链接]
发表于 2 小时前 | 显示全部楼层 |阅读模式
在 Vue3 项目中,Pinia Store 默认保存在当前页面的内存里。用户正常退出登录时,如果只删除 token 或跳转登录页,业务 Store 中的状态可能残留,切换账号后容易读到旧数据。因此,登出逻辑除了清理 token、用户信息、菜单和路由,还需要把业务 Store 重置到初始状态。Pinia 提供了 $reset(),但前提是能拿到对应 Store 实例;当项目在 stores/index.ts 中统一命名导出多个 Store 时,如何遍历重置是本文要解决的问题。

一、前提:统一导出 Store

原文示例在 src/stores/index.ts 中使用命名导出,把各 Store 的默认导出重命名为 useXxxStore。这样每个 Store 有明确名称,便于按需导入和类型推断。
  1. export { default as useTokenStore } from './tokenStore';
  2. export { default as useUserInfoStore } from './userInfoStore';
  3. export { default as useUserMenuStore } from './userMenuStore';
  4. export { default as useUserRouteStore } from './userRouteStore';
  5. export { default as useCapitalInfoStore } from './capitalInfoStore';
  6. export { default as useCapitalAllocateStore } from './capitalAllocateStore';
  7. export { default as useCapitalAllOptionStore } from './capitalAllOptionStore';
  8. export { default as useDepartmentStore } from './departmentStore';
复制代码

二、原始登出 hook 的问题

src/hooks/useLogout.ts 中先获取 tokenStore、userInfoStore、userMenuStore、userRouteStore,再把部分业务 Store 放进 businessStores 数组,最后逐个调用 $reset()。核心代码如下:
  1. import { resetRoutes } from '@/router';
  2. import {
  3.   useCapitalAllocateStore,
  4.   useCapitalInfoStore,
  5.   useTokenStore,
  6.   useUserInfoStore,
  7.   useUserMenuStore,
  8.   useUserRouteStore
  9. } from '@/stores';
  10. import { useRouter } from 'vue-router';
  11. export const useLogout = () => {
  12.   const tokenStore = useTokenStore();
  13.   const userInfoStore = useUserInfoStore();
  14.   const userMenuStore = useUserMenuStore();
  15.   const userRouteStore = useUserRouteStore();
  16.   const router = useRouter();
  17.   const businessStores = [useCapitalAllocateStore(), useCapitalInfoStore()];
  18.   const logout = () => {
  19.     try {
  20.       tokenStore.removeToken();
  21.       userInfoStore.removeInfo();
  22.       userMenuStore.removeMenu();
  23.       userRouteStore.removeRoute();
  24.       resetRoutes();
  25.       for (const store of businessStores) {
  26.         store?.$reset();
  27.       }
  28.     } catch (error) {
  29.       console.log('登出/退出系统出错:', error);
  30.     } finally {
  31.       router.push('/login');
  32.     }
  33.   };
  34.   return { logout };
  35. };
  36. export default useLogout;
复制代码

这段逻辑能工作,但 businessStores 是手写数组,新增业务 Store 后如果忘记加入,登出时就不会被重置。下面两种方案解决维护问题。

三、方案一:统一改用 stores.xxx 调用

思路是 import * as stores from '@/stores',把命名导入改成 stores.useXxxStore() 调用,然后遍历 stores 对象的所有导出。只要导出值是函数,就尝试获取 Store 实例,并在 $reset 是函数时调用。
  1. import * as stores from '@/stores';
  2. import { resetRoutes } from '@/router';
  3. import { useRouter } from 'vue-router';
  4. export const useLogout = () => {
  5.   const router = useRouter();
  6.   const logout = () => {
  7.     try {
  8.       const tokenStore = stores.useTokenStore();
  9.       tokenStore.removeToken();
  10.       const userInfoStore = stores.useUserInfoStore();
  11.       userInfoStore.removeInfo();
  12.       const userMenuStore = stores.useUserMenuStore();
  13.       userMenuStore.removeMenu();
  14.       const userRouteStore = stores.useUserRouteStore();
  15.       userRouteStore.removeRoute();
  16.       resetRoutes();
  17.       Object.values(stores).forEach((useStore) => {
  18.         if (typeof useStore === 'function') {
  19.           const store = useStore();
  20.           if (store && typeof store.$reset === 'function') {
  21.             store.$reset();
  22.           }
  23.         }
  24.       });
  25.     } catch (error) {
  26.       console.log('登出/退出系统出错:', error);
  27.     } finally {
  28.       router.push('/login');
  29.     }
  30.   };
  31.   return { logout };
  32. };
复制代码

优点:新增任何 Store 后无需修改登出逻辑,会被 Object.values(stores) 自动遍历重置。缺点:需要将原有命名导入全部替换为 stores.xxx 调用,改动范围较大。

四、方案二:额外导出构造函数数组

保留原有命名导入方式,在 stores/index.ts 中额外维护一个 businessStoreConstructors 数组,只放需要重置的业务 Store 构造函数。登出时遍历这个数组。

stores/index.ts:
  1. export { default as useTokenStore } from './tokenStore';
  2. export { default as useUserInfoStore } from './userInfoStore';
  3. export { default as useUserMenuStore } from './userMenuStore';
  4. export { default as useUserRouteStore } from './userRouteStore';
  5. export { default as useCapitalInfoStore } from './capitalInfoStore';
  6. export { default as useCapitalAllocateStore } from './capitalAllocateStore';
  7. export { default as useCapitalAllOptionStore } from './capitalAllOptionStore';
  8. export { default as useDepartmentStore } from './departmentStore';
  9. export const businessStoreConstructors = [
  10.   useCapitalInfoStore,
  11.   useCapitalAllocateStore,
  12.   useCapitalAllOptionStore,
  13.   useDepartmentStore,
  14. ];
复制代码

useLogout.ts:
  1. import { resetRoutes } from '@/router';
  2. import {
  3.   useTokenStore,
  4.   useUserInfoStore,
  5.   useUserMenuStore,
  6.   useUserRouteStore,
  7.   businessStoreConstructors,
  8. } from '@/stores';
  9. import { useRouter } from 'vue-router';
  10. export const useLogout = () => {
  11.   const router = useRouter();
  12.   const logout = () => {
  13.     try {
  14.       const tokenStore = useTokenStore();
  15.       tokenStore.removeToken();
  16.       const userInfoStore = useUserInfoStore();
  17.       userInfoStore.removeInfo();
  18.       const userMenuStore = useUserMenuStore();
  19.       userMenuStore.removeMenu();
  20.       const userRouteStore = useUserRouteStore();
  21.       userRouteStore.removeRoute();
  22.       resetRoutes();
  23.       businessStoreConstructors.forEach((useStore) => {
  24.         const store = useStore();
  25.         if (store && typeof store.$reset === 'function') {
  26.           store.$reset();
  27.         }
  28.       });
  29.     } catch (error) {
  30.       console.log('登出/退出系统出错:', error);
  31.     } finally {
  32.       router.push('/login');
  33.     }
  34.   };
  35.   return { logout };
  36. };
复制代码

方案二的关键是把构造函数集中到数组,数组元素要指向各 Store 的 useXxxStore 构造函数。原文示例在 index.ts 中直接列出这些名称,落地时需确保它们在该模块作用域内可引用。优点:无需改动原有命名导入的调用代码,只需在 index.ts 中维护一个数组,新增 Store 时手动添加一次即可。缺点:仍需手动维护数组,但只集中在一处,比每次改登出逻辑方便。

五、两种方案怎么选

如果希望新增 Store 后完全不用改登出代码,选方案一,代价是一次性修改 useLogout 中的调用方式。如果希望保留原有命名导入,且愿意在 stores/index.ts 维护数组,选方案二。两种方案均能解决 Store 重置问题。

六、$reset 判断与 resetRoutes 顺序

原文中 if (store && typeof store.$reset === 'function') 与 store?.$reset?.() 等价,都是先判断 store 存在、$reset 是函数再调用,可以避免方法不存在时报错。用可选链更简洁,但不是必须改。

关于 resetRoutes() 的位置:原文当前顺序是先删 token、用户信息、菜单、路由,再 resetRoutes(),再遍历重置业务 Store,最后跳登录。这个顺序不会导致错误,因为 resetRoutes 清理动态路由,不依赖 Store 状态;登录页通常是静态路由,不会被移除。若追求可读性,可以调整为:先清理用户状态,再重置业务 Store,再重置动态路由,最后 router.push('/login'),形成“清数据、清路由、跳登录”的顺序。

七、直接关闭浏览器会重置 Store 吗

会。Pinia Store 默认存在当前标签页的 JavaScript 运行时内存中,关闭标签页或浏览器后内存释放,下次打开重新初始化,state 函数返回初始值。因此正常退出调用 $reset 是为了立即清掉内存状态,防止切换账号残留;直接关闭浏览器则由内存释放自然完成重置。

如果项目后续引入 pinia-plugin-persistedstate 等持久化插件,把数据写入 localStorage 或 sessionStorage,关闭浏览器再打开可能恢复旧数据。此时登出逻辑除了 $reset,还要考虑清除对应持久化存储,否则仅靠内存重置不够。原文示例项目未引入持久化插件。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-10-23 12:19 , Processed in 0.028965 second(s), 29 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部