Agent skill

store-pc-pinia

基于若依-vue-plus框架的PC端Pinia状态管理完整规范。定义Store模块化架构、数据持久化策略、异步Action标准、响应式数据访问及TypeScript类型安全规范。 触发场景: - 开发用户信息管理、权限路由控制、系统设置或字典数据缓存等需要跨组件数据共享 - 需要实现Token持久化、用户登录状态管理、权限动态路由生成 - 需要管理全局应用状态(如侧边栏折叠状态、主题配置、字典数据缓存等) - 需要在组件间共享复杂业务逻辑或异步数据流 触发词:状态管理、Pinia、Store、用户信息、持久化、Token、权限管理、路由控制、全局状态、响应式数据

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/store-pc

SKILL.md

Pinia 状态管理规范

核心规范

规范1:Store模块化定义与命名规范

详细说明: 使用defineStore定义Store,必须遵循以下命名规范:

  • Store ID命名:采用小写字母加中划线,如'user''permission''app-settings'
  • Store函数命名:采用use[模块名]Store格式,如useUserStoreusePermissionStore
  • 模块化拆分:按业务功能拆分Store模块(用户、权限、设置、字典等),避免单一Store过于臃肿
  • 文件组织:每个Store模块独立文件存放在src/store/modules/目录下

核心原则:

  • 单一职责原则:每个Store只负责一个明确的业务领域
  • 唯一ID原则:Store ID在整个应用中必须唯一,避免命名冲突
  • 模块独立性:各Store模块之间尽量解耦,减少相互依赖

规范2:数据持久化配置标准

详细说明: 对于需要持久化的状态(如Token、用户信息、侧边栏状态、主题配置等),必须启用persist配置,并遵循以下规范:

存储策略选择:

  • localStorage:用于需要长期保存的数据(Token、用户偏好设置、主题配置)
  • sessionStorage:用于会话级别的临时数据(临时权限、页面缓存状态)

paths配置原则:

  • 必须显式指定需要持久化的字段路径,避免全量持久化
  • 禁止持久化临时状态(如loading、error、缓存时间戳等)
  • 支持嵌套路径配置,如['user.profile', 'user.settings']

key命名规范:

  • 采用项目前缀+模块名格式,如'ruoyi-user''ruoyi-permission'
  • 避免与其他项目或第三方库的Storage Key冲突

示例代码:

javascript
import { defineStore } from 'pinia';

// 标准的Store定义结构
export const useUserStore = defineStore('user', {
  state: () => ({
    // 认证信息
    token: '',
    refreshToken: '',
    // 用户基本信息
    name: '',
    avatar: '',
    nickName: '',
    // 权限相关
    roles: [],
    permissions: [],
    // 临时状态(不持久化)
    isLoading: false,
    lastUpdateTime: null
  }),
  
  getters: {
    // 计算属性:判断是否已登录
    isLoggedIn: (state) => !!state.token,
    // 计算属性:是否有特定权限
    hasPermission: (state) => (permission) => {
      return state.permissions.includes(permission);
    }
  },
  
  actions: {
    // ...actions定义见规范3
  },
  
  // 数据持久化配置
  persist: {
    enabled: true,
    strategies: [
      {
        key: 'ruoyi-user',           // 唯一的Storage Key
        storage: localStorage,        // 长期存储
        paths: ['token', 'refreshToken', 'name', 'avatar', 'roles', 'permissions'] // 显式指定持久化字段
      }
    ]
  }
});

规范3:异步Action与API解耦规范

详细说明: 所有异步逻辑(如登录请求、获取用户信息、数据CRUD操作)必须封装在actions中,严格遵循以下规范:

API调用规范:

  • 禁止在Vue组件中直接调用API接口
  • Action内部必须调用src/api目录下对应的接口方法
  • API方法与Action方法保持清晰的职责分离:API负责HTTP请求,Action负责状态更新

