Pinia状态管理从入门到实战踩坑全记录

闲人鑫哲 框架 阅读 2,661
赞 18 收藏
二维码
手机扫码查看
反馈

我的写法,亲测靠谱

用Pinia也有一年多了,从Vue2迁移到Vue3的时候顺便把Vuex换成了Pinia。说实话,官方文档写得确实比Vuex清爽很多,但实际项目中还是有些坑需要填。

Pinia状态管理从入门到实战踩坑全记录

先说说我的store结构,我一直坚持这种写法:

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

export const useUserStore = defineStore('user', {
  // 状态
  state: () => ({
    userInfo: null,
    token: '',
    permissions: []
  }),

  // 计算属性  
  getters: {
    isLoggedIn: (state) => !!state.token,
    hasAdminPermission: (state) => state.permissions.includes('admin')
  },

  // 方法
  actions: {
    async login(credentials) {
      try {
        const response = await fetch('https://jztheme.com/api/login', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(credentials)
        })
        
        const data = await response.json()
        this.token = data.token
        this.userInfo = data.user
        this.permissions = data.permissions
      } catch (error) {
        console.error('Login failed:', error)
        throw error
      }
    },

    logout() {
      this.$reset()
    },

    updateUserInfo(newInfo) {
      this.userInfo = { ...this.userInfo, ...newInfo }
    }
  }
})

这里有几个关键点要注意。首先,我在actions里直接修改state,不用commit什么mutation,这个比Vuex方便太多了。其次,$reset()这个方法很实用,一键清空所有状态,特别适合登出场景。

组件里这样使用:

<template>
  <div>
    <p v-if="userStore.isLoggedIn">你好,{{ userStore.userInfo.name }}</p>
    <button @click="handleLogout" v-if="userStore.isLoggedIn">登出</button>
  </div>
</template>

<script setup>
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'

const userStore = useUserStore()

// 解构的时候记得用storeToRefs,不然会失去响应性
const { userInfo, isLoggedIn } = storeToRefs(userStore)

const handleLogout = () => {
  userStore.logout()
}
</script>

这个storeToRefs真的很关键!我刚开始没用这个,解构赋值后数据更新了但页面没变化,折腾了半天才发现这个问题。

这几种错误写法,别再踩坑了

最常见的错误就是这种:

// ❌ 错误写法
const { userInfo } = useUserStore() // 直接解构,会失去响应性

// ❌ 更离谱的错误
const userStore = useUserStore()
const { userInfo } = userStore // 这样也不行

还有人喜欢这样写action:

// ❌ 错误的异步写法
actions: {
  async fetchUserData() {
    const data = await api.getUserData()
    return data // 这样返回值在模板里不好获取
  }
}

正确的应该是:

// ✅ 正确写法
actions: {
  async fetchUserData() {
    try {
      this.loading = true
      const data = await api.getUserData()
      this.userData = data
      return data // 需要的话也可以返回
    } finally {
      this.loading = false
    }
  }
}

还有一种错误是嵌套太深的状态管理:

// ❌ 不推荐
state: () => ({
  profile: {
    personal: {
      basic: {
        name: '',
        age: 0
      },
      contact: {
        email: '',
        phone: ''
      }
    }
  }
})

这种深层嵌套的结构,调试起来要命,建议拆分成多个store或者扁平化设计。

实际项目中的坑

第一个坑是在SSR环境下。如果你的项目用了Nuxt或者自己搭的SSR,一定要注意store实例的隔离。我之前在服务端渲染时遇到状态串了的问题,解决办法是在插件中创建独立的store实例:

// plugins/pinia.js
import { createPinia } from 'pinia'

export default defineNuxtPlugin((nuxtApp) => {
  const pinia = createPinia()
  nuxtApp.vueApp.use(pinia)
  
  return {
    provide: {
      pinia
    }
  }
})

第二个坑是热重载。开发环境下修改store代码后页面经常卡住,这个问题困扰了我很久。后来发现是HMR导致的,解决方法是在main.js里加一段代码:

// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'

const app = createApp(App)
const pinia = createPinia()

app.use(pinia)

// 开发环境下的热重载修复
if (process.env.NODE_ENV === 'development') {
  if (window.__PINIA__) {
    window.__PINIA__ = pinia
  } else {
    window.__PINIA__ = pinia
  }
}

app.mount('#app')

第三个坑是持久化存储。很多人用pinia-plugin-persistedstate,但我发现这个插件有时候会出现序列化失败的问题,特别是store里有函数、Symbol等非JSON序列化类型的数据。我的建议是:

  • 只持久化简单的数据类型
  • 对于复杂对象,先处理成JSON兼容的格式
  • 设置合适的缓存key,避免不同用户的数据冲突
// 配置持久化
import persistedstate from 'pinia-plugin-persistedstate'

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

export default pinia

// 在store中使用
export const useUserStore = defineStore('user', {
  state: () => ({
    userInfo: null,
    token: '',
    lastVisitTime: null
  }),
  persist: {
    key: user-store-${import.meta.env.VITE_APP_NAME}, // 动态key避免冲突
    storage: localStorage,
    pick: ['token', 'userInfo'], // 只持久化指定字段
  }
})

最后一个坑是调试。生产环境下如果出了问题,console.log基本没用,建议集成错误监控:

// utils/errorHandler.js
export function handleStoreError(error, storeName, actionName) {
  console.error(Store Error in ${storeName}.${actionName}:, error)
  
  // 发送到错误监控平台
  if (import.meta.env.PROD) {
    // sentry.captureException(error)
  }
}

// 在store中使用
actions: {
  async riskyAction() {
    try {
      // 执行某些可能出错的操作
      await someAsyncOperation()
    } catch (error) {
      handleStoreError(error, 'user', 'riskyAction')
      throw error
    }
  }
}

这些坑我都踩过,有些还挺隐蔽的,希望能帮到大家。

性能优化小技巧

Pinia本身性能不错,但在大型应用中还是需要注意一些细节。比如getter的计算缓存问题:

// ❌ 不好的写法
getters: {
  filteredList() {
    return this.list.filter(item => item.status === this.currentStatus)
  }
}

// ✅ 更好的写法
getters: {
  filteredList: (state) => (status = state.currentStatus) => {
    return state.list.filter(item => item.status === status)
  }
}

这样可以避免不必要的重新计算。还有就是在组件中合理使用computed来缓存复杂的计算结果,减少getter的重复执行。

以上是我用Pinia一年多来的实战总结,涵盖了写法、常见错误和实际项目的注意事项。有更优的实现方式欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论