状态管理不是只有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,
}
}
模块级变量users、loading等在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/