异步处理规范:

  • 使用async/await语法处理异步操作,避免回调地狱
  • 使用try/catch进行错误捕获,或依赖全局Axios拦截器统一处理
  • 异步Action应返回Promise,便于组件中链式调用和错误处理

状态更新规范:

  • Action中使用this访问和修改state
  • 复杂的状态更新逻辑应拆分为多个私有方法
  • 保证状态更新的原子性,避免中间状态暴露

错误处理策略:

  • 业务级错误(如登录失败):在Action中捕获并抛出给组件处理
  • 系统级错误(如网络超时):由全局Axios拦截器统一处理
  • 关键操作(如登录、支付):需要在Action中记录错误日志

示例代码:

javascript
// src/api/login.js - API层
import request from '@/utils/request';

export function login(username, password, code, uuid) {
  return request({
    url: '/login',
    method: 'post',
    data: { username, password, code, uuid }
  });
}

export function getInfo() {
  return request({
    url: '/getInfo',
    method: 'get'
  });
}

export function logout() {
  return request({
    url: '/logout',
    method: 'post'
  });
}
javascript
// src/store/modules/user.js - Store层
import { defineStore } from 'pinia';
import { login, getInfo, logout } from '@/api/login';
import { getToken, setToken, removeToken } from '@/utils/auth';
import defAva from '@/assets/images/profile.jpg';

export const useUserStore = defineStore('user', {
  state: () => ({
    token: getToken(),
    name: '',
    avatar: '',
    roles: [],
    permissions: []
  }),
  
  actions: {
    // 登录Action
    async login(userInfo) {
      const username = userInfo.username.trim();
      const password = userInfo.password;
      const code = userInfo.code;
      const uuid = userInfo.uuid;
      
      try {
        const res = await login(username, password, code, uuid);
        setToken(res.token);
        this.token = res.token;
        return res;
      } catch (error) {
        console.error('Login failed:', error);
        throw error; // 抛出错误供组件处理
      }
    },
    
    // 获取用户信息Action
    async getInfo() {
      try {
        const res = await getInfo();
        const user = res.user;
        const avatar = user.avatar === '' || user.avatar == null ? defAva : user.avatar;
        
        // 批量更新状态
        this.name = user.userName;
        this.avatar = avatar;
        this.roles = res.roles;
        this.permissions = res.permissions;
        
        return res;
      } catch (error) {
        console.error('Failed to get user info:', error);
        throw error;
      }
    },
    
    // 登出Action
    async logout() {
      try {
        await logout();
        this.resetState();
        removeToken();
      } catch (error) {
        console.error('Logout failed:', error);
        throw error;
      }
    },
    
    // 重置状态的私有方法
    resetState() {
      this.token = '';
      this.name = '';
      this.avatar = '';
      this.roles = [];
      this.permissions = [];
    }
  },
  
  persist: {
    enabled: true,
    strategies: [
      {
        key: 'ruoyi-user',
        storage: localStorage,
        paths: ['token', 'name', 'avatar', 'roles', 'permissions']
      }
    ]
  }
});

规范4:响应式数据访问规范

详细说明: 在Vue 3的Composition API中访问Store数据时,必须使用storeToRefs保持响应式:

响应式解构规范:

  • 使用storeToRefs解构state和getters,保持响应性
  • 直接解构actions(不需要转换为ref)
  • 避免直接解构整个store对象

组件中使用示例:

vue
<!-- 组件中正确使用Store -->
<script setup>
import { storeToRefs } from 'pinia';
import { useUserStore } from '@/store/modules/user';

const userStore = useUserStore();

// ✅ 正确:使用storeToRefs解构state,保持响应式
const { token, name, avatar, roles, permissions, isLoggedIn } = storeToRefs(userStore);

// ✅ 正确:直接解构actions(不需要storeToRefs)
const { login, logout, getInfo } = userStore;

// ❌ 错误:直接解构state会失去响应性
// const { token, name } = userStore;

