人人都会AI编程

13.4 大型项目状态分层与模块化设计最佳实践

更新时间:2026-07-11

当项目规模增长到几十个页面、上百个组件时,一股脑把所有状态扔进全局 Store 会让代码变成灾难:命名冲突、模块耦合、难以定位的数据变更、单个文件动辄上千行。想让状态管理可控,核心原则只有两条:分层模块化

第一层:局部组件状态

不是所有状态都值得全局共享。遵循“就近原则”:如果一个数据只被当前组件及其直接子组件使用,就放在组件自身的 refreactive 中。

判断标准:

  • 该数据是否只在当前组件内部使用?→ 用组件状态。
  • 是否只有父子之间需要传递?→ 用 props + emitsdefineModel
  • 是否多个不相关的视图组件需要共享?→ 才考虑提升到 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 使用 refreactive,保持响应式。
  • 对外的数据通过 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)可以轻松把关键模块的状态同步到 localStoragesessionStorage。在选择持久化时要有取舍:只持久化核心状态(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 做共享,全局基础设施做横切——各司其职,代码自然好改、好测、好交接。