Vue3 状态管理方案:Pinia 全指南

📦 Vue3 状态管理方案:Pinia 全指南

Pinia 是 Vue 官方推荐的下一代状态管理库,完全替代 Vuex,支持 Vue3 Composition API、TypeScript 友好、轻量灵活。本文从基础使用、核心 API、高级技巧、注意事项、常见坑五个维度全面讲解。


一、快速上手:安装与基础使用

1. 安装方式

(1)Vite/Vue CLI 项目
bash 复制代码
# npm
npm install pinia
# yarn
yarn add pinia
# pnpm
pnpm add pinia
(2)CDN 引入
html 复制代码
<script src="https://unpkg.com/pinia@2.1.7/dist/pinia.iife.js"></script>
<script>
  const { createPinia } = Pinia
</script>

2. 初始化 Pinia

main.ts 中注册 Pinia:

typescript 复制代码
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
const pinia = createPinia()
app.use(pinia) // 注册 Pinia
app.mount('#app')

3. 创建第一个 Store

Pinia 中没有 modules,每个 Store 都是独立的,通过 defineStore 定义:

(1)选项式写法(类似 Vuex)
typescript 复制代码
// src/stores/counter.ts
import { defineStore } from 'pinia'

// 第一个参数是 Store 的唯一 ID(必须全局唯一)
export const useCounterStore = defineStore('counter', {
  // 状态:返回初始状态的函数
  state: () => ({
    count: 0,
    name: 'Pinia'
  }),
  // 计算属性:类似 Vue 的 computed,自动缓存
  getters: {
    doubleCount: (state) => state.count * 2,
    // 访问其他 getters 用 this(需指定返回值类型)
    doubleCountPlusOne(): number {
      return this.doubleCount + 1
    }
  },
  // 动作:支持同步/异步,修改状态的唯一推荐方式
  actions: {
    increment() {
      this.count++
    },
    async fetchData() {
      const res = await fetch('/api/data')
      const data = await res.json()
      this.name = data.name
    }
  }
})
(2)组合式写法(Vue3 Composition API)

更灵活,适合复杂场景:

typescript 复制代码
// src/stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useUserStore = defineStore('user', () => {
  // 状态:用 ref/reactive 定义
  const userInfo = ref({ name: '', age: 0 })
  const token = ref('')

  // 计算属性:用 computed 替代 getters
  const isAdult = computed(() => userInfo.value.age >= 18)

  // 动作:普通函数,支持同步/异步
  const updateUser = (info: { name: string; age: number }) => {
    userInfo.value = info
  }
  const login = async (username: string, password: string) => {
    const res = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify({ username, password })
    })
    const data = await res.json()
    token.value = data.token
  }

  // 返回需要暴露的属性和方法
  return { userInfo, token, isAdult, updateUser, login }
})

二、核心 API 详解

1. Store 实例属性

API 作用 示例
$state 获取/替换整个状态对象 store.$state = { count: 10 }
$patch 批量修改状态(推荐) store.$patch({ count: store.count + 1 }) 或函数式:store.$patch(state => state.count++)
$reset 重置状态到初始值(仅选项式写法支持,组合式需手动实现) store.$reset()
$subscribe 监听状态变化 store.$subscribe((mutation, state) => console.log('状态变化', mutation))
$onAction 监听 actions 调用 store.$onAction(({ name, args, after, onError }) => {})

2. Getters 特性

  • 缓存机制:只有依赖的状态变化时才重新计算;

  • 访问其他 Store :在 getters 中可以调用其他 Store 实例;

    typescript 复制代码
    import { useCounterStore } from './counter'
    getters: {
      crossStoreCount(state) {
        const counterStore = useCounterStore()
        return state.count + counterStore.count
      }
    }
  • 传递参数 :getters 返回函数实现参数传递(此时不会缓存结果);

    typescript 复制代码
    getters: {
      getCountByMultiplier: (state) => (multiplier: number) => state.count * multiplier
    }
    // 组件中调用:store.getCountByMultiplier(3)