// 使用示例
const handleLogin = async () => {
  try {
    await login({ username: 'admin', password: '123456' });
    console.log('登录成功,用户名:', name.value);
  } catch (error) {
    console.error('登录失败', error);
  }
};
</script>

<template>
  <div>
    <!-- 响应式数据自动更新 -->
    <div v-if="isLoggedIn">
      欢迎,{{ name }}
      <img :src="avatar" alt="头像" />
    </div>
  </div>
</template>

规范5:TypeScript类型安全规范(推荐)

详细说明: 在TypeScript项目中,为Store添加完整的类型定义以提升开发体验和代码健壮性:

typescript
// src/store/modules/user.ts
import { defineStore } from 'pinia';
import { login, getInfo, logout } from '@/api/login';
import { getToken, setToken, removeToken } from '@/utils/auth';

// 定义State类型
interface UserState {
  token: string;
  name: string;
  avatar: string;
  roles: string[];
  permissions: string[];
}

// 定义登录表单类型
interface LoginForm {
  username: string;
  password: string;
  code?: string;
  uuid?: string;
}

export const useUserStore = defineStore('user', {
  state: (): UserState => ({
    token: getToken() || '',
    name: '',
    avatar: '',
    roles: [],
    permissions: []
  }),
  
  getters: {
    isLoggedIn: (state): boolean => !!state.token,
    hasPermission: (state) => {
      return (permission: string): boolean => {
        return state.permissions.includes(permission);
      };
    }
  },
  
  actions: {
    async login(userInfo: LoginForm): Promise<void> {
      const username = userInfo.username.trim();
      const password = userInfo.password;
      
      try {
        const res = await login(username, password, userInfo.code, userInfo.uuid);
        setToken(res.token);
        this.token = res.token;
      } catch (error) {
        console.error('Login failed:', error);
        throw error;
      }
    },
    
    async getInfo(): Promise<void> {
      try {
        const res = await getInfo();
        this.name = res.user.userName;
        this.avatar = res.user.avatar || '';
        this.roles = res.roles;
        this.permissions = res.permissions;
      } catch (error) {
        console.error('Failed to get user info:', error);
        throw error;
      }
    }
  }
});

规范6:Store模块间通信规范

详细说明: 当多个Store需要相互调用时,遵循以下规范:

通信原则:

  • 避免循环依赖:Store A依赖Store B,则Store B不应依赖Store A
  • 优先使用事件总线或组合式API进行松耦合通信
  • 必要时可在Action中导入其他Store实例
javascript
// src/store/modules/permission.js
import { defineStore } from 'pinia';
import { useUserStore } from './user';  // 导入其他Store

export const usePermissionStore = defineStore('permission', {
  state: () => ({
    routes: [],
    addRoutes: []
  }),
  
  actions: {
    async generateRoutes() {
      // 在Action中使用其他Store
      const userStore = useUserStore();
      const roles = userStore.roles;
      
      // 根据角色生成路由...
      const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles);
      this.addRoutes = accessedRoutes;
      this.routes = constantRoutes.concat(accessedRoutes);
      
      return accessedRoutes;
    }
  }
});

禁止事项

状态管理禁止事项

  • 禁止在Vue组件中直接修改state:必须通过Store的actions修改状态,保证状态变更的可追踪性

    javascript
    // ❌ 错误做法
    const userStore = useUserStore();
    userStore.token = 'new-token';  // 直接修改
    
    // ✅ 正确做法
    userStore.updateToken('new-token');  // 通过action修改
    
  • 禁止在Store中直接调用HTTP请求:API调用必须封装在src/api目录中,Store只负责状态管理

    javascript
    // ❌ 错误做法
    actions: {
      async login() {
        const res = await axios.post('/login', data);  // 直接调用axios
      }
    }
    
    // ✅ 正确做法
    import { login } from '@/api/login';
    actions: {
      async login() {
        const res = await login(data);  // 调用封装好的API
      }
    }
    
  • 禁止持久化临时状态persist.paths必须显式指定,避免持久化loading、error等临时状态

    javascript
    // ❌ 错误做法
    persist: {
      enabled: true,
      strategies: [{ storage: localStorage }]  // 未指定paths,全量持久化
    }
    
    // ✅ 正确做法
    persist: {
      enabled: true,
      strategies: [{ 
        storage: localStorage,
        paths: ['token', 'name']  // 只持久化必要字段
      }]
    }
    

