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

Vue3生态中Pinia已全面取代Vuex成为官方推荐的状态管理方案。相比Vuex,Pinia移除了mutation概念,API设计更贴合Composition API风格,TypeScript类型推导也更完善。前端开发中状态管理的核心需求包括跨组件数据共享、异步数据流处理、状态持久化和模块化拆分。Pinia通过简洁的defineStore API和插件机制覆盖了这些场景,bundle size仅约1.5KB(gzipped),对前端工程化项目的侵入性极低。

Pinia安装与Store定义

Pinia的安装和初始化只需三行代码。Store定义支持两种语法风格:Options Store(类似Vuex的state/getters/actions结构)和Setup Store(Composition API风格)。实际项目中推荐使用Setup Store,类型推导和代码组织更灵活。

// 安装
// npm install pinia @pinia/nuxt

// main.ts 初始化
import { createApp } from 'vue'
import { createPinia } from 'pinia'

const app = createApp(App)
app.use(createPinia())
app.mount('#app')

// Setup Store 定义
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 (computed)
  const isLoggedIn = computed(() => !!token.value)
  const isAdmin = computed(() => permissions.value.includes('admin'))
  const displayName = computed(() => {
    if (!userInfo.value) return '未登录'
    return userInfo.value.nickname || userInfo.value.username
  })
  
  // actions
  async function login(credentials: LoginParams) {
    const res = await authApi.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 = []
    router.push('/login')
  }
  
  function hasPermission(perm: string): boolean {
    return permissions.value.includes(perm)
  }
  
  return {
    token, userInfo, permissions,
    isLoggedIn, isAdmin, displayName,
    login, logout, hasPermission,
  }
})

Setup Store中,ref对应state,computed对应getters,普通函数对应actions。这种方式的好处是可以在Store内部自由使用其他Composition API(watch、watchEffect、provide/inject),而不受Options API的结构限制。Store的返回值既包含state也包含derived state和方法,组件中解构使用时所有响应性都自动保留。

Store模块化设计与组合式API

大型前端项目的状态管理需要按业务域拆分Store,Pinia的模块化天然支持——每个defineStore调用就是一个独立模块,模块间通过Store组合实现跨域状态依赖。无需Vuex的modules嵌套配置,各Store之间可直接引用。

// stores/cart.ts - 购物车Store
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useUserStore } from './user'

export const useCartStore = defineStore('cart', () => {
  const userStore = useUserStore()  // 跨Store引用
  
  const items = ref([])
  const couponCode = ref('')
  
  // 跨Store计算
  const totalPrice = computed(() => {
    const subtotal = items.value.reduce((sum, item) => {
      return sum + item.price * item.quantity
    }, 0)
    
    // 根据用户会员等级计算折扣
    const discount = userStore.isAdmin ? 0.85 : 1.0
    return subtotal * discount
  })
  
  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('请先登录')
    }
    const product = await productApi.getProduct(productId)
    const existing = items.value.find(i => i.productId === productId)
    if (existing) {
      existing.quantity += quantity
    } else {
      items.value.push({ productId, quantity, price: product.price, name: product.name })
    }
  }
  
  function removeFromCart(productId: number) {
    items.value = items.value.filter(i => i.productId !== productId)
  }
  
  return { items, couponCode, totalPrice, itemCount, addToCart, removeFromCart }
})

// stores/order.ts - 订单Store依赖购物车Store
import { defineStore } from 'pinia'
import { useCartStore } from './cart'
import { useUserStore } from './user'

export const useOrderStore = defineStore('order', () => {
  const cartStore = useCartStore()
  const userStore = useUserStore()
  
  const orders = ref([])
  const creating = ref(false)
  
  async function createOrder() {
    if (cartStore.items.length === 0) return
    
    creating.value = true
    try {
      const order = await orderApi.create({
        userId: userStore.userInfo!.id,
        items: cartStore.items,
        totalAmount: cartStore.totalPrice,
        couponCode: cartStore.couponCode,
      })
      orders.value.unshift(order)
      cartStore.items = []  // 清空购物车
      return order
    } finally {
      creating.value = false
    }
  }
  
  return { orders, creating, createOrder }
})

