Vue3 Pinia状态管理实战:Store模块化设计与持久化插件配置

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/

(0)
小编小编
上一篇 11小时前
下一篇 11小时前

相关推荐