Pinia :下一代 Vue 状态管理

一、Pinia 是什么?为什么选择它?

1.1 Pinia 简介

Pinia(发音 /piːnjʌ/,类似英文中的 "peenya")是 Vue.js 的下一代状态管理库。它由 Vue 核心团队成员开发,旨在提供一个更简单、更直观的状态管理方案。

1.2 为什么从 Vuex 迁移到 Pinia?

特性 Vuex Pinia
API 复杂度 较高,需要理解多个概念 极简,更符合 Vue 3 理念
TypeScript 支持 需要额外配置 一流的 TypeScript 支持
模块系统 需要 modules 配置 每个 store 都是模块
Composition API 支持但不够自然 为 Composition API 而生
打包体积 约 10KB 约 1KB
开发体验 良好 优秀

二、快速开始

2.1 安装

bash

复制代码
# 使用 npm
npm install pinia

# 使用 yarn
yarn add pinia

# 使用 pnpm
pnpm add pinia

2.2 基本配置

javascript

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

// 创建 Pinia 实例
const pinia = createPinia()

// 创建 Vue 应用并挂载 Pinia
const app = createApp(App)
app.use(pinia)
app.mount('#app')

三、核心概念详解

3.1 Store(仓库)

Pinia 的核心概念是 store,它包含状态、操作和 getter。

3.1.1 创建第一个 Store

typescript

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

// 使用 defineStore 定义 store
// 第一个参数是 store 的唯一 ID
export const useCounterStore = defineStore('counter', {
  // 状态(相当于 Vuex 的 state)
  state: () => ({
    count: 0,
    name: 'Pinia Store'
  }),
  
  // 计算属性(相当于 Vuex 的 getters)
  getters: {
    doubleCount: (state) => state.count * 2,
    // 使用 this 访问其他 getter
    doubleCountPlusOne(): number {
      return this.doubleCount + 1
    }
  },
  
  // 操作方法(相当于 Vuex 的 actions + mutations)
  actions: {
    increment() {
      this.count++
    },
    decrement() {
      this.count--
    },
    // 可以异步
    async incrementAsync() {
      await new Promise(resolve => setTimeout(resolve, 1000))
      this.increment()
    }
  }
})
3.1.2 在组件中使用

vue

复制代码
<template>
  <div>
    <h1>{{ counterStore.name }}</h1>
    <p>Count: {{ counterStore.count }}</p>
    <p>Double Count: {{ counterStore.doubleCount }}</p>
    <p>Double Count + 1: {{ counterStore.doubleCountPlusOne }}</p>
    
    <button @click="counterStore.increment()">+</button>
    <button @click="counterStore.decrement()">-</button>
    <button @click="counterStore.incrementAsync()">Async +</button>
    
    <button @click="reset">Reset</button>
  </div>
</template>

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

// 使用 store
const counterStore = useCounterStore()

// 重置 store
const reset = () => {
  counterStore.$reset()
}

// 监听变化
counterStore.$subscribe((mutation, state) => {
  console.log('状态变化:', mutation)
  console.log('新状态:', state)
})

// 监听 action
counterStore.$onAction(({ name, store, args, after, onError }) => {
  console.log(`开始执行 action: ${name}`)
  
  after((result) => {
    console.log(`action ${name} 执行完成`)
  })
  
  onError((error) => {
    console.warn(`action ${name} 执行失败:`, error)
  })
})
</script>

3.2 State(状态)

3.2.1 定义状态

typescript

复制代码
// stores/user.ts
export const useUserStore = defineStore('user', {
  state: () => ({
    // 基础类型
    id: null as number | null,
    name: '',
    email: '',
    
    // 对象
    profile: {
      avatar: '',
      bio: '',
      location: ''
    },
    
    // 数组
    permissions: [] as string[],
    
    // 复杂嵌套
    preferences: {
      theme: 'light',
      notifications: {
        email: true,
        push: false
      }
    }
  })
})
3.2.2 访问和修改状态

vue

