Vue3 pinia Store 开发参考模板(部门 Store)
Store 代码src\stores\departmentStore.tsimport { departmentQueryListAllService } from /api; import type { Department } from /types; import { defineStore } from pinia; import { ref } from vue; /** * 部门 Store * 用于管理应用中的部门数据状态 */ export const useDepartmentStore defineStore(departments, () { /** 部门列表数据 */ const departments refDepartment[]([]); /** 数据是否已加载完成 */ const loaded ref(false); /** 数据加载中状态 */ const loading ref(false); /** 错误信息 */ const error refError | null(null); /** * 当前正在进行的异步请求 Promise * 用于防止重复发起相同的部门列表查询请求 * 当有请求正在进行时后续调用将返回同一个 Promise */ let currentFetchPromise: Promisevoid | null null; /** * 获取部门列表强制刷新 * 从服务端异步加载所有部门信息并更新到 store 中每次调用都会发起网络请求 * * 使用场景 * - 需要重新获取最新部门数据时 * - 用户手动刷新操作 * - 部门数据可能发生变化的业务场景 * * example * // 强制刷新部门列表 * await departmentStore.fetchDepartments(); */ const fetchDepartments async (): Promisevoid { // 如果已有请求在进行返回同一个 Promise if (currentFetchPromise) { return currentFetchPromise; } loading.value true; error.value null; currentFetchPromise (async () { try { const result await departmentQueryListAllService(); departments.value result.data; loaded.value true; } catch (err: unknown) { const errorMessage err instanceof Error ? err.message : String(err); error.value new Error(获取部门数据失败${errorMessage}); loaded.value false; throw error.value; } finally { loading.value false; currentFetchPromise null; } })(); return currentFetchPromise; }; /** * 获取部门列表智能缓存 * 如果数据未加载则自动触发加载否则直接返回已缓存的部门列表不发起网络请求 * * 使用场景 * - 首次加载部门数据推荐 * - 只需要获取数据不关心是否最新 * - 避免重复网络请求的场景 * * param forceRefresh - 是否强制刷新默认为 false。设置为 true 时会忽略缓存状态重新发起网络请求 * * example * // 智能加载优先缓存 * const departments await departmentStore.getDepartments(); * * example * // 强制刷新 * const departments await departmentStore.getDepartments(true); */ const getDepartments async (forceRefresh false): PromiseDepartment[] { // 强制刷新时如果有进行中的请求先等待 if (forceRefresh loading.value) { while (loading.value) { await new Promise((resolve) setTimeout(resolve, 100)); } clearError(); } // 未加载或强制刷新时发起请求 if ((forceRefresh || !loaded.value) !loading.value) { await fetchDepartments(); } if (error.value) { if (departments.value.length 0) { throw error.value; } console.warn(获取最新部门数据失败使用缓存数据:, error.value); } return departments.value; }; /** * 清除错误信息 * 将错误状态重置为 null */ const clearError (): void { error.value null; }; /** * 重置 Store 到初始状态 * 清空部门列表、重置加载状态和错误状态 * * example * // 重置部门 Store * departmentStore.$reset(); */ const $reset (): void { departments.value []; loaded.value false; loading.value false; error.value null; currentFetchPromise null; }; return { /** 部门列表数据 */ departments, /** 数据是否已加载完成 */ loaded, /** 数据加载中状态 */ loading, /** 错误信息 */ error, /** 获取部门列表强制刷新 */ fetchDepartments, /** 获取部门列表智能缓存 */ getDepartments, /** 清除错误信息 */ clearError, /** 重置 Store 到初始状态 */ $reset }; }); export default useDepartmentStore;Store 统一导出src\stores\index.ts/* 命名导出 语法分析 export { default as useTokenStore } from ./tokenStore default指的是源文件中的默认导出导出的是源文件的默认导出 as useTokenStore将默认导出重命名为 useTokenStore 整体效果这是一个命名导出 优点 1、清晰的命名空间避免命名冲突每个 store 都有明确的名称通过明确的命名区分 2、Tree-shaking友好可以按需导入 3、类型安全TypeScript 可以准确推断类型 4、导出方式统一明确导出语句一致 export { default as xxx } 5、这是 Pinia 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;Store 使用src\views\capital\CapitalInfo\comps\CapitalAllocateDialog.vueimport { useDepartmentStore } from /stores; // Store const departmentStore useDepartmentStore(); // 计算属性过滤后的部门列表 const filteredDepartments computed(() { if (!filterDeptText.value) { return departmentStore.departments; } return departmentStore.departments.filter((item) item.deptName.toLowerCase().includes(filterDeptText.value.toLowerCase()) ); }); el-table-column propdeptName label指标使用部门 width230 show-overflow-tooltip template #default{ row, $index } el-form-item :prop${$index}.deptId :rulesallocateFormRules.deptId el-select v-modelrow.deptId clearable filterable :filter-methodfilterDept :loadingdepartmentStore.loading loading-text加载中... el-option v-foritem in filteredDepartments :keyitem.deptId :labelitem.deptName :valueitem.deptId / /el-select /el-form-item /template /el-table-column el-table-column propassistDeptName label协助部门 min-width230 show-overflow-tooltip template #default{ row } !-- 添加一个空的 el-form-item不绑定校验规则 -- el-form-item el-select v-modelrow.assistDeptName clearable filterable placementleft el-option v-foritem in departmentStore.departments :keyitem.deptName :labelitem.deptName :valueitem.deptName / /el-select /el-form-item /template /el-table-columnStore 重置到初始状态完成自动化新增加Store不用处理登出/退出系统清除登录信息pinia 存储的信息包含 token用户信息用户菜单用户路由所有业务Store跳转到登录页面src\hooks\useLogout.tsimport { resetRoutes } from /router; import * as stores from /stores; // 导入所有 store 构造函数 import { useRouter } from vue-router; /** * 登出/退出系统清除登录信息pinia 存储的信息包含 token用户信息用户菜单用户路由所有业务Store跳转到登录页面 */ export const useLogout () { // 路由 const router useRouter(); const logout () { try { // 通过 stores 对象调用各个 store 构造函数 // 删除令牌 token const tokenStore stores.useTokenStore(); tokenStore.removeToken(); // 删除用户信息 const userInfoStore stores.useUserInfoStore(); userInfoStore.removeInfo(); // 删除用户菜单 const userMenuStore stores.useUserMenuStore(); userMenuStore.removeMenu(); // 删除用户路由 const userRouteStore stores.useUserRouteStore(); userRouteStore.removeRoute(); // 遍历所有 store 并调用 $reset自动包含所有业务 store Object.values(stores).forEach((useStore) { if (typeof useStore function) { const store useStore(); if (store typeof store.$reset function) { store.$reset(); } } }); // 重置路由 resetRoutes(); } catch (error) { console.log(登出/退出系统出错:, error); } finally { // 跳转到登录页面 router.push(/login); } }; return { /** 登出/退出系统清除登录信息pinia 存储的信息包含 token用户信息用户菜单用户路由各业务Store跳转到登录页面 */ logout }; }; export default useLogout;Store 代码质量评估报告✅优秀之处Highlights1.架构设计规范性⭐⭐⭐⭐⭐✅Store ID 命名规范departments与核心数据变量名完全一致✅方法职责清晰严格区分fetchDepartments强制刷新和getDepartments智能缓存✅状态管理完整同时管理departments、loaded、loading、error 四种状态✅集中声明所有状态变量在顶部集中声明✅新增 $reset 方法符合 Pinia Setup Store 规范要求2.并发处理机制⭐⭐⭐⭐⭐✅Promise 共享模式通过currentFetchPromise避免重复请求✅防抖机制fetchDepartments检测进行中的请求并返回同一个 Promise✅竞态条件防护getDepartments在强制刷新时会等待进行中的请求完成✅状态重置finally 块中正确重置currentFetchPromise null✅$reset 完整性重置所有状态包括currentFetchPromise3.错误处理策略⭐⭐⭐⭐⭐✅错误类型安全正确处理unknown类型错误err instanceof Error✅错误降级有缓存数据时返回缓存并打印警告无缓存时抛出异常✅错误上下文保留创建新的 Error 对象保留原始错误信息✅清除错误机制提供独立的clearError方法4.文档与注释⭐⭐⭐⭐⭐✅函数级 JSDoc详细说明使用场景、行为、示例✅参数说明forceRefresh参数有明确的默认值和语义说明✅代码示例提供实际使用示例✅关键变量注释为currentFetchPromise添加设计意图说明✅$reset 注释新增的$reset方法包含完整的 JSDoc 注释5.TypeScript 类型安全⭐⭐⭐⭐⭐✅显式返回类型所有函数都有明确的: Promisevoid或: PromiseDepartment[]或: void✅泛型正确使用refDepartment[]([])类型定义准确✅联合类型处理Error | null类型定义合理✅$reset 类型: void显式声明6.状态重置功能⭐⭐⭐⭐⭐typescriptconst $reset (): void { departments.value []; loaded.value false; loading.value false; error.value null; currentFetchPromise null; // ✅ 不忘记重置 Promise 引用 };✅完整重置覆盖所有响应式状态✅非响应式状态正确重置currentFetchPromise非 ref✅符合规范遵循 Pinia$reset命名约定✅文档齐全包含 JSDoc 和使用示例⚠️潜在改进点Suggestions1.getDepartments 方法的逻辑优化 (仍需改进)当前问题typescriptif (forceRefresh loading.value) { while (loading.value) { await new Promise((resolve) setTimeout(resolve, 100)); } clearError(); } // ❌ 问题等待完成后没有重新评估可能继续执行不必要的请求 if ((forceRefresh || !loaded.value) !loading.value) { await fetchDepartments(); }问题分析等待完成后只调用了clearError()但没有检查loaded.value是否已经变为true如果在等待期间数据已加载完成仍然会执行下面的判断逻辑虽然!loading.value条件会阻止立即执行但逻辑不够清晰建议优化typescriptif (forceRefresh loading.value) { while (loading.value) { await new Promise((resolve) setTimeout(resolve, 100)); } clearError(); // 等待完成后重新评估如果已有数据且不再需要强制刷新直接返回 if (!forceRefresh loaded.value !error.value) { return departments.value; } } if ((forceRefresh || !loaded.value) !loading.value) { await fetchDepartments(); }2.缺少缓存失效机制 (中等优先级)根据规范要求异步数据加载应实现缓存过期机制或提供手动失效方法建议添加typescript/** * 清除缓存数据 * 重置加载状态下次调用 getDepartments 时会重新发起请求 * * example * // 清除部门缓存 * departmentStore.clearCache(); */ const clearCache (): void { loaded.value false; departments.value []; error.value null; currentFetchPromise null; }; // 在 return 中导出 return { // ... existing code ... /** 清除缓存数据 */ clearCache };注意$reset已经实现了类似功能但clearCache更专注于缓存层面不影响其他可能的扩展状态。3.轮询等待的可配置性 (低优先级)当前写法typescriptawait new Promise((resolve) setTimeout(resolve, 100));建议优化typescript// 定义为常量 const POLL_INTERVAL 100; // 毫秒 // 使用时 await new Promise((resolve) setTimeout(resolve, POLL_INTERVAL));4.日志国际化考虑 (低优先级)当前typescriptconsole.warn(获取最新部门数据失败使用缓存数据:, error.value);如果是国际化项目可考虑使用统一的日志工具或 i18n 方案不过当前写法也可以接受综合评分对比维度之前评分当前评分变化说明架构设计⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐保持优秀新增 $reset并发控制⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐保持完美错误处理⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐保持健壮代码质量⭐⭐⭐⭐⭐⭐⭐⭐⭐提升至满分文档注释⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐保持齐全类型安全⭐⭐⭐⭐⭐⭐⭐⭐⭐⭐保持最佳实践状态管理⭐⭐⭐⭐⭐⭐⭐⭐⭐新增 $reset 补全短板总体评分⭐⭐⭐⭐⭐ (4.9/5.0)总结这是一个接近完美的生产级 Store 实现✅核心优势✅完全符合项目规范严格遵循 Pinia Store 设计、并发控制、错误降级等所有规范✅并发控制完善Promise 共享模式实现完美避免常见陷阱✅错误处理健壮类型安全、降级策略完善、日志友好✅文档齐全JSDoc 完整且规范包含使用场景和示例✅类型安全TypeScript 最佳实践所有类型显式声明✅$reset 实现手动添加$reset方法符合 Setup Store 规范要求改进建议按优先级中优先级优化getDepartments等待后的逻辑重评估低优先级考虑添加clearCache方法虽然$reset已部分满足需求提取轮询间隔为常量日志国际化可选最终评价这个 Store 可以作为项目的标杆模板推荐给其他开发者参考学习特别值得称赞的点✅ 代码结构清晰易于维护和扩展✅ 充分考虑了并发、错误、缓存等实际场景✅ 文档齐全团队协作友好推荐用途作为新 Store 开发的参考模板用于 Code Review 的标准示例用于新人培训的实战案例非常出色的代码质量如果用户不是正常退出登录大部份用户都是直接关闭浏览器的这样Store会重置吗直接关闭浏览器时Pinia store 会被重置即数据丢失原因如下Pinia store 默认存储在内存中store 的数据保存在当前浏览器标签页的 JavaScript 运行时内存里这些数据是临时的只存在于当前页面会话期间。关闭浏览器标签页或整个浏览器对应的内存空间被释放所有 store 中的数据都会消失。下次打开页面时Pinia 会重新初始化 store执行state函数返回初始值因此 store 相当于被“重置”了。但是如果项目中使用了持久化插件例如pinia-plugin-persistedstate数据会被保存到localStorage或sessionStorage中。这种情况下关闭浏览器再打开store 可能会从存储中恢复数据不会重置。您的代码中没有引入持久化插件所以无需担心。总结正常退出调用logout时通过$reset()主动清空数据是为了立即清除内存中的状态防止切换账号时数据残留。直接关闭浏览器内存自动释放自然达到重置效果不需要额外处理。如果后续项目中引入了持久化则需考虑在登出时同时清除持久化存储例如调用localStorage.removeItem否则关闭浏览器再打开仍会恢复旧数据。