Vue3生态中Pinia取代Vuex成为官方推荐的状态管理方案。Pinia移除了Mutations概念,直接在State中修改状态,API更简洁。Tree-shaking支持使打包体积更小,TypeScript类型推导也更完善。对于中大型Vue3项目,合理的Store模块化设计和持久化策略直接影响应用可维护性。
Pinia基础Store定义与组合式API写法
Pinia支持Options API和Composition API两种写法。组合式API写法更灵活,推荐用于Vue3项目:
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
// State
const token = ref('')
const userInfo = ref(null)
const permissions = ref([])
// Getters
const isLoggedIn = computed(() => !!token.value)
const hasPermission = computed(() => (perm) => permissions.value.includes(perm))
// Actions
async function login(credentials) {
const res = await api.login(credentials)
token.value = res.data.token
userInfo.value = res.data.user
permissions.value = res.data.permissions
return res
}
function logout() {
token.value = ''
userInfo.value = null
permissions.value = []
router.push('/login')
}
return { token, userInfo, permissions, isLoggedIn, hasPermission, login, logout }
})
与Vuex不同,Pinia的Action中可以直接修改State,不需要commit mutation。异步操作在函数内直接使用async/await即可。
Store模块化拆分与跨Store调用
大型项目按功能域拆分Store,每个Store职责单一。Pinia天然模块化,每个defineStore即独立模块,无需Vuex的modules嵌套:
stores/
├── user.ts # 用户认证
├── cart.ts # 购物车
├── product.ts # 商品
├── theme.ts # 主题切换
└── index.ts # 统一导出
Store之间可直接互相调用,通过useXxxStore()在Action中获取其他Store实例:
import { useUserStore } from './user'
export const useCartStore = defineStore('cart', () => {
const items = ref([])
const total = computed(() => items.value.reduce((sum, i) => sum + i.price * i.qty, 0))
async function addToCart(product) {
const userStore = useUserStore()
if (!userStore.isLoggedIn) {
throw new Error('请先登录')
}
const existing = items.value.find(i => i.id === product.id)
if (existing) {
existing.qty++
} else {
items.value.push({ ...product, qty: 1 })
}
}
async function checkout() {
const userStore = useUserStore()
const res = await api.createOrder({
userId: userStore.userInfo.id,
items: items.value
})
items.value = []
return res
}
return { items, total, addToCart, checkout }
})
组件中使用Store与响应式解构
在组件中通过storeToRefs解构State和Getters以保持响应性,Actions可直接解构:
<script setup>
import { useUserStore } from '@/stores/user'
import { useCartStore } from '@/stores/cart'
import { storeToRefs } from 'pinia'
const userStore = useUserStore()
const cartStore = useCartStore()
// 解构State和Getters需要storeToRefs保持响应式
const { isLoggedIn, userInfo } = storeToRefs(userStore)
const { total } = storeToRefs(cartStore)
// Actions可以直接解构
const { login, logout } = userStore
const { addToCart } = cartStore
</script>
直接使用const { token } = userStore会丢失响应性,因为Pinia的State本质是Vue的reactive对象,解构后得到的是普通值。storeToRefs内部使用toRefs包装每个属性。
持久化插件pinia-plugin-persistedstate配置
默认情况下Pinia状态存储在内存中,页面刷新后丢失。持久化插件将指定Store的状态写入localStorage或sessionStorage:
// main.ts
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
app.use(pinia)
在Store中通过persist选项配置持久化策略:
export const useUserStore = defineStore('user', () => {
const token = ref('')
const userInfo = ref(null)
const permissions = ref([])
return { token, userInfo, permissions }
}, {
persist: {
key: 'app-user',
storage: localStorage,
paths: ['token', 'userInfo'], // 仅持久化token和userInfo
}
})
paths指定需要持久化的字段,不设置则持久化整个State。storage可替换为sessionStorage或自定义存储(如IndexedDB)。
对于需要加密的敏感数据,可自定义序列化/反序列化函数:
persist: {
key: 'app-user',
storage: localStorage,
serializer: {
serialize: (state) => CryptoJS.AES.encrypt(JSON.stringify(state), SECRET_KEY).toString(),
deserialize: (encrypted) => JSON.parse(CryptoJS.AES.decrypt(encrypted, SECRET_KEY).toString(CryptoJS.enc.Utf8))
}
}
SSR场景下的持久化处理
Nuxt3等SSR框架中,localStorage在服务端不可用。需要在插件中做环境判断,或使用cookie-based的持久化方案。Nuxt3中可通过useCookie实现跨端状态同步:
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@pinia/nuxt'],
})
// stores/auth.ts
export const useAuthStore = defineStore('auth', () => {
const token = useCookie('auth-token', {
maxAge: 60 * 60 * 24 * 7,
sameSite: 'strict',
secure: true
})
const userInfo = ref(null)
const isLoggedIn = computed(() => !!token.value)
async function login(credentials) {
const res = await $fetch('/api/auth/login', { method: 'POST', body: credentials })
token.value = res.token
userInfo.value = res.user
}
return { token, userInfo, isLoggedIn, login }
})
useCookie在服务端渲染时读取请求中的Cookie,在客户端 hydration 后读写浏览器Cookie,自动实现跨端同步。
Store重置与测试技巧
Pinia提供$reset方法将State恢复到初始值。组合式API写法需要手动实现:
export const useUserStore = defineStore('user', () => {
const token = ref('')
const userInfo = ref(null)
function $reset() {
token.value = ''
userInfo.value = null
}
return { token, userInfo, $reset }
})
单元测试中使用setActivePinia(createPinia())为每个测试用例创建独立的Pinia实例,避免状态污染:
import { setActivePinia, createPinia } from 'pinia'
import { useUserStore } from '@/stores/user'
beforeEach(() => {
setActivePinia(createPinia())
})
test('login updates state', async () => {
const store = useUserStore()
await store.login({ username: 'test', password: '123' })
expect(store.isLoggedIn).toBe(true)
expect(store.token).toBeTruthy()
})
test('logout clears state', () => {
const store = useUserStore()
store.token = 'fake-token'
store.logout()
expect(store.token).toBe('')
expect(store.isLoggedIn).toBe(false)
})
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3pinia-zhuang-tai-guan-li-shi-zhan-store-mo-kuai-hua-she/