Pinia状态管理

下面,我们来系统的梳理关于 Pinia 状态管理 的基本知识点:


一、Pinia 核心概念

1.1 什么是 Pinia?

Pinia(发音为 /piːnjʌ/)是 Vue 的官方状态管理库,专为 Vue 3 设计,同时支持 Vue 2。它提供了比 Vuex 更简洁的 API 和更强大的 TypeScript 支持,是 Vue 生态中现代状态管理的首选方案。

1.2 Pinia 的核心优势

特性说明与 Vuex 对比
简洁 API移除 mutations,只有 state/getters/actions减少样板代码
TypeScript 支持一流的类型推断开箱即用,无需额外配置
模块化自动命名空间无需手动管理命名空间
Composition API原生支持更符合 Vue 3 设计哲学
体积约 1KB比 Vuex 更轻量
Devtools 支持完整集成与 Vuex 相同体验

1.3 何时使用 Pinia?

  • 需要跨组件共享状态
  • 需要持久化应用状态
  • 管理复杂业务逻辑
  • 需要状态的时间旅行调试
  • 在 Vue 3 项目中替代 Vuex

二、安装与基础使用

2.1 安装 Pinia

npm install pinia
# 或
yarn add pinia

2.2 创建 Pinia 实例

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

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

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

2.3 创建第一个 Store

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

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: 'Pinia User'
  }),
  getters: {
    doubleCount: (state) => state.count * 2,
    personalizedGreeting: (state) => `Hello, ${state.name}!`
  },
  actions: {
    increment() {
      this.count++
    },
    async incrementAsync() {
      await new Promise(resolve => setTimeout(resolve, 1000))
      this.increment()
    }
  }
})

2.4 在组件中使用 Store

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

const counterStore = useCounterStore()
</script>

<template>
  <div>
    <h1>{{ counterStore.name }}</h1>
    <p>Count: {{ counterStore.count }}</p>
    <p>Double: {{ counterStore.doubleCount }}</p>
    <button @click="counterStore.increment">Increment</button>
    <button @click="counterStore.incrementAsync">Increment Async</button>
  </div>
</template>

三、核心概念详解

3.1 State

定义响应式状态

state: () => ({
  user: {
    id: 1,
    name: 'Alice',
    email: 'alice@example.com'
  },
  preferences: {
    theme: 'dark',
    notifications: true
  },
  items: []
})

状态操作

// 重置状态
counterStore.$reset()

// 修改状态
counterStore.count = 10

// 批量更新
counterStore.$patch({
  count: counterStore.count + 1,
  name: 'Bob'
})

// 函数式更新
counterStore.$patch((state) => {
  state.items.push({ id: 1, name: 'Item' })
  state.user.name = 'Updated'
})

3.2 Getters

计算属性

getters: {
  // 基本 getter
  itemCount: (state) => state.items.length,
  
  // 使用其他 getter
  itemCountMessage() {
    return `Total items: ${this.itemCount}`
  },
  
  // 带参数的 getter
  getItemById: (state) => (id) => {
    return state.items.find(item => item.id === id)
  }
}

使用

const itemCount = counterStore.itemCount
const item = counterStore.getItemById(1)

3.3 Actions

同步与异步操作

actions: {
  // 同步 action
  addItem(item) {
    this.items.push(item)
  },
  
  // 异步 action
  async fetchUser(userId) {
    try {
      const response = await fetch(`/api/users/${userId}`)
      this.user = await response.json()
    } catch (error) {
      console.error('Failed to fetch user:', error)
    }
  },
  
  // 组合 actions
  async fetchAndProcessUser(userId) {
    await this.fetchUser(userId)
    this.processUserData()
  },
  
  processUserData() {
    // 处理用户数据
  }
}

四、高级特性

4.1 Composition API 风格 Store

export const useProductStore = defineStore('products', () => {
  // 状态
  const products = ref([])
  const loading = ref(false)
  const error = ref(null)
  
  // getters
  const featuredProducts = computed(() => 
    products.value.filter(p => p.isFeatured)
  )
  
  // actions
  async function fetchProducts() {
    try {
      loading.value = true
      const response = await fetch('/api/products')
      products.value = await response.json()
    } catch (err) {
      error.value = err.message
    } finally {
      loading.value = false
    }
  }
  
  return { products, loading, error, featuredProducts, fetchProducts }
})