3. Actions 特性

  • 支持异步 :直接在 actions 中使用 async/await
  • 修改状态 :可直接修改 this.count 或用 $patch
  • 访问其他 Store:同 getters,直接在 actions 中调用其他 Store;
  • 错误处理 :可通过 try/catch$onAction 监听错误。

三、组件中使用 Store

1. Setup 语法糖(推荐)

vue 复制代码
<template>
  <div>{{ count }}</div>
  <button @click="increment">+1</button>
</template>

<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'

// 获取 Store 实例
const counterStore = useCounterStore()

// 直接访问状态/动作
const increment = () => counterStore.increment()

// 解构状态时必须用 storeToRefs 保持响应式!
const { count, doubleCount } = storeToRefs(counterStore)
</script>

2. 选项式 API 兼容

适合老项目迁移:

vue 复制代码
<script lang="ts">
import { useCounterStore } from '@/stores/counter'
import { mapState, mapActions, mapGetters } from 'pinia'

export default {
  computed: {
    ...mapState(useCounterStore, ['count', 'name']),
    ...mapGetters(useCounterStore, ['doubleCount'])
  },
  methods: {
    ...mapActions(useCounterStore, ['increment'])
  },
  mounted() {
    this.increment()
  }
}
</script>

四、高级技巧

1. Store 持久化(插件)

使用 pinia-plugin-persistedstate 实现状态持久化到 localStorage/sessionStorage:

安装
bash 复制代码
npm install pinia-plugin-persistedstate
配置
typescript 复制代码
// main.ts
import { createPinia } from 'pinia'
import persistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(persistedstate)
在 Store 中启用
typescript 复制代码
// 选项式写法
export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  persist: true // 全局默认配置(localStorage)
  // 自定义配置
  // persist: {
  //   key: 'counter-store',
  //   storage: sessionStorage,
  //   paths: ['count'] // 仅持久化count字段
  // }
})

// 组合式写法
export const useUserStore = defineStore('user', () => {
  const userInfo = ref({})
  return { userInfo }
}, { persist: true })

2. Store 模块化拆分

按业务模块拆分 Store,如 user.tscart.tssetting.ts,通过目录结构管理:

复制代码
src/stores/
├── index.ts       # 导出所有Store
├── user.ts        # 用户模块
├── cart.ts        # 购物车模块
└── setting.ts     # 设置模块

index.ts 统一导出:

typescript 复制代码
export * from './user'
export * from './cart'
export * from './setting'

3. Pinia 插件开发

自定义插件扩展 Store 功能,比如添加日志、全局状态监听:

typescript 复制代码
// src/plugins/pinia-logger.ts
export const piniaLogger = ({ store }) => {
  store.$subscribe((mutation, state) => {
    console.log(`[Pinia Logger] ${mutation.storeId} 状态变化:`, mutation.payload, state)
  })
  store.$onAction(({ name, args, after, onError }) => {
    console.log(`[Pinia Logger] 调用动作: ${name}, 参数:`, args)
    after((result) => {
      console.log(`[Pinia Logger] 动作完成: ${name}, 返回值:`, result)
    })
    onError((error) => {
      console.error(`[Pinia Logger] 动作失败: ${name}, 错误:`, error)
    })
  })
}

// main.ts 注册插件
pinia.use(piniaLogger)

五、注意事项

1. 响应式处理

  • 禁止直接解构 Store :直接解构会丢失响应式,必须用 storeToRefs
  • 修改状态的推荐方式 :优先用 actions,其次用 $patch,不推荐直接修改 store.count = 10(虽然语法允许,但不利于调试和维护);
  • 组合式写法中的响应式 :用 ref/reactive 定义状态,确保响应式。

2. TypeScript 支持

  • 状态类型自动推断 :选项式写法中 Pinia 会自动推断 state 的类型;

  • 组合式写法需手动定义类型 :复杂对象建议用接口定义:

    typescript 复制代码
    interface UserInfo {
      name: string;
      age: number;
    }
    const userInfo = ref<UserInfo>({ name: '', age: 0 })
  • Actions 参数类型:明确参数类型,避免隐式 any。

