Vue3组合式API状态管理实战:告别Pinia冗余,用composable构建可复用状态层

状态管理不是只有Pinia

Vue3项目里一提到状态管理,绝大多数团队直接上Pinia。Pinia确实好用,但当项目规模增长,你会发现几个问题:store文件越来越多、跨store的逻辑复用困难、组件里useXxxStore()的调用散布各处形成隐式依赖。更关键的是——很多状态根本不需要全局store,一个composable函数就够了。

组合式API的composable模式,本质上就是在组件之外封装响应式状态和操作逻辑。当多个组件需要共享同一份状态时,composable天然提供了单例模式(模块级变量),不需要额外引入状态管理库。

判断什么时候用composable、什么时候用Pinia:

composable:状态作用域有限(几个相关组件共享),逻辑内聚且自包含
Pinia:状态需要跨多个不相关的功能模块访问,需要DevTools调试状态快照,需要SSR时状态水合

用composable构建领域状态

以一个用户管理模块为例。用户列表、搜索过滤、分页、选中状态——这些逻辑在用户管理页面和用户选择弹窗中都要用到,但和订单模块无关。用composable封装:

// composables/useUserManager.ts
import { ref, computed, readonly } from 'vue'

// 模块级变量——多处调用共享同一份状态
const users = ref<User[]>([])
const loading = ref(false)
const searchKeyword = ref('')
const currentPage = ref(1)
const pageSize = ref(20)
const selectedIds = ref<Set<string>>(new Set())

export function useUserManager() {
  const filteredUsers = computed(() => {
    if (!searchKeyword.value) return users.value
    const kw = searchKeyword.value.toLowerCase()
    return users.value.filter(
      u => u.name.toLowerCase().includes(kw) || u.email.toLowerCase().includes(kw)
    )
  })

  const pagedUsers = computed(() => {
    const start = (currentPage.value - 1) * pageSize.value
    return filteredUsers.value.slice(start, start + pageSize.value)
  })

  const totalPages = computed(() =>
    Math.ceil(filteredUsers.value.length / pageSize.value)
  )

  async function fetchUsers() {
    loading.value = true
    try {
      const { data } = await api.get('/users', {
        params: { page: currentPage.value, size: pageSize.value, keyword: searchKeyword.value }
      })
      users.value = data.items
    } finally {
      loading.value = false
    }
  }

  function toggleSelect(id: string) {
    if (selectedIds.value.has(id)) {
      selectedIds.value.delete(id)
    } else {
      selectedIds.value.add(id)
    }
    // 触发响应式更新——Set的add/delete不是响应式的
    selectedIds.value = new Set(selectedIds.value)
  }

  function reset() {
    searchKeyword.value = ''
    currentPage.value = 1
    selectedIds.value.clear()
  }

  return {
    users: readonly(users),
    loading: readonly(loading),
    searchKeyword,  // 允许外部修改
    currentPage,
    filteredUsers,
    pagedUsers,
    totalPages,
    selectedIds: readonly(selectedIds),
    fetchUsers,
    toggleSelect,
    reset,
  }
}

模块级变量usersloading等在composable函数外部声明,所有调用useUserManager()的组件共享同一份响应式状态。对外暴露readonly版本防止外部直接修改,修改只能通过composable提供的方法。这就是composable做状态管理的核心模式。

composable的依赖注入与测试

composable直接引用模块级变量有个问题——单元测试时多个测试用例共享状态,测试之间会互相污染。解法是用provide/inject做依赖注入

// composables/useUserManager.ts
import { inject, provide, ref, computed, type InjectionKey } from 'vue'

// 定义注入key的类型
export const UserManagerKey: InjectionKey<ReturnType<typeof createUserManager>> 
  = Symbol('UserManager')

// 创建函数——每次调用返回独立实例
function createUserManager() {
  const users = ref<User[]>([])
  const loading = ref(false)
  // ...和之前一样的逻辑

  return { users, loading, fetchUsers, /* ... */ }
}

// Provider组件
export function provideUserManager() {
  const manager = createUserManager()
  provide(UserManagerKey, manager)
  return manager
}

// Consumer composable
export function useUserManager() {
  const manager = inject(UserManagerKey)
  if (!manager) {
    throw new Error('useUserManager must be used after provideUserManager')
  }
  return manager
}

在根组件提供:

// App.vue
import { provideUserManager } from './composables/useUserManager'

const userManager = provideUserManager()

测试时可以独立创建实例,互不干扰:

// tests/useUserManager.test.ts
import { createUserManager } from '@/composables/useUserManager'

test('搜索过滤用户', () => {
  const manager = createUserManager()  // 独立实例
  manager.users.value = [
    { id: '1', name: 'Alice', email: 'alice@test.com' },
    { id: '2', name: 'Bob', email: 'bob@test.com' },
  ]
  manager.searchKeyword.value = 'ali'
  expect(manager.filteredUsers.value).toHaveLength(1)
  expect(manager.filteredUsers.value[0].name).toBe('Alice')
})

composable之间的组合

当业务逻辑变复杂,一个composable会依赖另一个composable的状态。比如”用户管理”composable依赖”权限”composable来判断当前用户能否执行某些操作。

// composables/usePermission.ts
const currentUserRole = ref<Role>('viewer')

export function usePermission() {
const canEdit = computed(() => ['admin', 'editor'].includes(currentUserRole.value))
const canDelete = computed(() => currentUserRole.value === 'admin')
return { currentUserRole, canEdit, canDelete }
}

// composables/useUserManager.ts
export function useUserManager() {
const { canEdit, canDelete } = usePermission()

// 在composable内部使用权限状态
const editableUsers = computed(() =>
canEdit.value ? filteredUsers.value : []
)

return { editableUsers, canEdit, canDelete, /* ... */ }
}

这种组合模式比Pinia的store互相引用更干净——composable之间的依赖是显式的函数调用关系,而不是通过全局store ID隐式耦合。

什么时候还是要用Pinia

composable做状态管理不是银弹,以下场景Pinia更合适:

1. DevTools调试:Pinia的Vue DevTools集成可以直接查看所有store的状态快照和时间旅行,composable的状态在DevTools里看不到
2. SSR状态水合:Nuxt3的Pinia模块自动处理server端状态序列化和client端水合,composable需要手动处理
3. 插件生态:Pinia的插件机制支持持久化(pinia-plugin-persistedstate)、状态快照、Action订阅等横切关注点
4. 跨应用状态共享:微前端场景下,Pinia的store可以通过Web SharedWorker跨应用共享状态

工程实践中,建议用composable处理领域内聚状态,用Pinia处理全局横切状态(如用户身份、主题、国际化)。两者不冲突,composable内部完全可以调用Pinia store。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-zhuang-tai-guan-li-shi-zhan-gao-bie/

(0)
小编小编
上一篇 17小时前
下一篇 17小时前

相关推荐