Store组合时需要注意循环依赖问题。如果Store A引用Store B,Store B又引用Store A,会导致初始化时死锁。解决方案是将共享逻辑提取到独立的composable函数中,两个Store都引用该composable而非直接互相引用。

持久化存储插件配置

Pinia的状态默认存储在内存中,页面刷新后丢失。对于需要持久化的状态(如用户token、主题偏好、购物车数据),使用pinia-plugin-persistedstate插件实现自动持久化,支持localStorage、sessionStorage和自定义存储后端。

// 安装
// npm install 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 theme = ref<'light' | 'dark'>('light')
  const sidebarCollapsed = ref(false)
  // ... 其他非持久化状态
  const tempFormData = ref({})
  
  return { token, theme, sidebarCollapsed, tempFormData }
}, {
  persist: {
    // 只持久化指定字段
    pick: ['token', 'theme', 'sidebarCollapsed'],
    // 或使用paths兼容旧版
    // paths: ['token', 'theme', 'sidebarCollapsed'],
    
    storage: localStorage,
    key: 'app-user-state',
  }
})

// 自定义存储后端(如加密存储)
import { encrypt, decrypt } from '@/utils/crypto'

const encryptedStorage = {
  getItem: (key: string) => {
    const raw = localStorage.getItem(key)
    return raw ? decrypt(raw) : null
  },
  setItem: (key: string, value: string) => {
    localStorage.setItem(key, encrypt(value))
  },
  removeItem: (key: string) => {
    localStorage.removeItem(key)
  }
}

// 使用自定义存储
export const useSettingsStore = defineStore('settings', () => {
  const apiKey = ref('')
  const apiEndpoint = ref('')
  return { apiKey, apiEndpoint }
}, {
  persist: {
    pick: ['apiKey', 'apiEndpoint'],
    storage: encryptedStorage,
  }
})

TypeScript类型安全与跨组件状态共享

Pinia的TypeScript集成是相比Vuex的显著优势。Setup Store中所有state和getters的类型自动推导,无需手动声明接口。组件中使用Store时,$state、$patch等API也有完整类型提示。

// 组件中使用Store
import { defineComponent } from 'vue'
import { useUserStore } from '@/stores/user'
import { useCartStore } from '@/stores/cart'
import { storeToRefs } from 'pinia'

export default defineComponent({
  setup() {
    const userStore = useUserStore()
    const cartStore = useCartStore()
    
    // storeToRefs 解构保持响应性
    // 直接解构 state/getters 会丢失响应性
    const { isLoggedIn, displayName, isAdmin } = storeToRefs(userStore)
    const { items, totalPrice, itemCount } = storeToRefs(cartStore)
    
    // actions 可以直接解构
    const { login, logout } = userStore
    const { addToCart, removeFromCart } = cartStore
    
    // $patch 批量更新state
    function updateUserProfile(data: Partial) {
      userStore.$patch((state) => {
        if (state.userInfo) {
          Object.assign(state.userInfo, data)
        }
      })
    }
    
    // $subscribe 监听state变化
    cartStore.$subscribe((mutation, state) => {
      console.log('cart changed:', mutation.type, state.items.length)
    }, { detached: true })  // detached: 组件卸载后仍监听
    
    // $onAction 监听action调用
    userStore.$onAction(({ name, after, onError }) => {
      console.log(`action ${name} started`)
      after((result) => {
        console.log(`action ${name} completed`, result)
      })
      onError((error) => {
        console.error(`action ${name} failed`, error)
      })
    })
    
    return {
      isLoggedIn, displayName, isAdmin,
      items, totalPrice, itemCount,
      login, logout, addToCart, removeFromCart,
      updateUserProfile,
    }
  }
})

$subscribe和$onAction为状态调试和副作用管理提供了细粒度控制。$subscribe的detached选项允许在组件销毁后继续监听Store变化,适用于全局数据同步场景(如WebSocket推送更新Store后触发其他模块响应)。$onAction可用于统一记录用户操作日志或实现action级别的乐观更新回滚。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3pinia-zhuang-tai-guan-li-shi-zhan-store-mo-kuai-hua-she/

赞 (0)
小编小编
上一篇 2026年8月25日
下一篇 2026年8月25日

相关推荐

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)
小编小编
上一篇 2026年8月24日
下一篇 2026年8月24日

相关推荐