Vue3的Composition API带来了全新的状态管理范式。Pinia作为Vue3官方推荐的状态管理库,替代Vuex成为新项目首选。相比Vuex,Pinia去除了mutations,提供更好的TypeScript支持,且支持模块化按需引入。本文通过实际项目案例演示Pinia store的设计模式、组合式用法和性能优化策略。
Pinia与Vuex的核心差异
Pinia的架构设计有三个关键改进。第一,移除mutations,直接在actions中修改state,代码更简洁。第二,Store天然模块化,无需modules嵌套,每个store独立文件。第三,完美的TypeScript推导,无需复杂的类型声明即可获得完整的类型提示。
安装Pinia:
npm install pinia
# 或
pnpm add pinia
// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')
Setup Store写法:组合式API风格
Pinia支持两种Store定义风格:Options Store和Setup Store。Setup Store使用Composition API语法,灵活性更强,推荐在新项目中使用。
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { userApi } from '@/api/user'
export const useUserStore = defineStore('user', () => {
// State
const userInfo = ref<UserInfo | null>(null)
const token = ref<string>(localStorage.getItem('token') || '')
const permissions = ref<string[]>([])
// Getters
const isLoggedIn = computed(() => !!token.value)
const userName = computed(() => userInfo.value?.name || '未登录')
const hasPermission = computed(() => (perm: string) =>
permissions.value.includes(perm) || permissions.value.includes('*')
)
// Actions
async function login(credentials: LoginParams) {
const res = await userApi.login(credentials)
token.value = res.token
localStorage.setItem('token', res.token)
await fetchUserInfo()
return res
}
async function fetchUserInfo() {
if (!token.value) return
const res = await userApi.getInfo()
userInfo.value = res.user
permissions.value = res.permissions
}
function logout() {
userInfo.value = null
token.value = ''
permissions.value = []
localStorage.removeItem('token')
}
return {
userInfo, token, permissions,
isLoggedIn, userName, hasPermission,
login, fetchUserInfo, logout
}
})
跨Store依赖与模块化组合
实际项目中多个Store之间常有依赖关系。Pinia允许在Store内部直接调用其他Store,无需复杂的模块注册。
// stores/cart.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useUserStore } from './user'
import { productApi } from '@/api/product'
export const useCartStore = defineStore('cart', () => {
const userStore = useUserStore()
const items = ref<CartItem[]>([])
const loading = ref(false)
const total = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
const itemCount = computed(() =>
items.value.reduce((sum, item) => sum + item.quantity, 0)
)
async function addToCart(productId: number, quantity: number = 1) {
if (!userStore.isLoggedIn) {
throw new Error('请先登录')
}
loading.value = true
try {
const product = await productApi.getById(productId)
const existing = items.value.find(i => i.productId === productId)
if (existing) {
existing.quantity += quantity
} else {
items.value.push({
productId: product.id,
name: product.name,
price: product.price,
quantity,
thumb: product.thumb
})
}
} finally {
loading.value = false
}
}
function removeFromCart(productId: number) {
items.value = items.value.filter(i => i.productId !== productId)
}
return { items, loading, total, itemCount, addToCart, removeFromCart }
})
组件中使用Store的多重方式
Pinia在组件中的使用方式灵活,根据场景选择合适的模式。不同写法影响响应性追踪和代码可读性。
// 组件中使用
import { useUserStore } from '@/stores/user'
import { useCartStore } from '@/stores/cart'
import { storeToRefs } from 'pinia'
const userStore = useUserStore()
const cartStore = useCartStore()
// 方式1: 直接解构state会丢失响应性(错误示范)
// const { userInfo, token } = userStore // 不推荐
// 方式2: 使用storeToRefs保持响应性(推荐)
const { userInfo, isLoggedIn, userName } = storeToRefs(userStore)
const { items, total, itemCount } = storeToRefs(cartStore)
// Actions可以直接解构,不需要storeToRefs
const { login, logout } = userStore
const { addToCart, removeFromCart } = cartStore
持久化插件:状态不丢失方案
默认Pinia状态仅存于内存,页面刷新即丢失。pinia-plugin-persistedstate插件实现自动持久化,支持localStorage、sessionStorage和自定义存储。
// 安装: npm install pinia-plugin-persistedstate
// main.ts
import { createPinia } from 'pinia'
import piniaPersist from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPersist)
// Store中配置持久化
export const useUserStore = defineStore('user', () => {
// ...store内容
}, {
persist: {
key: 'app-user',
storage: localStorage,
pick: ['token', 'userInfo']
}
})
// 多持久化目标
export const useAppStore = defineStore('app', () => {
const theme = ref('light')
const draftContent = ref('')
const sidebarCollapsed = ref(false)
}, {
persist: [
{
key: 'app-settings',
storage: localStorage,
pick: ['theme', 'sidebarCollapsed']
},
{
key: 'app-draft',
storage: sessionStorage,
pick: ['draftContent']
}
]
})
响应式性能优化:避免不必要的重新渲染
Pinia状态变化会触发所有依赖该状态的组件更新。大型应用中不当使用Store会导致性能问题。优化核心是缩小响应式追踪范围。
// 反面案例:整个list都响应式,任何修改触发全量更新
const list = ref<Product[]>([])
function updateOne(id: number, data: Partial<Product>) {
list.value = list.value.map(item =>
item.id === id ? { ...item, ...data } : item
)
}
// 优化方案1: shallowRef + 手动触发
import { shallowRef, triggerRef } from 'vue'
const list = shallowRef<Product[]>([])
function updateOne(id: number, data: Partial<Product>) {
const item = list.value.find(i => i.id === id)
if (item) {
Object.assign(item, data)
triggerRef(list)
}
}
// 优化方案2: 细粒度拆分Store,按功能模块独立
// 将大的Store拆分为productList、productDetail等独立Store
// 组件层面优化:只订阅需要的字段
const expensiveList = computed(() =>
list.value.filter(p => p.status === 'active')
)
针对大型列表场景,配合前端工程化最佳实践:使用shallowRef处理大数据集;虚拟滚动渲染大量列表项;分页加载减少单次数据量。组件库设计时,将Store的订阅粒度尽量细化,每个组件只订阅自己需要的切片数据。
Pinia的优势在于简洁的API和优秀的TypeScript实战体验。配合合理的Store拆分策略和响应式优化,能支撑大型Vue3应用的复杂状态管理需求。关键原则是保持Store职责单一、按功能域拆分、避免全局Store膨胀。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3compositionapi-zhuang-tai-guan-li-shi-zhan-piniastore/