pinia

77 阅读2分钟

1. Pinia 的基本概念

Pinia 是 Vue 的官方状态管理库,相比 Vuex 更加轻量和简单。主要特点:

  • 无需命名空间

  • 更好的 TypeScript 支持

  • 更简单的 API

  • 支持组合式 API

  • 无需嵌套模块

2. Store 定义

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

// 选项式写法
export const useCounterStore = defineStore('counter', {
  // 状态
  state: () => ({
    count: 0,
    todos: [] as Todo[],
    user: null as User | null
  }),
  
  // 计算属性
  getters: {
    doubleCount: (state) => state.count * 2,
    doneTodos: (state) => state.todos.filter(todo => todo.done),
    // 带参数的 getter
    getTodoById: (state) => (id: number) => {
      return state.todos.find(todo => todo.id === id)
    }
  },
  
  // 操作方法
  actions: {
    increment() {
      this.count++
    },
    async fetchTodos() {
      const todos = await api.getTodos()
      this.todos = todos
    }
  }
})

// 组合式写法
export const useUserStore = defineStore('user', () => {
  // 状态
  const token = ref<string | null>(null)
  const userInfo = ref<User | null>(null)
  
  // 计算属性
  const isLoggedIn = computed(() => !!token.value)
  
  // 操作方法
  async function login(credentials: Credentials) {
    const response = await api.login(credentials)
    token.value = response.token
    userInfo.value = response.user
  }
  
  function logout() {
    token.value = null
    userInfo.value = null
  }
  
  return {
    token,
    userInfo,
    isLoggedIn,
    login,
    logout
  }
})

3. 在组件中使用

<template>
  <div>
    <!-- 使用状态 -->
    <p>Count: {{ counter.count }}</p>
    <p>Double Count: {{ counter.doubleCount }}</p>
    <p>User: {{ user.userInfo?.name }}</p>
    
    <!-- 操作状态 -->
    <button @click="increment">Increment</button>
    <button @click="handleLogin">Login</button>
    <button @click="user.logout">Logout</button>
    
    <!-- 使用 getter -->
    <ul>
      <li v-for="todo in counter.doneTodos" :key="todo.id">
        {{ todo.text }}
      </li>
    </ul>
  </div>
</template>

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

const counter = useCounterStore()
const user = useUserStore()

// 解构(使用 storeToRefs 保持响应性)
const { count, doubleCount } = storeToRefs(counter)
const { isLoggedIn } = storeToRefs(user)

// 方法可以直接解构
const { increment } = counter

// 登录处理
const handleLogin = async () => {
  await user.login({
    username: 'test',
    password: '123456'
  })
}

// 监听状态变化
watch(
  () => counter.count,
  (newValue) => {
    console.log('count changed:', newValue)
  }
)
</script>

4. 持久化插件

// plugins/persistedState.ts
import { createPersistedState } from 'pinia-plugin-persistedstate'

export const piniaPersistedState = createPersistedState({
  key: (id) => `store-${id}`,
  storage: localStorage,
  paths: ['user.token'] // 指定要持久化的路径
})

// main.ts
import { createPinia } from 'pinia'
import { piniaPersistedState } from './plugins/persistedState'

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

app.use(pinia)

5. 自定义插件

// plugins/resetStore.ts
import { PiniaPluginContext } from 'pinia'

export function resetStore({ store }: PiniaPluginContext) {
  const initialState = JSON.parse(JSON.stringify(store.$state))
  
  store.$reset = () => {
    store.$patch(initialState)
  }
}

// main.ts
const pinia = createPinia()
pinia.use(resetStore)

6. TypeScript 支持

// types/store.ts
export interface User {
  id: number
  name: string
  email: string
}

export interface Todo {
  id: number
  text: string
  done: boolean
}

// stores/counter.ts
import type { User, Todo } from '@/types/store'

interface CounterState {
  count: number
  todos: Todo[]
}

export const useCounterStore = defineStore('counter', {
  state: (): CounterState => ({
    count: 0,
    todos: []
  })
})

7. 实际应用示例

// stores/cart.ts
interface CartItem {
  id: number
  name: string
  price: number
  quantity: number
}

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [] as CartItem[]
  }),
  
  getters: {
    totalItems: (state) => state.items.reduce((sum, item) => sum + item.quantity, 0),
    totalAmount: (state) => state.items.reduce((sum, item) => sum + item.price * item.quantity, 0),
    
    itemById: (state) => (id: number) => state.items.find(item => item.id === id)
  },
  
  actions: {
    addItem(product: Product, quantity = 1) {
      const existingItem = this.items.find(item => item.id === product.id)
      
      if (existingItem) {
        existingItem.quantity += quantity
      } else {
        this.items.push({
          id: product.id,
          name: product.name,
          price: product.price,
          quantity
        })
      }
    },
    
    removeItem(id: number) {
      const index = this.items.findIndex(item => item.id === id)
      if (index > -1) {
        this.items.splice(index, 1)
      }
    },
    
    clearCart() {
      this.items = []
    },
    
    async checkout() {
      try {
        await api.createOrder(this.items)
        this.clearCart()
        return true
      } catch (error) {
        console.error('Checkout failed:', error)
        return false
      }
    }
  }
})

要点:

  1. Pinia vs Vuex:
  • 更简单的 API

  • 更好的 TypeScript 支持

  • 无需模块嵌套

  • 更轻量级

  1. 状态管理最佳实践:
  • 合理划分 store

  • 使用 storeToRefs 保持响应性

  • 适当使用持久化

  • 处理异步操作

  1. 性能优化:
  • 避免不必要的计算属性

  • 合理使用缓存

  • 按需导入 store

  1. 调试技巧:
  • Vue DevTools 支持

  • 状态快照

  • 时间旅行调试

  1. 实际应用场景:
  • 用户认证

  • 购物车管理

  • 全局配置

  • 数据缓存