在 Vue3 项目中,Pinia Store 默认保存在当前页面的内存里。用户正常退出登录时,如果只删除 token 或跳转登录页,业务 Store 中的状态可能残留,切换账号后容易读到旧数据。因此,登出逻辑除了清理 token、用户信息、菜单和路由,还需要把业务 Store 重置到初始状态。Pinia 提供了 $reset(),但前提是能拿到对应 Store 实例;当项目在 stores/index.ts 中统一命名导出多个 Store 时,如何遍历重置是本文要解决的问题。
一、前提:统一导出 Store
原文示例在 src/stores/index.ts 中使用命名导出,把各 Store 的默认导出重命名为 useXxxStore。这样每个 Store 有明确名称,便于按需导入和类型推断。- export { default as useTokenStore } from './tokenStore';
- export { default as useUserInfoStore } from './userInfoStore';
- export { default as useUserMenuStore } from './userMenuStore';
- export { default as useUserRouteStore } from './userRouteStore';
- export { default as useCapitalInfoStore } from './capitalInfoStore';
- export { default as useCapitalAllocateStore } from './capitalAllocateStore';
- export { default as useCapitalAllOptionStore } from './capitalAllOptionStore';
- export { default as useDepartmentStore } from './departmentStore';
复制代码
二、原始登出 hook 的问题
src/hooks/useLogout.ts 中先获取 tokenStore、userInfoStore、userMenuStore、userRouteStore,再把部分业务 Store 放进 businessStores 数组,最后逐个调用 $reset()。核心代码如下:- import { resetRoutes } from '@/router';
- import {
- useCapitalAllocateStore,
- useCapitalInfoStore,
- useTokenStore,
- useUserInfoStore,
- useUserMenuStore,
- useUserRouteStore
- } from '@/stores';
- import { useRouter } from 'vue-router';
- export const useLogout = () => {
- const tokenStore = useTokenStore();
- const userInfoStore = useUserInfoStore();
- const userMenuStore = useUserMenuStore();
- const userRouteStore = useUserRouteStore();
- const router = useRouter();
- const businessStores = [useCapitalAllocateStore(), useCapitalInfoStore()];
- const logout = () => {
- try {
- tokenStore.removeToken();
- userInfoStore.removeInfo();
- userMenuStore.removeMenu();
- userRouteStore.removeRoute();
- resetRoutes();
- for (const store of businessStores) {
- store?.$reset();
- }
- } catch (error) {
- console.log('登出/退出系统出错:', error);
- } finally {
- router.push('/login');
- }
- };
- return { logout };
- };
- export default useLogout;
复制代码
这段逻辑能工作,但 businessStores 是手写数组,新增业务 Store 后如果忘记加入,登出时就不会被重置。下面两种方案解决维护问题。
三、方案一:统一改用 stores.xxx 调用
思路是 import * as stores from '@/stores',把命名导入改成 stores.useXxxStore() 调用,然后遍历 stores 对象的所有导出。只要导出值是函数,就尝试获取 Store 实例,并在 $reset 是函数时调用。- import * as stores from '@/stores';
- import { resetRoutes } from '@/router';
- import { useRouter } from 'vue-router';
- export const useLogout = () => {
- const router = useRouter();
- const logout = () => {
- try {
- const tokenStore = stores.useTokenStore();
- tokenStore.removeToken();
- const userInfoStore = stores.useUserInfoStore();
- userInfoStore.removeInfo();
- const userMenuStore = stores.useUserMenuStore();
- userMenuStore.removeMenu();
- const userRouteStore = stores.useUserRouteStore();
- userRouteStore.removeRoute();
- resetRoutes();
- Object.values(stores).forEach((useStore) => {
- if (typeof useStore === 'function') {
- const store = useStore();
- if (store && typeof store.$reset === 'function') {
- store.$reset();
- }
- }
- });
- } catch (error) {
- console.log('登出/退出系统出错:', error);
- } finally {
- router.push('/login');
- }
- };
- return { logout };
- };
复制代码
优点:新增任何 Store 后无需修改登出逻辑,会被 Object.values(stores) 自动遍历重置。缺点:需要将原有命名导入全部替换为 stores.xxx 调用,改动范围较大。
四、方案二:额外导出构造函数数组
保留原有命名导入方式,在 stores/index.ts 中额外维护一个 businessStoreConstructors 数组,只放需要重置的业务 Store 构造函数。登出时遍历这个数组。
stores/index.ts:- export { default as useTokenStore } from './tokenStore';
- export { default as useUserInfoStore } from './userInfoStore';
- export { default as useUserMenuStore } from './userMenuStore';
- export { default as useUserRouteStore } from './userRouteStore';
- export { default as useCapitalInfoStore } from './capitalInfoStore';
- export { default as useCapitalAllocateStore } from './capitalAllocateStore';
- export { default as useCapitalAllOptionStore } from './capitalAllOptionStore';
- export { default as useDepartmentStore } from './departmentStore';
- export const businessStoreConstructors = [
- useCapitalInfoStore,
- useCapitalAllocateStore,
- useCapitalAllOptionStore,
- useDepartmentStore,
- ];
复制代码
useLogout.ts:- import { resetRoutes } from '@/router';
- import {
- useTokenStore,
- useUserInfoStore,
- useUserMenuStore,
- useUserRouteStore,
- businessStoreConstructors,
- } from '@/stores';
- import { useRouter } from 'vue-router';
- export const useLogout = () => {
- const router = useRouter();
- const logout = () => {
- try {
- const tokenStore = useTokenStore();
- tokenStore.removeToken();
- const userInfoStore = useUserInfoStore();
- userInfoStore.removeInfo();
- const userMenuStore = useUserMenuStore();
- userMenuStore.removeMenu();
- const userRouteStore = useUserRouteStore();
- userRouteStore.removeRoute();
- resetRoutes();
- businessStoreConstructors.forEach((useStore) => {
- const store = useStore();
- if (store && typeof store.$reset === 'function') {
- store.$reset();
- }
- });
- } catch (error) {
- console.log('登出/退出系统出错:', error);
- } finally {
- router.push('/login');
- }
- };
- return { logout };
- };
复制代码
方案二的关键是把构造函数集中到数组,数组元素要指向各 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,还要考虑清除对应持久化存储,否则仅靠内存重置不够。原文示例项目未引入持久化插件。 |