响应式相关禁止事项

  • 禁止直接解构Store失去响应性:必须使用storeToRefs

    javascript
    // ❌ 错误做法
    const { name, avatar } = useUserStore();  // 失去响应性
    
    // ✅ 正确做法
    const { name, avatar } = storeToRefs(useUserStore());
    
  • 禁止在setup外使用mapActions/mapState:Vue 3推荐使用Composition API

    javascript
    // ❌ 错误做法(Options API)
    computed: {
      ...mapState(useUserStore, ['name', 'avatar'])
    }
    
    // ✅ 正确做法(Composition API)
    const { name, avatar } = storeToRefs(useUserStore());
    

架构设计禁止事项

  • 禁止Store承担业务逻辑:Store只负责状态管理,复杂业务逻辑应封装在独立的服务层
  • 禁止创建过于臃肿的Store:单个Store文件超过300行应考虑拆分
  • 禁止Store之间循环依赖:Store A依赖Store B,则Store B不能依赖Store A
  • 禁止在Store中使用Vue组件实例:Store应独立于组件存在,不依赖Vue实例
  • 禁止在SSR场景下使用全局Store实例:SSR中每个请求应创建独立的Store实例

参考代码

若依框架核心Store模块

  • 用户Storesrc/store/modules/user.js - 用户信息、Token、权限管理
  • 权限Storesrc/store/modules/permission.js - 动态路由、菜单权限控制
  • 应用Storesrc/store/modules/app.js - 侧边栏状态、设备类型、语言配置
  • 设置Storesrc/store/modules/settings.js - 主题配置、布局设置
  • 字典Storesrc/store/modules/dict.js - 字典数据缓存

Store入口文件

  • 主入口src/store/index.js - Pinia实例创建和插件配置

配套工具文件

  • Token管理src/utils/auth.js - Token的存取删除工具函数
  • 请求封装src/utils/request.js - Axios实例配置和拦截器

最佳实践

1. Store模块划分建议

按照业务领域进行模块划分,常见模块:

  • user:用户认证、个人信息、会话管理
  • permission:权限控制、动态路由、菜单管理
  • app:应用全局状态(侧边栏、设备检测、多语言)
  • settings:用户偏好设置(主题、布局、字体大小)
  • dict:字典数据缓存(减少重复请求)
  • tags-view:标签页管理(已访问页面历史)

2. 性能优化建议

  • 按需加载:只在需要时才导入Store模块
  • 计算属性缓存:使用getters缓存复杂计算结果
  • 避免过度持久化:只持久化必要的状态,减少Storage占用
  • 批量更新:多个状态变更应在一个action中完成,减少响应式更新次数
  • 懒加载路由Store:路由权限Store可在登录后再初始化

3. 调试技巧

  • Pinia DevTools:使用Vue DevTools的Pinia扩展查看状态变化
  • 时间旅行调试:在DevTools中回溯状态变更历史
  • Action日志:在关键Action中添加console.log记录状态变化
  • 持久化调试:检查localStorage/sessionStorage确认持久化是否生效

4. 测试建议

javascript
// 单元测试示例
import { setActivePinia, createPinia } from 'pinia';
import { useUserStore } from '@/store/modules/user';

describe('User Store', () => {
  beforeEach(() => {
    setActivePinia(createPinia());
  });

  it('should update user info', () => {
    const store = useUserStore();
    store.name = 'Test User';
    expect(store.name).toBe('Test User');
  });

  it('should check login status', () => {
    const store = useUserStore();
    store.token = 'test-token';
    expect(store.isLoggedIn).toBe(true);
  });
});

