Pinia 的模块化设计理念与 Vuex 有根本性不同。Vuex 通过嵌套的模块树来组织状态,需要借助 namespaced、rootGetters、dispatch('module/action') 等机制隔离子模块,配置繁琐且类型推断困难。而 Pinia 采用扁平化的多 Store 设计:每个 Store 都是一个独立的“状态单元”,彼此之间可以自由引用,没有强制的层级结构。
多 Store 设计:用文件天然分割模块
在 Pinia 中,模块化是通过创建多个 Store 文件来实现的。每个 Store 文件包含该业务域的 state、getters 和 actions,通过命名约定(如 useUserStore、useCartStore)和直观的导入(import)来关联。
src/stores/
├── user.js # 用户相关状态与方法
├── cart.js # 购物车逻辑
└── product.js # 商品列表与详情
在任何组件或其它 Store 中,直接引入需要的 Store 实例即可:
// 在 cart.js 中引用 user.js
import { defineStore } from 'pinia'
import { useUserStore } from './user'
export const useCartStore = defineStore('cart', () => {
const userStore = useUserStore()
// 现在可以在 cart 的 getter 或 action 中使用 userStore.userInfo
})
这种设计的实际好处很直接:
- 没有嵌套层级,不需要记忆
dispatch('moduleA/moduleB/action')的路径,直接userStore.login()。 - 类型推断完整,TypeScript 下每个 Store 的类型自动推导,不需要额外声明模块类型。
- 按需引用,不会像 Vuex 的模块树那样一次性注册所有模块,Pinia 的 Store 只有在第一次使用时才会被初始化。
你不需要在项目开始时就决定“全局状态应该划分成几层嵌套”,而是随着业务发展自然地在 stores 目录下新增文件,每个 Store 只关注自己那一块业务。
Store 的两种定义方式
Pinia 提供了两种定义 Store 的语法,选择哪一种完全取决于你的编码习惯。
1. 选项式语法(类似 Vuex)
如果你习惯 Vue 的 Options API,可以用一个对象来定义 Store,包含 state、getters 和 actions 三个固定属性。
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0,
}),
getters: {
doubleCount: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})
注意 state 必须是一个箭头函数,确保每个请求或 SSR 上下文都有独立状态。在 actions 中使用 this 可以访问当前 Store 实例,它会被 Pinia 正确绑定。
2. 组合式语法(推荐)
利用 Composition API 的 ref、computed、普通函数来构建 Store,这就是一个普通的组合式函数(Hook),但通过 defineStore 包装后变成可全局共享的单例。
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0) // state
const doubleCount = computed(() => count.value * 2) // getter
function increment() { // action
count.value++
}
return { count, doubleCount, increment }
})
这种写法更贴合 Vue 3 的组合式 API 风格,没有 this,更容易做逻辑抽取和复用。你在 Store 中返回任何响应式数据和方法,它们会在组件中保持响应。
两者对比与选择建议:
- 选项式语法更适合从 Vuex 迁移的项目或喜欢清晰分区的开发者。
- 组合式语法更灵活,可以自由使用
watch、生命周期钩子(如onMounted在 Store 中通常不推荐,但技术上可行),也更容易将业务逻辑抽象成可复用的函数。官方推荐在项目中统一使用组合式语法,因为它与 Vue 3 的组合式 API 理念一致。
真实项目中的 Store 组织
一个典型的用户 Store 可能长这样(组合式语法):
// stores/useUserStore.js
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
import { loginApi, getUserInfoApi } from '@/api/user'
import router from '@/router'
export const useUserStore = defineStore('user', () => {
const token = ref(localStorage.getItem('token') || '')
const userInfo = ref(null)
const isLoggedIn = computed(() => !!token.value)
async function login(credentials) {
const res = await loginApi(credentials)
token.value = res.token
localStorage.setItem('token', res.token)
await fetchUserInfo()
}
async function fetchUserInfo() {
const res = await getUserInfoApi()
userInfo.value = res
}
function logout() {
token.value = ''
userInfo.value = null
localStorage.removeItem('token')
router.push('/login')
}
return { token, userInfo, isLoggedIn, login, fetchUserInfo, logout }
})
组件中使用时简洁直观:
const userStore = useUserStore()
// 调用 userStore.login(form) 或读取 userStore.isLoggedIn
这种模块化方式与组合式定义,让状态管理代码真正变成了“有结构、可追溯的业务层”,而不是一个难以维护的全局大对象。