4.2 Store 之间的交互

// stores/cart.js
export const useCartStore = defineStore('cart', {
  actions: {
    async checkout() {
      const authStore = useAuthStore()
      
      if (!authStore.isAuthenticated) {
        throw new Error('User not authenticated')
      }
      
      // 结账逻辑...
    }
  }
})

4.3 状态订阅

// 订阅状态变化
const unsubscribe = cartStore.$subscribe((mutation, state) => {
  console.log('State changed:', mutation)
  localStorage.setItem('cart', JSON.stringify(state))
})

// 取消订阅
unsubscribe()

// 订阅 actions
cartStore.$onAction(({ name, store, args, after, onError }) => {
  console.log(`Action "${name}" started`, args)
  
  after((result) => {
    console.log(`Action "${name}" completed`, result)
  })
  
  onError((error) => {
    console.error(`Action "${name}" failed`, error)
  })
})

4.4 插件开发

持久化插件示例

// plugins/persistence.js
export const piniaPersistencePlugin = ({ store }) => {
  // 从 localStorage 恢复状态
  const savedState = localStorage.getItem(store.$id)
  if (savedState) {
    store.$patch(JSON.parse(savedState))
  }
  
  // 订阅状态变化
  store.$subscribe((mutation, state) => {
    localStorage.setItem(store.$id, JSON.stringify(state))
  })
}

// main.js
import { piniaPersistencePlugin } from '@/plugins/persistence'

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

4.5 服务端渲染 (SSR)