3. 性能优化

  • Getters 缓存:合理利用 getters 的缓存特性,避免重复计算;
  • 批量修改状态 :用 $patch 批量修改,减少响应式更新次数;
  • 避免在 actions 中执行大量同步计算:复杂计算建议放到 getters 或单独的工具函数中。

六、开发中常见的坑

1. 解构 Store 丢失响应式

❌ 错误写法:

typescript 复制代码
const { count } = useCounterStore() // count 是普通值,不会响应式更新

✅ 正确写法:

typescript 复制代码
import { storeToRefs } from 'pinia'
const { count } = storeToRefs(useCounterStore()) // count 是 ref,保持响应式

2. 组合式写法中无法使用 $reset

选项式写法的 $reset 是自动生成的,组合式写法需手动实现:

typescript 复制代码
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  // 手动实现重置函数
  const reset = () => {
    count.value = 0
  }
  return { count, reset }
})

3. 持久化插件不生效

  • 确保插件已正确注册到 Pinia;

  • 组合式写法中,必须在 defineStore 的第三个参数中配置 persist: true

  • 如果状态包含复杂对象(如 Date、Map),需配置 serializer 自定义序列化:

    typescript 复制代码
    persist: {
      serializer: {
        serialize: (value) => JSON.stringify(value, (key, val) => {
          if (val instanceof Date) return val.toISOString()
          return val
        }),
        deserialize: (value) => JSON.parse(value, (key, val) => {
          if (key === 'createTime') return new Date(val)
          return val
        })
      }
    }

4. 在组件外使用 Store 实例

❌ 错误写法(组件外直接调用 Store,可能导致 Pinia 未初始化):

typescript 复制代码
// src/utils/api.ts
import { useUserStore } from '@/stores/user'
const userStore = useUserStore() // 组件外调用,可能报错

async function fetchData() {
  const token = userStore.token
}

✅ 正确写法(在函数内部调用 Store):

typescript 复制代码
async function fetchData() {
  const userStore = useUserStore() // 函数内部调用,确保 Pinia 已初始化
  const token = userStore.token
}

5. Getters 中使用 this 丢失类型

选项式写法中,getters 用箭头函数会丢失 this 类型,需用普通函数:

❌ 错误写法:

typescript 复制代码
getters: {
  doubleCount: () => this.count * 2 // this 类型为 any
}

✅ 正确写法:

typescript 复制代码
getters: {
  doubleCount(): number { // 必须指定返回值类型
    return this.count * 2
  }
}

七、Pinia vs Vuex 对比

特性 Pinia Vuex
模块化 每个 Store 都是独立模块,无嵌套 需嵌套 modules,结构复杂
Mutations 无,直接在 actions 中修改状态 必须通过 mutations 修改状态
TypeScript 天生支持,自动推断类型 需手动定义大量类型,繁琐
体积 轻量(约 1KB) 相对笨重
DevTools 支持 Vue3 DevTools,调试更友好 对 Vue3 支持有限

Pinia 完全替代 Vuex,是 Vue3 状态管理的首选方案!

相关推荐
米丘3 小时前
ESTree 规范 (acorn@8.15.0示例)
前端·javascript·编译器
饺子不吃醋3 小时前
深入理解浏览器渲染流程
前端·javascript
我命由我123453 小时前
React - 组件优化、children props 与 render props、错误边界
前端·javascript·react.js·前端框架·html·ecmascript·js
Java小卷4 小时前
前端表单构建神器 - formkit初体验
vue.js·低代码
李宏伟~4 小时前
大文件分片案例html + nodejs + 视频上传案例
javascript·html·音视频
计算机学姐4 小时前
基于SpringBoot的在线学习网站平台【个性化推荐+数据可视化+课程章节学习】
java·vue.js·spring boot·后端·学习·mysql·信息可视化
史迪仔01124 小时前
[QML] QT5和QT6 圆角的不同设置方法
前端·javascript·qt
暴力袋鼠哥4 小时前
基于 Django 与 Vue 的汽车数据分析系统设计与实现
vue.js·django·汽车
cat10month4 小时前
react坑点记录
前端·javascript·react.js