当项目规模增长到几十个页面、上百个组件时,一股脑把所有状态扔进全局 Store 会让代码变成灾难:命名冲突、模块耦合、难以定位的数据变更、单个文件动辄上千行。想让状态管理可控,核心原则只有两条:分层与模块化。
第一层:局部组件状态
不是所有状态都值得全局共享。遵循“就近原则”:如果一个数据只被当前组件及其直接子组件使用,就放在组件自身的 ref 或 reactive 中。
判断标准:
- 该数据是否只在当前组件内部使用?→ 用组件状态。
- 是否只有父子之间需要传递?→ 用
props+emits或defineModel。 - 是否多个不相关的视图组件需要共享?→ 才考虑提升到 Store。
示例:
<script setup>
// 这个表单的展开/收起状态只在这个组件内有用,不需要交给 Pinia
const isFormCollapsed = ref(false)
const formData = reactive({ name: '', age: 0 })
</script>
过早地把组件细节暴露进全局 Store,会让 Store 变成“大杂烩”,且组件失去独立性。
第二层:模块级共享状态
跨组件、跨页面的共享数据,用 Pinia 的 模块化 Store 管理。Pinia 天然支持按业务域拆分多个 Store,每个 Store 独立定义、独立使用。
模块划分原则:
- 按业务域拆分,而不是按技术维度。例如:
useUserStore(用户信息、登录态)、usePermissionStore(权限菜单)、useCartStore(购物车)、useOrderStore(订单流程)。 - 每个 Store 保持职责单一,一个 Store 只管理一个明确业务概念的状态和逻辑。
- 避免把所有共享状态塞进一个巨大的
useAppStore,这会走回 Vuex 时代“超级 Module”的老路。
目录结构参考:
src/
stores/
modules/
user.js # 用户信息、登录/登出
permission.js # 权限路由、按钮权限
cart.js # 购物车数据
order.js # 订单数据
index.js # 统一导出(可选)
每个 Store 文件内部结构清晰:
// stores/modules/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { getUserInfo, login } from '@/api/user'
export const useUserStore = defineStore('user', () => {
// State
const userInfo = ref(null)
const token = ref(localStorage.getItem('token') || '')
// Getters
const isLoggedIn = computed(() => !!token.value)
const userName = computed(() => userInfo.value?.name ?? '未登录')
// Actions
async function loginAction(username, password) {
const res = await login({ username, password })
token.value = res.token
localStorage.setItem('token', res.token)
await fetchUserInfo()
}
async function fetchUserInfo() {
if (!token.value) return
userInfo.value = await getUserInfo()
}
function logout() {
token.value = ''
userInfo.value = null
localStorage.removeItem('token')
}
return { userInfo, token, isLoggedIn, userName, loginAction, fetchUserInfo, logout }
})
关键规范:
- State 使用
ref或reactive,保持响应式。 - 对外的数据通过 Getter 暴露,避免外部直接篡改关键内部状态。
- 异步操作一律放在
actions中,不直接在组件内调用state.userInfo = ...后再发请求。 - Store 的命名约定:
use[Name]Store,让自动导入和代码补全更一致。
第三层:全局基础设施状态
有些数据与应用级基础设施相关,例如:全局加载状态、国际化语言、主题配置、WebSocket 连接状态。这类“横切关注点”也可以放在独立的 Store 中,但要严格控制数量,避免变成另一个垃圾桶。
示例:
// stores/modules/app.js
export const useAppStore = defineStore('app', () => {
const globalLoading = ref(false)
const language = ref('zh-CN')
const theme = ref('light')
function setTheme(themeName) {
theme.value = themeName
document.documentElement.setAttribute('data-theme', themeName)
}
return { globalLoading, language, theme, setTheme }
})
第四层:持久化状态与跨标签页同步
Pinia 本身不包含持久化,但通过插件(如 pinia-plugin-persistedstate)可以轻松把关键模块的状态同步到 localStorage 或 sessionStorage。在选择持久化时要有取舍:只持久化核心状态(token、用户偏好),不要把整个购物车或表单草稿全量持久化,避免存储膨胀和数据泄露风险。
// 在 main.js 中
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
在需要持久化的 Store 中配置:
export const useUserStore = defineStore('user', () => {
// ...
return { token, userInfo }
}, {
persist: {
storage: localStorage,
paths: ['token'] // 只持久化 token,userInfo 由登录后重新获取
}
})
模块间通信与协作
理想情况下 Store 之间应尽量减少直接依赖,让组件作为“协调者”来调用多个 Store 的 Action。当确实需要 Store 间引用时,直接在 Action 内部调用另一个 Store 的实例即可(Pinia 没有 Vuex 的模块嵌套限制)。
推荐做法:组件编排多个 Store
<script setup>
import { useUserStore } from '@/stores/modules/user'
import { useCartStore } from '@/stores/modules/cart'
const user = useUserStore()
const cart = useCartStore()
async function handleCheckout() {
if (!user.isLoggedIn) return router.push('/login')
await cart.submitOrder()
}
</script>
如果必须 Store 间通信:
// 在 order Store 中调用 user Store
import { useUserStore } from './user'
export const useOrderStore = defineStore('order', () => {
async function submitOrder() {
const user = useUserStore()
if (!user.token) throw new Error('未登录')
// 提交订单逻辑...
}
return { submitOrder }
})
这种方式的局限在于会产生隐式耦合,建议仅在少数核心流程中使用,并在文档注释中标明依赖关系。
避免反模式
- 不要把所有状态都往 Pinia 里扔,尤其是表单的临时可见性、动画状态等局部数据。
- 不要在一个 Action 里直接修改另一个 Store 的 State,应通过另一个 Store 的 Action 来操作,保证行为封装。
- 避免无节制使用全局 Store,新组件先思考:“这个状态有几个地方需要?” 少于3个,大概率留在组件内更好。
- 避免在 Getter 中进行重型计算,它应该是个纯函数、快速返回。复杂派生数据考虑使用
computed加缓存或后台计算。
一句话总结: 大型项目的状态管理,不是“把所有数据放在一个地方管起来”,而是“明确每一份数据的归属和责任边界”。组件状态做细节,模块 Store 做共享,全局基础设施做横切——各司其职,代码自然好改、好测、好交接。