Pinia

什么是 Pinia?

Pinia 是 Vue 3 官方推荐的状态管理库,它替代了 Vuex,提供了更简洁的 API、更好的 TypeScript 支持和更灵活的结构。Pinia 支持 Vue 2 和 Vue 3,并且可以与组合式 API 无缝集成。

1. 安装 Pinia

使用 npm 或 yarn 安装 Pinia。

示例:安装 Pinia

# 使用 npm
npm install pinia

# 使用 yarn
yarn add pinia

2. 基本配置

创建 Pinia 实例并在应用中使用。

示例:基本配置

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

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

app.use(pinia)
app.mount('#app')

3. 创建 Store

使用 defineStore() 函数创建一个 store。

示例:创建 Store

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

export const useCounterStore = defineStore('counter', {
  // 状态
  state: () => ({
    count: 0,
    name: 'Vue3'
  }),
  
  // 计算属性
  getters: {
    doubleCount: (state) => state.count * 2,
    
    // 使用其他 getter
    doubleCountPlusOne() {
      return this.doubleCount + 1
    }
  },
  
  // 方法
  actions: {
    increment() {
      this.count++
    },
    
    // 异步操作
    async fetchData() {
      const response = await fetch('https://api.example.com/data')
      const data = await response.json()
      this.name = data.name
    }
  }
})

4. 使用 Store

在组件中使用 store。

示例:在组件中使用 Store

<template>
  <div class="counter">
    <h2>计数器: {{ counter.count }}</h2>
    <h3>双倍计数: {{ counter.doubleCount }}</h3>
    <h3>双倍计数 + 1: {{ counter.doubleCountPlusOne }}</h3>
    <h3>名称: {{ counter.name }}</h3>
    
    <button @click="counter.increment">增加</button>
    <button @click="counter.fetchData">获取数据</button>
    <button @click="reset">重置</button>
  </div>
</template>

<script setup>
import { useCounterStore } from '../stores/counter'
import { storeToRefs } from 'pinia'

// 创建 store 实例
const counter = useCounterStore()

// 使用 storeToRefs 将状态转换为响应式引用
const { count, name } = storeToRefs(counter)

// 重置状态
function reset() {
  counter.$reset()
}
</script>

5. 修改状态

Pinia 提供了多种修改状态的方式:

5.1 直接修改

示例:直接修改状态

const counter = useCounterStore()
counter.count++
counter.name = 'New Name'

5.2 使用 $patch 方法

使用 $patch 方法可以同时修改多个状态。

示例:使用 $patch 方法

const counter = useCounterStore()

// 对象形式
counter.$patch({
  count: counter.count + 1,
  name: 'New Name'
})

// 函数形式
counter.$patch((state) => {
  state.count++
  state.name = 'New Name'
})

5.3 使用 actions

对于复杂的状态修改逻辑,建议使用 actions。

示例:使用 actions

// stores/counter.js
export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),
  actions: {
    increment(amount = 1) {
      this.count += amount
    },
    
    async incrementAsync(amount = 1) {
      await new Promise(resolve => setTimeout(resolve, 1000))
      this.count += amount
    }
  }
})

// 在组件中使用
const counter = useCounterStore()
counter.increment(5) // 增加 5
counter.incrementAsync(10) // 异步增加 10

6. Getters

Getters 用于计算派生状态,类似于组件中的 computed 属性。

示例:Getters

// stores/user.js
export const useUserStore = defineStore('user', {
  state: () => ({
    users: [
      { id: 1, name: 'Alice', age: 25 },
      { id: 2, name: 'Bob', age: 30 },
      { id: 3, name: 'Charlie', age: 35 }
    ]
  }),
  getters: {
    // 基本 getter
    totalUsers: (state) => state.users.length,
    
    // 带参数的 getter
    getUserById: (state) => (id) => {
      return state.users.find(user => user.id === id)
    },
    
    // 使用其他 store 的 getter
    usersWithDetails(state) {
      const counterStore = useCounterStore()
      return state.users.map(user => ({
        ...user,
        counter: counterStore.count
      }))
    }
  }
})

// 在组件中使用
const userStore = useUserStore()
const totalUsers = userStore.totalUsers
const alice = userStore.getUserById(1) // { id: 1, name: 'Alice', age: 25 }

7. Actions

Actions 用于处理业务逻辑,可以是同步或异步的。

示例:Actions

// stores/todo.js
export const useTodoStore = defineStore('todo', {
  state: () => ({
    todos: [],
    loading: false,
    error: null
  }),
  actions: {
    async fetchTodos() {
      this.loading = true
      this.error = null
      
      try {
        const response = await fetch('https://jsonplaceholder.typicode.com/todos')
        if (!response.ok) {
          throw new Error('Failed to fetch todos')
        }
        this.todos = await response.json()
      } catch (error) {
        this.error = error.message
      } finally {
        this.loading = false
      }
    },
    
    addTodo(todo) {
      this.todos.push({
        id: Date.now(),
        ...todo,
        completed: false
      })
    },
    
    toggleTodo(id) {
      const todo = this.todos.find(t => t.id === id)
      if (todo) {
        todo.completed = !todo.completed
      }
    },
    
    deleteTodo(id) {
      this.todos = this.todos.filter(t => t.id !== id)
    }
  }
})

8. Pinia 与组合式 API

Pinia 可以与组合式 API 无缝集成,使用 defineStore() 的函数式写法。

示例:组合式 API 写法

// stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  // 状态
  const count = ref(0)
  const name = ref('Vue3')
  
  // 计算属性
  const doubleCount = computed(() => count.value * 2)
  const doubleCountPlusOne = computed(() => doubleCount.value + 1)
  
  // 方法
  function increment() {
    count.value++
  }
  
  async function fetchData() {
    const response = await fetch('https://api.example.com/data')
    const data = await response.json()
    name.value = data.name
  }
  
  // 返回状态、计算属性和方法
  return {
    count,
    name,
    doubleCount,
    doubleCountPlusOne,
    increment,
    fetchData
  }
})

// 在组件中使用
const counter = useCounterStore()
console.log(counter.count) // 0
counter.increment() // count 变为 1

9. Pinia 插件

Pinia 支持插件系统,可以扩展 Pinia 的功能。

示例:创建 Pinia 插件

// pinia-plugin-logger.js
export function loggerPlugin({ store }) {
  // 在 store 初始化时执行
  console.log(`Store ${store.$id} 初始化`)
  
  // 监听状态变化
  store.$subscribe((mutation, state) => {
    console.log(`Store ${store.$id} 状态变化:`, mutation, state)
  })
  
  // 监听 action 调用
  store.$onAction(({ name, args, after, onError }) => {
    console.log(`Store ${store.$id} 执行 action: ${name}`, args)
    
    after((result) => {
      console.log(`Store ${store.$id} action ${name} 成功:`, result)
    })
    
    onError((error) => {
      console.error(`Store ${store.$id} action ${name} 失败:`, error)
    })
  })
}

// 在 main.js 中使用插件
import { createPinia } from 'pinia'
import { loggerPlugin } from './pinia-plugin-logger'

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

10. Pinia 最佳实践

  • 将状态按功能模块划分到不同的 store 中
  • 使用 TypeScript 定义 store 类型,获得更好的类型支持
  • 对于复杂的业务逻辑,使用 actions 而不是直接修改状态
  • 使用 getters 计算派生状态,避免重复计算
  • 合理使用插件扩展 Pinia 功能
  • 考虑使用 Pinia DevTools 进行调试
  • 对于大型应用,可以考虑模块化 store