6.2 模块化与 Pinia:仓库的拆与换


文档摘要

6.2 模块化与 Pinia:仓库的拆与换 本节摘要:大项目的 Vuex store 靠 modules 按领域拆分,命名空间解决命名冲突,但跨模块调用与根实例耦合仍是痛点。Pinia 用去中心化的多个小 store 替代单一根 store,砍掉 mutations 层,天然支持组合式 API 与 TypeScript。本节讲清拆分方法与换代逻辑,并给出迁移判断。 学习目标 阅读完本节,你应当能够: 把巨型 store 按领域拆成带命名空间的 modules; 写出跨模块读状态与调 action 的代码,避开常见耦合坑; 用 Pinia 的 setup store 风格组织一个小仓库; 判断存量 Vuex 工程要不要迁、怎么分阶段迁。

6.2 模块化与 Pinia:仓库的拆与换

本节摘要:大项目的 Vuex store 靠 modules 按领域拆分,命名空间解决命名冲突,但跨模块调用与根实例耦合仍是痛点。Pinia 用去中心化的多个小 store 替代单一根 store,砍掉 mutations 层,天然支持组合式 API 与 TypeScript。本节讲清拆分方法与换代逻辑,并给出迁移判断。

学习目标

阅读完本节,你应当能够:

  1. 把巨型 store 按领域拆成带命名空间的 modules;
  2. 写出跨模块读状态与调 action 的代码,避开常见耦合坑;
  3. 用 Pinia 的 setup store 风格组织一个小仓库;
  4. 判断存量 Vuex 工程要不要迁、怎么分阶段迁。

一、Vuex 模块化:按领域拆,不按页面拆

单一 store 超过几百行就该拆。拆分维度选领域(用户、订单、购物车)而不是页面——页面会改版合并,领域相对稳定;且页面组件天然只关心自己领域的数据。

// 每个模块一个文件,各自拥有完整的四件套 const user = { namespaced: true, // 关键:开启命名空间 state: () => ({ profile: null, token: '' }), getters: { isLoggedIn: (s) => !!s.token }, mutations: { SET_PROFILE(state, p) { state.profile = p; } }, actions: { async login({ commit }, form) { const res = await api.login(form); commit('SET_PROFILE', res.profile); } } }; export default new Vuex.Store({ modules: { user, cart, order } });

命名空间开启后,调用要带模块前缀:

this.$store.state.user.profile; // 读状态带模块名 this.$store.dispatch('user/login', form); // 动作带斜杠路径 ...mapGetters('user', ['isLoggedIn']); // 辅助函数第一参数指定模块

跨模块交互用 action 上下文里的 rootState 与 rootGetters:

const order = { namespaced: true, actions: { async checkout({ commit, dispatch, rootState, rootGetters }) { if (!rootGetters['user/isLoggedIn']) { return dispatch('user/login', null, { root: true }); // 调别模块的 action } // 下单逻辑... } } };

坑有两处。一是模块内部 mutation 与 getter 注册默认是全局命名空间(不开启 namespaced 时所有模块的 mutations 混在一个池子里,同名互相覆盖),所以除了极小项目,模块一律开命名空间;二是模块间大量互相 dispatch 是坏味道——领域之间耦合到需要频繁互调,说明领域边界划错了,先审边界再写代码。

二、Pinia:把一棵树换成一片森林

Pinia 的核心变化:没有根 store,只有一组平等的独立 store;mutations 层被删除,组件与 action 都直接读写 state。

import { defineStore } from 'pinia'; // 选项式风格:与 Vuex 模块神似,迁移成本低 export const useCartStore = defineStore('cart', { state: () => ({ items: [] }), getters: { total: (s) => s.items.reduce((sum, i) => sum + i.price * i.count, 0) }, actions: { addItem(item) { this.items.push({ ...item, count: 1 }); // 直接改,没有 mutation } } });
// 组合式风格:就是一组 ref 与函数,Vue 3 项目的推荐形态 export const useUserStore = defineStore('user', () => { const token = ref(''); const isLoggedIn = computed(() => !!token.value); async function login(form) { const res = await api.login(form); token.value = res.token; } return { token, isLoggedIn, login }; });

组件里使用:

const cart = useCartStore(); // 像调一个组合函数 cart.addItem(goods); const total = computed(() => cart.total); // 保持响应式

Vuex 与 Pinia 对比矩阵

Vuex 与 Pinia 对比矩阵

三、Pinia 敢砍 mutation 的理由

Vuex 的同步红线是为了时间旅行快照的确定性。Pinia 的解法换了个角度:变更追踪改由 patch 与 subscribe 承担——需要批量变更时用 patch 提交对象或函数,需要监听时订阅整个 store 的变化,快照语义由订阅机制保证,不再需要把所有写入逼进一条窄门。代价是"所有修改必须走 mutation"这条强约束带来的代码纪律消失了,团队约定要补位:action 命名保持事件语义、组件里只读不写,这两条写进规范即可兜住大半风险。

四、迁移判断与路线

存量 Vuex 4 工程的处理建议:

  • 不迁移的理由成立就别动:Vuex 4 在 Vue 3 下工作正常,无维护压力的项目迁移收益纯靠情怀;
  • 触发迁移的信号:TypeScript 化改造(Vuex 类型推导痛苦在 TS 项目里被放大)、新模块与老仓库并行开发别扭、团队招人后上手成本高;
  • 分阶段路线:两套可共存——新功能全部用 Pinia 建 store,老模块按改动频率逐步搬,公共桥接处用老仓库的 plugin 或直接在组件里同时引两边,最后一次性清掉 Vuex。

搬运单模块的映射很机械:模块的 state 变成新 store 的 state,mutations 与 actions 合并进 actions(同步异步都放),getters 原样。命名空间前缀自动消失,调用点从 dispatch('user/login') 改为 useUserStore().login(),IDE 重构工具能覆盖大部分改动。

本节要点回顾

  • 拆分维度:按领域拆模块、一律开命名空间,模块间高频互调说明边界划错;
  • 跨模块:rootState/rootGetters 读,root 参数调,是受控的逃生口而非日常通道;
  • Pinia 结构:多 store 平等共存、无 mutation 层、setup 风格即组合函数;
  • 砍层的代价与补偿:追踪改由 patch/subscribe 承担,纪律靠团队规范补位;
  • 迁移策略:无压力不迁,触发信号出现则新功能先行、老模块渐搬、最后清库。

两套仓库共存期还有一个容易被忽略的坑:同一段业务数据只能有一个主人。如果用户信息在 Vuex 与 Pinia 里各存了一份,两边各有更新逻辑,状态不一致只是时间问题。共存期的纪律是按领域划界——老的 user 模块还归 Vuex,新的订单域用 Pinia,任何数据都不允许双边写,等某领域彻底迁完再交割。这条纪律写进迁移文档,比口头约定可靠得多。搬移过程中每完成一个领域,记得同步删除老仓库里的对应模块与调用点,半拉子共存最容易在几个月后被新同事误用——他们在搜索示例时找到老写法,照抄之后一个项目里两种心智模型并存,沟通成本反而高于不迁。

状态有了主人,接下来解决"页面级视图怎么切换"——路由管理。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U