// server.js
import { createSSRApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

export function createApp() {
  const app = createSSRApp(App)
  const pinia = createPinia()
  
  app.use(pinia)
  
  return { app, pinia }
}

// 在渲染上下文中附加状态
const piniaState = JSON.stringify(pinia.state.value)

// client.js
const pinia = createPinia()
app.use(pinia)

if (window.__PINIA_STATE__) {
  pinia.state.value = JSON.parse(window.__PINIA_STATE__)
}

五、TypeScript 集成

5.1 类型化 Store

// stores/user.ts
interface User {
  id: number
  name: string
  email: string
}

interface UserState {
  user: User | null
  loading: boolean
  error: string | null
}

export const useUserStore = defineStore('user', {
  state: (): UserState => ({
    user: null,
    loading: false,
    error: null
  }),
  getters: {
    isAuthenticated: (state) => state.user !== null
  },
  actions: {
    async fetchUser(id: number) {
      try {
        this.loading = true
        const response = await fetch(`/api/users/${id}`)
        this.user = await response.json()
      } catch (err: any) {
        this.error = err.message
      } finally {
        this.loading = false
      }
    }
  }
})

5.2 类型化使用

import { useUserStore } from '@/stores/user'

const userStore = useUserStore()

// 自动类型推断
userStore.user?.name // string | undefined
userStore.isAuthenticated // boolean
userStore.fetchUser(1) // 期望 number 参数

六、项目结构最佳实践

6.1 推荐项目结构

src/
├── stores/
│   ├── modules/
│   │   ├── auth.store.ts
│   │   ├── cart.store.ts
│   │   └── products.store.ts
│   ├── index.ts          # 导出所有 store
│   └── types.ts          # 共享类型定义
├── components/
├── composables/          # 组合式函数
├── views/
└── App.vue

6.2 模块化 Store 示例

// stores/modules/auth.store.ts
import { defineStore } from 'pinia'

interface AuthState {
  user: User | null
  token: string | null
}

export const useAuthStore = defineStore('auth', {
  state: (): AuthState => ({
    user: null,
    token: null
  }),
  getters: {
    isAuthenticated: state => state.token !== null
  },
  actions: {
    login(email: string, password: string) {
      // 登录逻辑
    },
    logout() {
      this.user = null
      this.token = null
    }
  }
})

6.3 组合式函数封装

// composables/useCartActions.ts
import { useCartStore } from '@/stores/cart'

export function useCartActions() {
  const cartStore = useCartStore()
  
  const addToCart = (product: Product) => {
    cartStore.addItem(product)
  }
  
  const removeFromCart = (productId: number) => {
    cartStore.removeItem(productId)
  }
  
  const clearCart = () => {
    cartStore.clear()
  }
  
  return {
    addToCart,
    removeFromCart,
    clearCart
  }
}

七、性能优化技巧

7.1 选择性状态解构

// 避免 - 破坏响应性
const { count, name } = counterStore // 失去响应性

// 推荐 - 使用 storeToRefs
import { storeToRefs } from 'pinia'

const counterStore = useCounterStore()
const { count, name } = storeToRefs(counterStore) // 保持响应性

7.2 高效状态更新

// 避免 - 多次更新
items.value.forEach(item => {
  cartStore.addItem(item) // 多次触发更新
})

// 推荐 - 批量更新
cartStore.$patch(state => {
  items.value.forEach(item => {
    state.items.push(item)
  })
})

7.3 计算属性优化

// 避免 - 复杂计算在 getter 中
getters: {
  expensiveComputation() {
    // 复杂计算
    return bigArray.value.filter(...).map(...)
  }
}

// 推荐 - 使用计算属性缓存
import { computed } from 'vue'

const expensiveResult = computed(() => {
  return bigArray.value.filter(...).map(...)
})

八、Pinia 插件生态系统

8.1 官方推荐插件

插件功能安装
pinia-plugin-persistedstate状态持久化npm i pinia-plugin-persistedstate
@pinia/nuxtNuxt.js 集成npm i @pinia/nuxt
pinia-ormORM 风格状态管理npm i pinia-orm

8.2 持久化状态插件使用

import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

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

// 在 store 中启用
export const useAuthStore = defineStore('auth', {
  persist: true,
  state: () => ({ token: null }),
  // ...
})

// 自定义配置
persist: {
  key: 'my-auth-store',
  storage: sessionStorage,
  paths: ['token'] // 只持久化 token
}

九、Pinia 与 Vuex 迁移指南

9.1 迁移步骤

  1. 安装 Pinianpm install pinia
  2. 创建 Pinia 实例:替换 Vuex 的创建方式
  3. 重构 Stores
    • 移除 mutations,将逻辑移到 actions
    • 保持 getters 不变
    • 简化模块结构
  4. 更新组件
    • 替换 mapState/mapGettersuseStore
    • 替换 mapActions 为直接调用
  5. 移除 Vuex:逐步替换直到完全移除

9.2 概念映射表

Vuex 概念Pinia 等效说明
statestate基本一致
gettersgetters基本一致
mutationsactions在 actions 中直接修改状态
actionsactions处理异步操作
modules独立 store每个 store 自动命名空间
namespaced: true自动实现无需配置

十、测试策略

10.1 单元测试 Store

import { setActivePinia, createPinia } from 'pinia'
import { useCounterStore } from '@/stores/counter'

describe('Counter Store', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })
  
  test('increment action', () => {
    const store = useCounterStore()
    store.increment()
    expect(store.count).toBe(1)
  })
  
  test('async increment', async () => {
    const store = useCounterStore()
    await store.incrementAsync()
    expect(store.count).toBe(1)
  })
  
  test('doubleCount getter', () => {
    const store = useCounterStore()
    store.count = 4
    expect(store.doubleCount).toBe(8)
  })
})

10.2 测试组件中的 Store

import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import CounterComponent from '@/components/Counter.vue'

test('component uses store', async () => {
  const wrapper = mount(CounterComponent, {
    global: {
      plugins: [createTestingPinia({
        initialState: {
          counter: { count: 10 }
        },
        stubActions: false
      })]
    }
  })
  
  expect(wrapper.text()).toContain('Count: 10')
  
  await wrapper.find('button').trigger('click')
  
  const store = useCounterStore()
  expect(store.increment).toHaveBeenCalledTimes(1)
})
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端岳大宝

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值