复制代码
<template>
  <div>
    <p>用户名: {{ userStore.name }}</p>
    <p>邮箱: {{ userStore.email }}</p>
    <p>主题: {{ userStore.preferences.theme }}</p>
    
    <!-- 直接修改 -->
    <input v-model="userStore.name" />
    
    <!-- 批量修改 -->
    <button @click="updateUser">更新用户信息</button>
    
    <!-- 替换整个 state -->
    <button @click="resetUser">重置用户</button>
  </div>
</template>

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

const userStore = useUserStore()

// 1. 直接修改
const updateName = () => {
  userStore.name = '新的用户名'
}

// 2. $patch 批量修改
const updateUser = () => {
  // 方式1:传递对象
  userStore.$patch({
    name: '张三',
    email: 'zhangsan@example.com'
  })
  
  // 方式2:传递函数(适合修改嵌套对象或数组)
  userStore.$patch((state) => {
    state.profile.avatar = '/new-avatar.jpg'
    state.permissions.push('admin')
    state.preferences.theme = 'dark'
  })
}

// 3. 替换整个 state
const resetUser = () => {
  userStore.$state = {
    id: null,
    name: '',
    email: '',
    profile: { avatar: '', bio: '', location: '' },
    permissions: [],
    preferences: {
      theme: 'light',
      notifications: { email: true, push: false }
    }
  }
}

// 4. 重置到初始状态
const reset = () => {
  userStore.$reset()
}
</script>

3.3 Getters(计算属性)

3.3.1 定义 Getter

typescript

复制代码
// stores/products.ts
export const useProductStore = defineStore('products', {
  state: () => ({
    products: [
      { id: 1, name: 'iPhone 13', price: 6999, category: 'phone', stock: 10 },
      { id: 2, name: 'MacBook Pro', price: 12999, category: 'laptop', stock: 5 },
      { id: 3, name: 'AirPods Pro', price: 1999, category: 'audio', stock: 20 },
      { id: 4, name: 'iPad Air', price: 4799, category: 'tablet', stock: 8 }
    ],
    selectedCategory: 'all'
  }),
  
  getters: {
    // 1. 基础 getter(自动推断类型)
    totalProducts: (state) => state.products.length,
    
    // 2. 使用 this 访问其他 getter
    outOfStockProducts(state): Product[] {
      return state.products.filter(p => p.stock === 0)
    },
    
    // 3. 带参数的 getter(返回函数)
    productsByCategory: (state) => {
      return (category: string) => {
        if (category === 'all') return state.products
        return state.products.filter(p => p.category === category)
      }
    },
    
    // 4. 结合多个状态计算
    averagePrice(): number {
      if (this.totalProducts === 0) return 0
      const total = this.products.reduce((sum, p) => sum + p.price, 0)
      return total / this.totalProducts
    },
    
    // 5. 计算库存总值
    totalInventoryValue(): number {
      return this.products.reduce((sum, p) => sum + (p.price * p.stock), 0)
    },
    
    // 6. 过滤后的产品(响应式)
    filteredProducts(): Product[] {
      if (this.selectedCategory === 'all') {
        return this.products
      }
      return this.products.filter(p => p.category === this.selectedCategory)
    }
  }
})
3.3.2 在组件中使用 Getter

vue

复制代码
<template>
  <div>
    <h2>产品统计</h2>
    <p>总产品数: {{ productStore.totalProducts }}</p>
    <p>平均价格: ¥{{ productStore.averagePrice.toFixed(2) }}</p>
    <p>库存总值: ¥{{ productStore.totalInventoryValue }}</p>
    
    <h3>按分类筛选</h3>
    <select v-model="selectedCategory">
      <option value="all">全部</option>
      <option value="phone">手机</option>
      <option value="laptop">笔记本</option>
      <option value="audio">音频</option>
      <option value="tablet">平板</option>
    </select>
    
    <!-- 使用带参数的 getter -->
    <div v-for="product in phoneProducts" :key="product.id">
      {{ product.name }} - ¥{{ product.price }}
    </div>
    
    <!-- 使用响应式 getter -->
    <h3>当前分类的产品</h3>
    <div v-for="product in productStore.filteredProducts" :key="product.id">
      {{ product.name }} (库存: {{ product.stock }})
    </div>
  </div>