检查清单

架构设计检查

  • Store模块是否按业务领域合理划分(user, permission, settings等)
  • Store ID命名是否唯一且符合kebab-case规范
  • 是否避免了Store之间的循环依赖
  • 单个Store文件是否控制在300行以内

数据持久化检查

  • 是否为需要持久化的状态配置了persist
  • 是否显式指定了paths参数,避免全量持久化
  • Storage Key命名是否包含项目前缀,避免冲突
  • 是否正确选择了localStorage/sessionStorage存储策略
  • 是否避免持久化临时状态(loading、error等)

API调用检查

  • 是否所有API调用都封装在src/api目录中
  • 是否所有异步操作都通过actions执行
  • 是否使用async/await处理异步逻辑
  • 是否有合理的错误处理机制(try/catch或全局拦截器)
  • Action是否返回Promise便于组件链式调用

响应式数据检查

  • 在组件中是否使用storeToRefs解构state和getters
  • 是否直接解构actions(不使用storeToRefs)
  • 是否避免了在组件中直接修改state
  • 是否正确使用.value访问ref包装的值

TypeScript检查(适用于TS项目)

  • 是否为State定义了完整的类型接口
  • 是否为Action参数和返回值定义了类型
  • 是否为Getters定义了返回值类型
  • 是否避免使用any类型

代码质量检查

  • Action命名是否语义清晰(login、logout、updateUserInfo)
  • 是否添加了必要的注释说明复杂逻辑
  • 是否有重置状态的方法(如logout时清空用户信息)
  • 是否遵循了单一职责原则(一个action只做一件事)

常见问题解答

Q1: 何时使用localStorage vs sessionStorage?

A:

  • localStorage:用于需要长期保存的数据(Token、用户偏好设置、记住我功能)
  • sessionStorage:用于会话级别的临时数据(临时权限、页面缓存状态、单次会话数据)

Q2: 如何处理Store中的异步并发问题?

A:

javascript
actions: {
  async fetchUserInfo() {
    if (this.isLoading) return;  // 防止重复请求
    this.isLoading = true;
    try {
      const res = await getInfo();
      this.name = res.user.userName;
    } finally {
      this.isLoading = false;
    }
  }
}

Q3: 如何在路由守卫中使用Store?

A:

javascript
// src/router/index.js
import { useUserStore } from '@/store/modules/user';

router.beforeEach(async (to, from, next) => {
  const userStore = useUserStore();
  
  if (userStore.token) {
    if (!userStore.roles.length) {
      await userStore.getInfo();
    }
    next();
  } else {
    next('/login');
  }
});

Q4: 如何重置所有Store状态?

A:

javascript
// 方法1:在各Store中提供reset方法
export const useUserStore = defineStore('user', {
  actions: {
    $reset() {
      this.token = '';
      this.name = '';
      // ... 重置所有状态
    }
  }
});

// 方法2:重新创建Pinia实例(谨慎使用)
import { createPinia } from 'pinia';
app.use(createPinia());

Q5: Store数据持久化后,如何处理版本升级导致的数据结构变化?

A:

javascript
// 在Store初始化时进行数据迁移
state: () => {
  const savedData = JSON.parse(localStorage.getItem('ruoyi-user') || '{}');
  
  // 版本检测和数据迁移
  if (savedData.version !== '2.0') {
    // 执行数据迁移逻辑
    return migrateData(savedData);
  }
  
  return savedData;
}

相关资源

官方文档

推荐插件

  • pinia-plugin-persistedstate:Pinia持久化插件
  • Vue DevTools:Vue官方调试工具(包含Pinia支持)

代码示例仓库

  • 若依Vue Plus官方仓库:参考完整的Store实现案例

Didn't find tool you were looking for?

Be as detailed as possible for better results