pinia状态管理工具

pinia状态管理工具

Pinia 是 Vue.js 官方推荐的新一代状态管理库,可以看作是 Vuex 的替代品。

1. 什么是 Pinia?

Pinia 是 Vue 的专属状态管理库,它允许你跨组件或页面共享状态。由 Vue.js 核心团队维护,并且对 TypeScript 有着极其出色的支持。

核心设计理念:

  • 直观: 像定义组件一样定义 Store,API 设计尽可能简单直观。
  • 类型安全: 全程提供出色的 TypeScript 支持,无需复杂的包装器。
  • 模块化: 可以拥有多个 Store,并自然地编码,让 Store 自动打包。
  • 轻量: 体积非常小(约 1KB),几乎不会增加打包负担。
  • Composition API: 完美契合 Vue 3 的 Composition API,但同时也支持 Option API。

2. 核心概念(与 Vuex 对比)

Pinia 的核心概念比 Vuex 更加简化,去掉了容易令人混淆的 mutations

概念 Vuex Pinia 说明
状态 state state 存储应用状态数据的地方
获取状态 getters getters 用于计算和派生状态,相当于 Store 的计算属性
修改状态 mutations actions 这是最大的不同 。Pinia 中 actions 既可用于同步,也可用于异步操作
异步操作 actions actions 在 Pinia 中,异步和同步操作都在 actions 中完成

简单来说,Pinia 就是:state + getters + actions


3. 安装与设置

1. 安装:

bash 复制代码
npm install pinia
# 或
yarn add pinia

2. 在 Vue 应用中创建并引入 Pinia:

main.jsmain.ts 中:

javascript 复制代码
import { createApp } from 'vue'
import { createPinia } from 'pinia' // 导入 createPinia 函数
import App from './App.vue'

// 创建 Pinia 实例
const pinia = createPinia()
// 创建 Vue 应用实例
const app = createApp(App)

// 使用 Pinia
app.use(pinia)

app.mount('#app')

4. 定义一个 Store

Pinia 使用 defineStore() 函数来定义一个 Store,它需要一个唯一名称(必填)作为第一个参数。这个名称是为了在 Devtools 中调试使用。

Store 有两种定义风格,类似于 Vue 的 Option APIComposition API

方式一:Option Store(选项式风格)

这种方式与 Vuex 的写法非常相似,更容易从 Vuex 迁移过来。

javascript 复制代码
// stores/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  // 状态数据
  state: () => ({
    count: 0,
    name: 'Eduardo',
  }),
  // 计算属性/派生状态
  getters: {
    doubleCount: (state) => state.count * 2,
    doubleCountPlusOne() { // 可以使用 this 访问整个 store 实例
      return this.doubleCount + 1
    },
  },
  // 操作方法(同步和异步)
  actions: {
    increment() {
      this.count++ // 通过 this 访问 state
    },
    async incrementAsync() {
      setTimeout(() => {
        this.increment() // 可以调用其他 action
      }, 1000)
    },
  },
})
方式二:Setup Store(组合式风格)

这种方式使用一个函数来定义 Store,类似于 Vue 的 setup() 函数和 Composition API。

javascript 复制代码
// stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  // state (使用 ref)
  const count = ref(0)
  const name = ref('Eduardo')

  // getters (使用 computed)
  const doubleCount = computed(() => count.value * 2)
  const doubleCountPlusOne = computed(() => doubleCount.value + 1)

  // actions (使用普通函数)
  function increment() {
    count.value++
  }
  async function incrementAsync() {
    setTimeout(() => {
      increment()
    }, 1000)
  }

  // 返回所有需要暴露的状态和方法
  return {
    count,
    name,
    doubleCount,
    doubleCountPlusOne,
    increment,
    incrementAsync,
  }
})

如何选择?

  • 如果你习惯 Vuex 或 Option API,选 Option Store
  • 如果你喜欢 Composition API 的灵活性,选 Setup Store

5. 在组件中使用 Store

在组件中,你需要导入并调用 你定义的 Store 函数(例如 useCounterStore)来使用它。Pinia 会自动管理单例。

访问 State 和 Getters
js 复制代码
<template>
  <div>
    <p>Count: {{ counter.count }}</p>
    <p>Double Count: {{ counter.doubleCount }}</p>
    <p>Double Count Plus One: {{ counter.doubleCountPlusOne }}</p>
    <p>Name: {{ name }}</p> <!-- 直接解构的 name -->
  </div>
