Vue3组合式API将组件逻辑组织方式从Options API的选项分散模式转变为函数聚合模式。Pinia作为Vue3官方状态管理库,天然契合组合式API风格。本文通过组合式函数封装业务逻辑、Pinia构建跨组件状态、持久化方案与测试,给出完整的工程化实践。
Pinia状态仓库搭建与模块化拆分
Pinia的Store使用defineStore定义,组合式API写法(Setup Store)直接返回响应式数据和方法,比Options写法更灵活。
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { userApi } from '@/api/user'
export const useUserStore = defineStore('user', () => {
const userInfo = ref<UserInfo | null>(null)
const token = ref<string>('')
const permissions = ref<string[]>([])
const isLoggedIn = computed(() => !!token.value)
const hasPermission = computed(() => {
return (perm: string) => permissions.value.includes(perm)
})
const displayName = computed(() => {
if (!userInfo.value) return '游客'
return userInfo.value.nickname || userInfo.value.username
})
async function login(credentials: LoginParams) {
const res = await userApi.login(credentials)
token.value = res.token
userInfo.value = res.user
permissions.value = res.permissions
return res
}
function logout() {
token.value = ''
userInfo.value = null
permissions.value = []
}
return {
userInfo, token, permissions,
isLoggedIn, hasPermission, displayName,
login, logout
}
})
大型项目按业务域拆分Store,通过组合式函数在Store之间共享逻辑:
// composables/useEntityCrud.ts
import { ref, reactive } from 'vue'
export function useEntityCrud<T extends { id: string | number }>(
fetchFn: (params: PageParams) => Promise<PageResult<T>>,
createFn: (data: Partial<T>) => Promise<T>,
updateFn: (id: T['id'], data: Partial<T>) => Promise<T>,
deleteFn: (id: T['id']) => Promise<void>
) {
const list = ref<T[]>([])
const total = ref(0)
const loading = ref(false)
const params = reactive<PageParams>({
page: 1, pageSize: 20, keyword: ''
})
async function fetchList() {
loading.value = true
try {
const res = await fetchFn(params)
list.value = res.items
total.value = res.total
} finally {
loading.value = false
}
}
async function create(data: Partial<T>) {
await createFn(data)
await fetchList()
}
async function update(id: T['id'], data: Partial<T>) {
await updateFn(id, data)
await fetchList()
}
async function remove(id: T['id']) {
await deleteFn(id)
await fetchList()
}
return { list, total, loading, params, fetchList, create, update, remove }
}
// 在Store中使用
export const useProductStore = defineStore('product', () => {
const crud = useEntityCrud<Product>(
productApi.list, productApi.create,
productApi.update, productApi.delete
)
return { ...crud }
})
组合式函数封装业务逻辑
组合式函数是Vue3逻辑复用的核心机制。命名约定为use开头,内部使用响应式API,返回响应式数据和方法。与Mixin相比,组合式函数具有明确的类型推导和来源追踪。
// composables/usePagination.ts
import { ref, computed } from 'vue'
export function usePagination(options: {
total: () => number
pageSize?: number
onChange?: (page: number) => void
}) {
const currentPage = ref(1)
const pageSize = ref(options.pageSize ?? 20)
const totalPages = computed(() =>
Math.ceil(options.total() / pageSize.value)
)
function goTo(page: number) {
if (page < 1 || page > totalPages.value) return
currentPage.value = page
options.onChange?.(page)
}
function next() { goTo(currentPage.value + 1) }
function prev() { goTo(currentPage.value - 1) }
function reset() { goTo(1) }
return { currentPage, pageSize, totalPages, goTo, next, prev, reset }
}
组件中使用:
<script setup lang="ts">
import { useProductStore } from '@/stores/product'
import { usePagination } from '@/composables/usePagination'
const store = useProductStore()
store.fetchList()
const pagination = usePagination({
total: () => store.total,
onChange: (page) => {
store.params.page = page
store.fetchList()
}
})
</script>
<template>
<div v-for="item in store.list" :key="item.id">{{ item.name }}</div>
<button @click="pagination.prev()" :disabled="pagination.currentPage === 1">上一页</button>
{{ pagination.currentPage }} / {{ pagination.totalPages }}
<button @click="pagination.next()" :disabled="pagination.currentPage === pagination.totalPages">下一页</button>
</template>
Pinia持久化与跨组件状态同步
使用pinia-plugin-persistedstate插件实现状态持久化:
// main.ts
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
// Store中声明持久化配置
export const useUserStore = defineStore('user', () => {
const token = ref('')
const userInfo = ref(null)
return { token, userInfo }
}, {
persist: {
key: 'app-user',
storage: localStorage,
pick: ['token'],
}
})
跨组件状态同步利用Pinia的响应式特性。多个组件引用同一个Store实例时,任一组件修改状态,依赖该状态的组件自动更新:
// Header.vue
<script setup>
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
</script>
<template>
<span>{{ userStore.displayName }}</span>
<button @click="userStore.logout()">退出</button>
</template>
// Sidebar.vue — 根据权限渲染菜单
<script setup>
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
const menus = [
{ label: '仪表盘', perm: 'dashboard:view' },
{ label: '用户管理', perm: 'user:manage' },
]
const visibleMenus = menus.filter(m => userStore.hasPermission(m.perm))
</script>
TypeScript类型推导与Store测试
Pinia的Setup Store天然支持TypeScript类型推导。测试时使用setActivePinia创建独立的Pinia实例:
// stores/__tests__/user.test.ts
import { setActivePinia, createPinia } from 'pinia'
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { useUserStore } from '../user'
import { userApi } from '@/api/user'
vi.mock('@/api/user')
describe('useUserStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('登录后应设置token和用户信息', async () => {
vi.mocked(userApi.login).mockResolvedValue({
token: 'test-token',
user: { id: 1, username: 'admin', nickname: '管理员' },
permissions: ['dashboard:view', 'user:manage']
})
const store = useUserStore()
await store.login({ username: 'admin', password: '123456' })
expect(store.token).toBe('test-token')
expect(store.isLoggedIn).toBe(true)
expect(store.displayName).toBe('管理员')
expect(store.hasPermission('user:manage')).toBe(true)
})
it('退出后应清空所有状态', () => {
const store = useUserStore()
store.token = 'some-token'
store.logout()
expect(store.token).toBe('')
expect(store.isLoggedIn).toBe(false)
})
})
组合式API配合Pinia的工程价值在于:逻辑以函数为单元拆分和复用,状态以Store为边界管理和持久化,类型在编译期完整推导,测试通过独立Pinia实例隔离副作用。这套模式在中大型Vue3项目中能显著降低组件复杂度,提升可维护性。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-shi-zhan-pinia-zhuang-tai-guan-li-yu-zu/