Vue3组合式API状态管理:Pinia架构设计与持久化缓存实战

Pinia取代Vuex的核心优势

Vue3项目里Pinia已经成为状态管理的事实标准。与Vuex相比,Pinia去掉了mutation这个冗余层,store定义直接用函数返回值,TypeScript类型推导零配置,模块化天然支持tree-shaking。一个中型项目从Vuex迁移到Pinia,打包体积减少约40KB,代码量减少30%。这篇从架构设计到持久化缓存,覆盖Pinia生产级用法。

Store架构设计:按业务域拆分

不要把所有状态塞进一个store。按业务域拆分,每个store只管一个领域的状态和逻辑:

// stores/user.ts
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', () => {
  const token = ref('')
  const userInfo = reactive({
    id: 0,
    name: '',
    role: '',
    permissions: [] as string[]
  })

  const isLoggedIn = computed(() => !!token.value)
  const hasPermission = (perm: string) => userInfo.permissions.includes(perm)

  async function login(credentials: LoginForm) {
    const res = await authApi.login(credentials)
    token.value = res.token
    Object.assign(userInfo, res.userInfo)
  }

  function logout() {
    token.value = ''
    Object.assign(userInfo, { id: 0, name: '', role: '', permissions: [] })
  }

  return { token, userInfo, isLoggedIn, hasPermission, login, logout }
})

组合式API风格(Setup Store)比选项式风格更灵活,可以直接组合composable函数:

// stores/cart.ts - 购物车store引用用户store
export const useCartStore = defineStore('cart', () => {
  const userStore = useUserStore()
  const items = ref<CartItem[]>([])

  const totalPrice = computed(() =>
    items.value.reduce((sum, item) => sum + item.price * item.count, 0)
  )

  // 跨store联动:未登录时只能浏览不能下单
  function canCheckout() {
    return userStore.isLoggedIn && items.value.length > 0
  }

  return { items, totalPrice, canCheckout }
})

异步Action与错误处理模式

Pinia的action支持async/await,配合请求拦截做统一错误处理:

// stores/product.ts
export const useProductStore = defineStore('product', () => {
  const products = ref<Product[]>([])
  const loading = ref(false)
  const error = ref<string | null>(null)

  async function fetchProducts(categoryId: number) {
    loading.value = true
    error.value = null
    try {
      const res = await productApi.getList({ categoryId })
      products.value = res.data
    } catch (e: any) {
      error.value = e.message || '商品加载失败'
      // 上报到Sentry
      captureException(e)
    } finally {
      loading.value = false
    }
  }

  return { products, loading, error, fetchProducts }
})

组件里使用:

const productStore = useProductStore()
const { products, loading, error } = storeToRefs(productStore)

onMounted(() => productStore.fetchProducts(props.categoryId))

storeToRefs解构保持响应式。直接解构store对象会丢失响应式——这是新手最常踩的坑。

Pinia持久化:pinia-plugin-persistedstate配置

用户token、偏好设置等状态需要跨页面刷新持久化。用pinia-plugin-persistedstate插件:

// main.ts
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

app.use(pinia)

在store里声明持久化策略:

// stores/user.ts
export const useUserStore = defineStore('user', () => {
  const token = ref('')
  const theme = ref<'light' | 'dark'>('light')
  const sidebarCollapsed = ref(false)

  return { token, theme, sidebarCollapsed }
}, {
  persist: {
    // 只持久化部分状态
    pick: ['token', 'theme', 'sidebarCollapsed'],
    // 自定义存储引擎(默认localStorage)
    storage: localStorage,
    // key前缀避免冲突
    key: 'app-user'
  }
})

如果token是JWT这类敏感数据,用sessionStorage代替localStorage,标签页关闭自动清除:

persist: {
  pick: ['token'],
  storage: sessionStorage
}

自定义持久化引擎:接入IndexedDB

大数据量状态(如离线商品目录)存localStorage有5MB限制,需要接入IndexedDB:

// utils/idb-storage.ts
import { openDB } from 'idb'

const idbStorage = {
  async getItem(key: string) {
    const db = await openDB('pinia-store', 1, {
      upgrade(db) {
        db.createObjectStore('persist')
      }
    })
    return db.get('persist', key)
  },
  async setItem(key: string, value: any) {
    const db = await openDB('pinia-store', 1)
    await db.put('persist', value, key)
  },
  async removeItem(key: string) {
    const db = await openDB('pinia-store', 1)
    await db.delete('persist', key)
  }
}

// 使用
persist: {
  storage: idbStorage,
  pick: ['offlineProducts']
}

Store热更新与插件调试

开发环境启用Pinia Devtools和热更新:

// vite.config.ts
export default defineConfig({
  plugins: [
    vue(),
    // Pinia热更新在Vite下自动支持
  ]
})

// 如果store定义在独立文件,确保HMR生效
if (import.meta.hot) {
  import.meta.hot.accept(acceptHMRUpdate(useUserStore, import.meta.hot))
}

Pinia Devtools面板可以直接查看store状态树、修改值、追踪action调用时间线。调试异步数据流时比console.log高效很多。

常见陷阱与解决方案

SSRhydration不匹配:服务端渲染时持久化数据导致首屏与服务端不一致。解决方案是在onMounted后再读取持久化数据,或使用ClientOnly组件包裹依赖持久化状态的部分。

循环引用导致内存泄漏:store之间互相引用形成循环时,组件卸载后store不释放。用computed做延迟计算而不是在setup里立即读取另一个store的状态。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-zhuang-tai-guan-li-pinia-jia-gou-she-ji/

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

相关推荐