</template>

<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia' // 重要!用于保持响应式

// 在 setup 中调用 Store 函数
const counter = useCounterStore()

// ❌ 错误:直接解构会失去响应式!
// const { count, name } = counter

// ✅ 正确:使用 storeToRefs 解构可以保持响应式
const { count, name } = storeToRefs(counter)
// 注意:actions 不需要也不应该用 storeToRefs 解构,直接通过 counter 调用即可
</script>
调用 Actions
js 复制代码
<template>
  <div>
    <button @click="counter.increment()">Increment</button>
    <button @click="counter.incrementAsync()">Increment Async</button>
  </div>
</template>

<script setup>
import { useCounterStore } from '@/stores/counter'

const counter = useCounterStore()

// 也可以在方法或事件处理逻辑中调用
function handleClick() {
  counter.increment()
}
</script>

6. 状态修改与响应式

  • 直接修改: 你可以直接修改状态(counter.count++),这在 Pinia 中是允许的。
  • 批量修改: 使用 $patch 方法进行批量更新,这有利于性能优化,并且 Devtools 会将其记录为一次修改。
javascript 复制代码
const counter = useCounterStore()

// 方式一:传入一个部分 state 对象
counter.$patch({
  count: counter.count + 1,
  name: 'New Name',
})

// 方式二:传入一个修改函数,适用于修改数组或嵌套对象
counter.$patch((state) => {
  state.items.push({ name: 'shoes', quantity: 1 })
  state.hasChanged = true
})
  • 替换整个状态: 可以通过 $state 属性替换整个 store 的状态。
javascript 复制代码
counter.$state = { count: 1000, name: 'Paimon' }

7. 高级特性与技巧

订阅状态变化

可以使用 $subscribe 来监听 state 及其变化,类似于 Vuex 的 subscribe。常用于持久化等操作。

javascript 复制代码
counter.$subscribe((mutation, state) => {
  // mutation 包含了修改的信息(events, type, storeId)
  // state 是修改后的新状态
  localStorage.setItem('counter', JSON.stringify(state))
})
订阅 Action

可以使用 $onAction 来监听 action 及其结果。

javascript 复制代码
const unsubscribe = counter.$onAction(({ name, store, args, after, onError }) => {
  // action 调用前执行
  console.log(`Start "${name}" with params [${args.join(', ')}].`)

  // after 在 action 成功并返回后执行
  after((result) => {
    console.log(`Finished "${name}". Result was: ${result}.`)
  })

  // onError 在 action 抛出错误或 reject 时执行
  onError((error) => {
    console.warn(`Failed "${name}": ${error}.`)
  })
})

// 手动移除订阅
// unsubscribe()
模块化(多 Store)

Pinia 天生是模块化的。你只需要定义多个不同的 Store 并在不同组件中引入它们即可。

javascript 复制代码
// stores/user.js
export const useUserStore = defineStore('user', {
  state: () => ({ user: null }),
  // ...
})

// stores/cart.js
export const useCartStore = defineStore('cart', {
  state: () => ({ items: [] }),
  // ...
})

然后在组件中可以同时使用多个 Store:

js 复制代码
<script setup>
import { useUserStore } from '@/stores/user'
import { useCartStore } from '@/stores/cart'

const userStore = useUserStore()
const cartStore = useCartStore()
// 甚至可以在一个 Store 中使用另一个 Store
// (注意避免循环引用)
</script>

总结:为什么选择 Pinia?

  1. 更简单的 API: 去除 mutations,只有 state, getters, actions,概念更清晰。
  2. 完美的 TS 支持: 无需复杂配置,类型推断非常强大。
  3. 模块化设计: 不需要嵌套模块,多个 Store 自然拆分。
  4. 轻量级: 体积极小,对应用打包体积影响几乎可以忽略不计。
  5. Composition API: 与 Vue 3 的编程思想完美契合,使用起来更加灵活。
  6. 官方推荐: 作为 Vuex 的继任者,是未来 Vue 生态的状态管理首选。

对于新项目,强烈推荐直接使用 Pinia。对于老项目,如果 Vuex 能满足需求且没有维护性问题,不一定需要立即迁移,但 Pinia 绝对是未来趋势。