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
  }
})
相关推荐
leoZ2313 小时前
Claude 驱动的全栈开发:Spring Boot + Vue 的 BS 架构实践
vue.js·spring boot·架构
荒诞英雄6 小时前
从 17MB Vendor 大包到按需加载:Vite 多入口 Vue 组件库首屏优化实践
vue.js·vite
jsonbro6 小时前
手把手带你实现发布订阅模式
前端·vue.js·设计模式
Cobyte9 小时前
23.Vue Vapor 组件 attrs 的实现
前端·javascript·vue.js
斯特凡今天也很帅9 小时前
xml页面可以打开,不报错,但是显示数据的地方也不显示,我是怎么解决的
xml·vue.js
柯克七七11 小时前
给老项目加了 TypeScript,本来只想自己爽,结果全公司代码审查标准被我抬高了
前端·vue.js·typescript
自然 醒13 小时前
前端如何实现在线预览office常见文件功能?
前端·vue.js
肉肉不吃 肉13 小时前
前端调试跨域如何解决
前端·vue.js
卓怡学长14 小时前
w268基于springboot + vue 物流系统
java·数据库·vue.js·spring boot·spring·intellij-idea
迅速的自行车1 天前
从一段模板说起
前端·javascript·vue.js