</template>

<script setup lang="ts">
import { useProductStore } from '@/stores/products'
import { computed, ref } from 'vue'

const productStore = useProductStore()
const selectedCategory = ref('all')

// 使用带参数的 getter
const phoneProducts = computed(() => {
  return productStore.productsByCategory('phone')
})

// 或者直接在模板中调用
const audioProducts = productStore.productsByCategory('audio')
</script>

3.4 Actions(操作方法)

3.4.1 定义 Action

typescript

复制代码
// stores/auth.ts
import type { LoginCredentials, User, ApiResponse } from '@/types'

export const useAuthStore = defineStore('auth', {
  state: () => ({
    user: null as User | null,
    token: localStorage.getItem('token') || '',
    loading: false,
    error: null as string | null
  }),
  
  actions: {
    // 1. 同步 action
    setUser(user: User) {
      this.user = user
    },
    
    // 2. 异步 action(支持 async/await)
    async login(credentials: LoginCredentials): Promise<ApiResponse<User>> {
      // 重置错误状态
      this.error = null
      this.loading = true
      
      try {
        // API 调用
        const response = await fetch('/api/login', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(credentials)
        })
        
        if (!response.ok) {
          throw new Error('登录失败')
        }
        
        const data = await response.json()
        
        // 更新状态
        this.user = data.user
        this.token = data.token
        
        // 保存到 localStorage
        localStorage.setItem('token', data.token)
        
        // 返回结果
        return {
          success: true,
          data: data.user,
          message: '登录成功'
        }
        
      } catch (error: any) {
        // 错误处理
        this.error = error.message || '登录失败'
        
        return {
          success: false,
          data: null,
          message: error.message
        }
        
      } finally {
        this.loading = false
      }
    },
    
    // 3. 调用其他 action
    async logout() {
      try {
        // 调用 API
        await fetch('/api/logout', {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${this.token}`
          }
        })
      } catch (error) {
        console.error('登出失败:', error)
      } finally {
        // 清理状态
        this.user = null
        this.token = ''
        localStorage.removeItem('token')
      }
    },
    
    // 4. 组合多个操作
    async registerAndLogin(userData: any) {
      // 注册
      const registerResult = await this.register(userData)
      
      if (!registerResult.success) {
        return registerResult
      }
      
      // 登录
      const loginResult = await this.login({
        email: userData.email,
        password: userData.password
      })
      
      return loginResult
    },
    
    // 5. 访问其他 store 的 action
    async completeLoginFlow(credentials: LoginCredentials) {
      const result = await this.login(credentials)
      
      if (result.success && this.user) {
        // 获取用户购物车
        const cartStore = useCartStore()
        await cartStore.fetchCart(this.user.id)
        
        // 获取用户偏好设置
        const preferenceStore = usePreferenceStore()
        await preferenceStore.fetchPreferences(this.user.id)
      }
      
      return result
    }
  }
})

四、模块化与 Store 组合

4.1 自动导入 Store(推荐)

javascript

复制代码
// stores/index.ts
export { useUserStore } from './user'
export { useProductStore } from './products'
export { useCartStore } from './cart'
export { useOrderStore } from './order'
export { useNotificationStore } from './notification'

// 或者使用自动导入
// 在 main.ts 中配置

4.2 Store 间通信

typescript

复制代码
// stores/cart.ts
export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [] as CartItem[],
    total: 0
  }),
  
  actions: {
    async addToCart(productId: number) {
      // 1. 访问其他 store 的状态
      const productStore = useProductStore()
      const product = productStore.getProductById(productId)
      
      if (!product || product.stock === 0) {
        throw new Error('商品不存在或库存不足')
      }
      
      // 2. 调用其他 store 的 action
      await productStore.decreaseStock(productId)
      
      // 3. 更新购物车
      const existingItem = this.items.find(item => item.productId === productId)
      
      if (existingItem) {
        existingItem.quantity++
      } else {
        this.items.push({
          productId,
          quantity: 1,
          price: product.price,
          name: product.name
        })
      }
      
      this.calculateTotal()
      
      // 4. 发送通知
      const notificationStore = useNotificationStore()
      notificationStore.show({
        message: '已添加到购物车',
        type: 'success'
      })
    },
    
    calculateTotal() {
      this.total = this.items.reduce((sum, item) => {
        return sum + (item.price * item.quantity)
      }, 0)
    }
  }
})

4.3 组合式 Store(Composition API 风格)

typescript

复制代码
// stores/composables/usePagination.ts
import { ref, computed } from 'vue'

export function usePagination<T>(items: T[], itemsPerPage = 10) {
  const currentPage = ref(1)
  const perPage = ref(itemsPerPage)
  
  const totalPages = computed(() => {
    return Math.ceil(items.length / perPage.value)
  })
  
  const paginatedItems = computed(() => {
    const start = (currentPage.value - 1) * perPage.value
    const end = start + perPage.value
    return items.slice(start, end)
  })
  
  const hasNextPage = computed(() => {
    return currentPage.value < totalPages.value
  })
  
  const hasPrevPage = computed(() => {
    return currentPage.value > 1
  })
  
  function nextPage() {
    if (hasNextPage.value) {
      currentPage.value++
    }
  }
  
  function prevPage() {
    if (hasPrevPage.value) {
      currentPage.value--
    }
  }
  
  function goToPage(page: number) {
    if (page >= 1 && page <= totalPages.value) {
      currentPage.value = page
    }
  }
  
  return {
    currentPage,
    perPage,
    totalPages,
    paginatedItems,
    hasNextPage,
    hasPrevPage,
    nextPage,
    prevPage,
    goToPage
  }
}

// 在 Store 中使用
export const useProductStore = defineStore('products', () => {
  // 使用 ref 定义状态(类似 Vue 3 的 Composition API)
  const products = ref<Product[]>([])
  const loading = ref(false)
  const error = ref<string | null>(null)
  
  // 使用 computed 定义 getter
  const totalProducts = computed(() => products.value.length)
  const featuredProducts = computed(() => 
    products.value.filter(p => p.isFeatured)
  )
  
  // 使用普通函数定义 action
  async function fetchProducts() {
    loading.value = true
    error.value = null
    
    try {
      const response = await api.getProducts()
      products.value = response.data
    } catch (err: any) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }
  
  // 组合使用 usePagination
  const pagination = usePagination(products)
  
  return {
    // 状态
    products,
    loading,
    error,
    
    // Getter
    totalProducts,
    featuredProducts,
    
    // Action
    fetchProducts,
    
    // 分页相关
    ...pagination
  }
})

五、TypeScript 支持

5.1 完整的类型定义

typescript

复制代码
// types/index.ts
export interface User {
  id: number
  name: string
  email: string
  avatar?: string
  roles: string[]
}

export interface Product {
  id: number
  name: string
  description: string
  price: number
  category: string
  stock: number
  images: string[]
  isFeatured: boolean
  createdAt: Date
  updatedAt: Date
}

export interface CartItem {
  productId: number
  quantity: number
  price: number
  name: string
  image?: string
}

// store 状态类型
export interface AuthState {
  user: User | null
  token: string
  loading: boolean
  error: string | null
}

// API 响应类型
export interface ApiResponse<T = any> {
  success: boolean
  data: T | null
  message: string
  code?: number
}

// stores/auth.ts
import type { AuthState, User, ApiResponse } from '@/types'

export const useAuthStore = defineStore('auth', {
  state: (): AuthState => ({
    user: null,
    token: '',
    loading: false,
    error: null
  }),
  
  getters: {
    // 完整类型推断
    isAuthenticated: (state): boolean => !!state.user,
    getUserName: (state): string => state.user?.name || '访客',
    // 带参数的 getter 类型
    hasPermission: (state) => {
      return (permission: string): boolean => {
        return state.user?.roles.includes(permission) || false
      }
    }
  },
  
  actions: {
    // 参数和返回值类型
    async login(email: string, password: string): Promise<ApiResponse<User>> {
      // ... 实现
    }
  }
})

5.2 自动类型推断

Pinia 会自动推断大部分类型,但你也可以显式指定:

typescript

复制代码
// 显式返回类型
const useStore = defineStore('store', {
  state: () => ({
    count: 0
  }),
  
  getters: {
    // 显式指定返回类型
    double(): number {
      return this.count * 2
    },
    
    // 或者让 TypeScript 推断
    triple() {
      return this.count * 3  // 自动推断为 number
    }
  },
  
  actions: {
    // 参数类型
    increment(amount: number = 1) {
      this.count += amount
    }
  }
})

六、持久化存储

6.1 使用插件

bash

复制代码
npm install pinia-plugin-persistedstate

6.2 配置持久化

typescript

复制代码
// main.ts
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

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

// 在 Store 中使用
export const useAuthStore = defineStore('auth', {
  state: () => ({
    user: null,
    token: ''
  }),
  
  // 开启持久化
  persist: {
    // 自定义 key
    key: 'my-app-auth',
    
    // 持久化到 localStorage
    storage: localStorage,
    
    // 只持久化部分状态
    paths: ['token', 'user.id', 'user.name'],
    
    // 自定义序列化
    serializer: {
      serialize: JSON.stringify,
      deserialize: JSON.parse
    },
    
    // 钩子函数
    beforeRestore: (ctx) => {
      console.log('准备恢复状态:', ctx.store.$id)
    },
    afterRestore: (ctx) => {
      console.log('状态恢复完成:', ctx.store.$id)
    }
  }
})

// 或者使用 Composition API 风格
export const useCartStore = defineStore('cart', () => {
  const items = ref<CartItem[]>([])
  const total = ref(0)
  
  return { items, total }
}, {
  persist: {
    key: 'shopping-cart',
    storage: sessionStorage,  // 使用 sessionStorage
    paths: ['items']  // 只持久化 items
  }
})

6.3 手动持久化

typescript

复制代码
export const useAuthStore = defineStore('auth', {
  state: () => ({
    user: null as User | null,
    token: ''
  }),
  
  actions: {
    initFromStorage() {
      const saved = localStorage.getItem('auth')
      if (saved) {
        const { user, token } = JSON.parse(saved)
        this.user = user
        this.token = token
      }
    },
    
    saveToStorage() {
      localStorage.setItem('auth', JSON.stringify({
        user: this.user,
        token: this.token
      }))
    }
  },
  
  // 监听状态变化自动保存
  onStateChange: {
    handler(state) {
      localStorage.setItem('auth', JSON.stringify(state))
    },
    deep: true
  }
})
相关推荐
闲蛋小超人笑嘻嘻2 小时前
非父子通信: provide和inject
前端·javascript·vue.js
AllinLin3 小时前
JS中的call apply bind全面解析
前端·javascript·vue.js
海绵宝龙3 小时前
Vue 中的 Diff 算法
前端·vue.js·算法
zhougl9963 小时前
vue中App.vue和index.html冲突问题
javascript·vue.js·html
袁煦丞 cpolar内网穿透实验室3 小时前
无需公网 IP 也能全球访问本地服务?cpolar+Spring Boot+Vue应用实践!
vue.js·spring boot·tcp/ip·远程工作·内网穿透·cpolar
浩泽学编程3 小时前
内网开发?系统环境变量无权限配置?快速解决使用其他版本node.js
前端·vue.js·vscode·node.js·js
狗哥哥3 小时前
Vue 3 插件系统重构实战:从过度设计到精简高效
前端·vue.js·架构
jenemy3 小时前
🚀 这个 ElDialog 封装方案,让我的代码量减少了 80%
vue.js·element
幽络源小助理3 小时前
SpringBoot+Vue雅苑小区管理系统源码 | Java物业项目免费下载 – 幽络源
java·vue.js·spring boot