Vue3状态管理为什么需要重新选型
Vue3组合式API的引入让状态管理发生了根本性变化。Options API时代,Vuex是唯一选项,但Composable函数天然具备状态封装能力,很多场景不再需要全局Store。前端开发中状态管理的核心问题是:什么时候用Composable函数就够了,什么时候必须引入Pinia?
判断标准很直接——状态是否需要跨组件树共享。如果只在组件及其子组件间传递,Composable + provide/inject即可;如果多个不相关组件需要读写同一份状态,Pinia是更合理的方案。
Composable函数封装局部状态
Composable函数是Vue3最优雅的状态封装方式,用ref和computed即可构建自包含的状态逻辑:
// composables/useFilter.ts
import { ref, computed } from 'vue'
export function useFilter<T>(items: Ref<T[]>, keyFn: (item: T) => string) {
const keyword = ref('')
const filtered = computed(() => {
if (!keyword.value) return items.value
return items.value.filter(item =>
keyFn(item).toLowerCase().includes(keyword.value.toLowerCase())
)
})
function setFilter(val: string) {
keyword.value = val
}
return { keyword, filtered, setFilter }
}
使用方式:
<script setup lang="ts">
import { useFilter } from '@/composables/useFilter'
const products = ref<Product[]>([...])
const { filtered, setFilter } = useFilter(products, item => item.name)
</script>
<template>
<input :value="keyword" @input="setFilter($event.target.value)" />
<ul>
<li v-for="item in filtered" :key="item.id">{{ item.name }}</li>
</ul>
</template>
Composable函数的状态生命周期与使用它的组件绑定,组件销毁时状态自动清理。这种模式适合搜索筛选、表单校验、分页等局部状态场景。
Pinia全局Store的架构设计
Pinia相比Vuex的优势:去掉了Mutations的冗余层,支持Composition API风格定义Store,天然支持TypeScript推导,支持多Store组合。
定义Store的标准模式:
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
// State
const token = ref('')
const userInfo = ref<UserInfo | null>(null)
const permissions = ref<string[]>([])
// Getters
const isLoggedIn = computed(() => !!token.value)
const hasPermission = computed(() => (perm: string) => permissions.value.includes(perm))
// Actions
async function login(credentials: LoginParams) {
const res = await authApi.login(credentials)
token.value = res.token
userInfo.value = res.user
permissions.value = res.permissions
localStorage.setItem('token', res.token)
}
function logout() {
token.value = ''
userInfo.value = null
permissions.value = []
localStorage.removeItem('token')
}
function loadFromStorage() {
const saved = localStorage.getItem('token')
if (saved) token.value = saved
}
return { token, userInfo, permissions, isLoggedIn, hasPermission, login, logout, loadFromStorage }
})
Store组合——一个Store引用另一个Store的数据:
// stores/cart.ts
import { defineStore } from 'pinia'
import { useUserStore } from './user'
export const useCartStore = defineStore('cart', () => {
const userStore = useUserStore()
const items = ref<CartItem[]>([])
const total = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
async function addItem(product: Product) {
if (!userStore.isLoggedIn) {
throw new Error('请先登录')
}
// 业务逻辑...
}
return { items, total, addItem }
})
Pinia插件扩展:持久化与重置策略
Pinia插件机制支持全局状态持久化、重置策略、DevTools集成等横切关注点:
// plugins/persisted.ts
import { PiniaPluginContext } from 'pinia'
export function piniaPersistPlugin({ store }: PiniaPluginContext) {
const storageKey = `pinia-${store.$id}`
// 从localStorage恢复状态
const saved = localStorage.getItem(storageKey)
if (saved) {
store.$patch(JSON.parse(saved))
}
// 监听状态变化并持久化
store.$subscribe((mutation, state) => {
localStorage.setItem(storageKey, JSON.stringify(state))
})
}
// 注册插件
const pinia = createPinia()
pinia.use(piniaPersistPlugin)
生产级持久化需要考虑更多细节——哪些字段需要持久化、哪些需要排除(token过期后不能复用)、序列化失败的处理:
// 生产级持久化插件
export function piniaPersistPlugin({ store }: PiniaPluginContext) {
const options = store.$id
const storageKey = `app-${store.$id}`
// 可在Store定义中声明需要持久化的字段
// const useUserStore = defineStore('user', () => { ... }, {
// persist: { paths: ['token', 'userInfo'], exclude: ['permissions'] }
// })
const persistPaths = (store.$id as any).persist?.paths
const saved = localStorage.getItem(storageKey)
if (saved) {
try {
const data = JSON.parse(saved)
store.$patch(persistPaths
? persistPaths.reduce((acc, key) => { acc[key] = data[key]; return acc }, {} as any)
: data
)
} catch (e) {
localStorage.removeItem(storageKey)
}
}
store.$subscribe(() => {
try {
const state = store.$state
const data = persistPaths
? persistPaths.reduce((acc, key) => { acc[key] = state[key]; return acc }, {} as any)
: state
localStorage.setItem(storageKey, JSON.stringify(data))
} catch (e) {
console.error(`Failed to persist store ${store.$id}`, e)
}
})
}
Composable与Pinia的混合架构模式
实际项目中两种模式往往混合使用。架构分层原则:
– 视图层(组件内):Composable管理UI状态(表单、弹窗、筛选条件)
– 业务层(Store):Pinia管理领域状态(用户信息、购物车、权限)
– 基础设施层:Composable封装三方SDK(地图、支付、IM)
// composables/useMap.ts - 封装第三方地图SDK
export function useMap(container: Ref<HTMLElement | null>) {
const mapInstance = ref<any>(null)
const isReady = ref(false)
onMounted(() => {
mapInstance.value = new AMap.Map(container.value, {
zoom: 12,
center: [116.397, 39.908]
})
mapInstance.value.on('complete', () => {
isReady.value = true
})
})
onUnmounted(() => {
mapInstance.value?.destroy()
})
function addMarker(lng: number, lat: number) {
if (!isReady.value) return
new AMap.Marker({ position: [lng, lat], map: mapInstance.value })
}
return { isReady, addMarker }
}
组件中的使用层次清晰:
<script setup lang="ts">
import { useUserStore } from '@/stores/user'
import { useCartStore } from '@/stores/cart'
import { useFilter } from '@/composables/useFilter'
import { useMap } from '@/composables/useMap'
// 全局Store
const userStore = useUserStore()
const cartStore = useCartStore()
// 局部Composable
const mapRef = ref<HTMLElement | null>(null)
const { isReady, addMarker } = useMap(mapRef)
const { filtered, setFilter } = useFilter(
computed(() => cartStore.items),
item => item.name
)
</script>
响应式性能优化:shallowRef与triggerRef
Pinia Store中的大型对象(如列表数据、嵌套JSON),Vue3默认深度响应式代理会带来可观的开销。当对象层级深、数据量大时,用shallowRef + triggerRef手动控制响应式触发时机:
// stores/products.ts
import { shallowRef, triggerRef } from 'vue'
export const useProductStore = defineStore('products', () => {
// 大列表用shallowRef避免深度代理
const products = shallowRef<Product[]>([])
async function fetchProducts() {
const data = await productApi.list()
products.value = data
// shallowRef需要手动触发更新
triggerRef(products)
}
function updateProduct(id: number, patch: Partial<Product>) {
const idx = products.value.findIndex(p => p.id === id)
if (idx !== -1) {
// 创建新数组引用触发更新
const newList = [...products.value]
newList[idx] = { ...newList[idx], ...patch }
products.value = newList
}
}
return { products, fetchProducts, updateProduct }
})
性能对比:在10000条商品数据的场景下,shallowRef比ref的初始化速度快6倍以上,更新单条数据时差异更显著——ref需要遍历整个数组做深度代理,shallowRef只替换引用。
Pinia Store的单元测试策略
Pinia Store的测试比Vuex简单很多,直接创建Store实例测试即可:
// stores/__tests__/user.test.ts
import { createPinia, setActivePinia } from 'pinia'
import { useUserStore } from '../user'
import { describe, it, expect, beforeEach, vi } from 'vitest'
describe('useUserStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('初始状态未登录', () => {
const store = useUserStore()
expect(store.isLoggedIn).toBe(false)
expect(store.token).toBe('')
})
it('登录后状态正确', async () => {
const store = useUserStore()
// Mock API
vi.spyOn(authApi, 'login').mockResolvedValue({
token: 'test-token',
user: { id: 1, name: 'test' },
permissions: ['read']
})
await store.login({ username: 'test', password: '123' })
expect(store.token).toBe('test-token')
expect(store.isLoggedIn).toBe(true)
expect(store.hasPermission('read')).toBe(true)
})
it('登出后清除状态', () => {
const store = useUserStore()
store.token = 'test-token'
store.logout()
expect(store.token).toBe('')
expect(store.isLoggedIn).toBe(false)
})
})
Composable函数的测试用@vue/test-utils的mount或直接在setup上下文中测试:
// composables/__tests__/useFilter.test.ts
import { ref } from 'vue'
import { useFilter } from '../useFilter'
import { describe, it, expect } from 'vitest'
describe('useFilter', () => {
it('按关键字过滤列表', () => {
const items = ref([
{ id: 1, name: 'Apple' },
{ id: 2, name: 'Banana' },
{ id: 3, name: 'Cherry' }
])
const { filtered, setFilter } = useFilter(items, item => item.name)
setFilter('ap')
expect(filtered.value).toHaveLength(1)
expect(filtered.value[0].name).toBe('Apple')
})
})
以上方案从Composable局部状态、Pinia全局Store、插件扩展、混合架构到性能优化和测试,覆盖了Vue3组合式API下状态管理的完整架构选型与实践路径。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-zhuang-tai-guan-li-shi-zhan-pinia-yu-2/