← 返回主页

第12课: Pinia状态管理

什么是Pinia?

Pinia是Vue3的官方状态管理库,是Vuex的继任者。它提供了更简单的API、更好的TypeScript支持和模块化设计。

安装Pinia

# npm
npm install pinia

# yarn
yarn add pinia

# pnpm
pnpm add pinia

配置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')

定义Store

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

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: '计数器'
  }),
  getters: {
    doubleCount: (state) => state.count * 2,
    doubleCountPlusOne() {
      return this.doubleCount + 1
    }
  },
  actions: {
    increment() {
      this.count++
    },
    decrement() {
      this.count--
    },
    incrementBy(amount) {
      this.count += amount
    }
  }
})

使用Store

<template>
  <div>
    <h2>{{ store.name }}</h2>
    <p>计数: {{ store.count }}</p>
    <p>双倍: {{ store.doubleCount }}</p>
    <button @click="store.increment">+1</button>
    <button @click="store.decrement">-1</button>
    <button @click="store.incrementBy(10)">+10</button>
  </div>
</template>

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

const store = useCounterStore()
</script>

Setup语法定义Store

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

export const useCounterStore = defineStore('counter', () => {
  // state
  const count = ref(0)
  const name = ref('计数器')

  // getters
  const doubleCount = computed(() => count.value * 2)

  // actions
  function increment() {
    count.value++
  }

  function decrement() {
    count.value--
  }

  function incrementBy(amount) {
    count.value += amount
  }

  return {
    count,
    name,
    doubleCount,
    increment,
    decrement,
    incrementBy
  }
})
推荐:Setup语法更灵活,与Composition API风格一致。

解构Store

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

const store = useCounterStore()

// 错误:直接解构会失去响应式
const { count, doubleCount } = store

// 正确:使用storeToRefs保持响应式
const { count, doubleCount } = storeToRefs(store)

// actions可以直接解构
const { increment, decrement } = store
</script>

修改State

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

const store = useCounterStore()

// 方式1:直接修改
store.count++

// 方式2:使用$patch对象
store.$patch({
  count: store.count + 1,
  name: '新名称'
})

// 方式3:使用$patch函数
store.$patch((state) => {
  state.count++
  state.name = '新名称'
})

// 方式4:使用action(推荐)
store.increment()
</script>

重置State

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

const store = useCounterStore()

// 重置到初始状态
store.$reset()
</script>

订阅State变化

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

const store = useCounterStore()

// 订阅state变化
store.$subscribe((mutation, state) => {
  console.log('mutation类型:', mutation.type)
  console.log('新状态:', state)

  // 持久化到localStorage
  localStorage.setItem('counter', JSON.stringify(state))
})

// 订阅actions
store.$onAction(({ name, args, after, onError }) => {
  console.log(`Action ${name} 被调用,参数:`, args)

  after((result) => {
    console.log('Action完成,结果:', result)
  })

  onError((error) => {
    console.error('Action错误:', error)
  })
})
</script>

Getters

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

export const useUserStore = defineStore('user', {
  state: () => ({
    users: [
      { id: 1, name: '张三', age: 25 },
      { id: 2, name: '李四', age: 30 },
      { id: 3, name: '王五', age: 35 }
    ]
  }),
  getters: {
    // 自动推导返回类型
    userCount: (state) => state.users.length,

    // 访问其他getter
    userCountMessage() {
      return `共有 ${this.userCount} 个用户`
    },

    // 返回函数(不会缓存)
    getUserById: (state) => {
      return (userId) => state.users.find(u => u.id === userId)
    },

    // 访问其他store
    otherStoreGetter() {
      const otherStore = useOtherStore()
      return otherStore.someValue
    }
  }
})

异步Actions

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

export const useUserStore = defineStore('user', {
  state: () => ({
    users: [],
    loading: false,
    error: null
  }),
  actions: {
    async fetchUsers() {
      this.loading = true
      this.error = null

      try {
        const response = await fetch('/api/users')
        this.users = await response.json()
      } catch (error) {
        this.error = error.message
      } finally {
        this.loading = false
      }
    },

    async createUser(userData) {
      const response = await fetch('/api/users', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(userData)
      })
      const newUser = await response.json()
      this.users.push(newUser)
      return newUser
    }
  }
})

Store组合

// stores/cart.js
import { defineStore } from 'pinia'
import { useUserStore } from './user'
import { useProductStore } from './product'

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: []
  }),
  getters: {
    total() {
      const productStore = useProductStore()
      return this.items.reduce((sum, item) => {
        const product = productStore.getProductById(item.productId)
        return sum + (product?.price || 0) * item.quantity
      }, 0)
    }
  },
  actions: {
    addItem(productId, quantity = 1) {
      const userStore = useUserStore()
      if (!userStore.isLoggedIn) {
        throw new Error('请先登录')
      }

      const existingItem = this.items.find(i => i.productId === productId)
      if (existingItem) {
        existingItem.quantity += quantity
      } else {
        this.items.push({ productId, quantity })
      }
    }
  }
})

持久化插件

// plugins/persist.js
export function persistPlugin({ store }) {
  // 从localStorage恢复
  const saved = localStorage.getItem(store.$id)
  if (saved) {
    store.$patch(JSON.parse(saved))
  }

  // 订阅变化并保存
  store.$subscribe((mutation, state) => {
    localStorage.setItem(store.$id, JSON.stringify(state))
  })
}
// main.js
import { createPinia } from 'pinia'
import { persistPlugin } from './plugins/persist'

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

app.use(pinia)

实战示例:用户认证Store

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

export const useAuthStore = defineStore('auth', () => {
  const user = ref(null)
  const token = ref(localStorage.getItem('token'))
  const loading = ref(false)

  const isLoggedIn = computed(() => !!token.value)
  const userName = computed(() => user.value?.name || '游客')

  async function login(credentials) {
    loading.value = true
    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(credentials)
      })
      const data = await response.json()

      token.value = data.token
      user.value = data.user
      localStorage.setItem('token', data.token)

      return true
    } catch (error) {
      console.error('登录失败:', error)
      return false
    } finally {
      loading.value = false
    }
  }

  function logout() {
    user.value = null
    token.value = null
    localStorage.removeItem('token')
  }

  async function fetchUser() {
    if (!token.value) return

    try {
      const response = await fetch('/api/user', {
        headers: { Authorization: `Bearer ${token.value}` }
      })
      user.value = await response.json()
    } catch (error) {
      logout()
    }
  }

  return {
    user,
    token,
    loading,
    isLoggedIn,
    userName,
    login,
    logout,
    fetchUser
  }
})

实战示例:购物车Store

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

export const useCartStore = defineStore('cart', () => {
  const items = ref([])

  const itemCount = computed(() => {
    return items.value.reduce((sum, item) => sum + item.quantity, 0)
  })

  const total = computed(() => {
    return items.value.reduce((sum, item) => {
      return sum + item.price * item.quantity
    }, 0)
  })

  function addItem(product) {
    const existingItem = items.value.find(i => i.id === product.id)

    if (existingItem) {
      existingItem.quantity++
    } else {
      items.value.push({
        id: product.id,
        name: product.name,
        price: product.price,
        quantity: 1
      })
    }
  }

  function removeItem(productId) {
    const index = items.value.findIndex(i => i.id === productId)
    if (index > -1) {
      items.value.splice(index, 1)
    }
  }

  function updateQuantity(productId, quantity) {
    const item = items.value.find(i => i.id === productId)
    if (item) {
      item.quantity = quantity
      if (item.quantity <= 0) {
        removeItem(productId)
      }
    }
  }

  function clear() {
    items.value = []
  }

  return {
    items,
    itemCount,
    total,
    addItem,
    removeItem,
    updateQuantity,
    clear
  }
})

Pinia vs Vuex

Pinia优势:
- 更简单的API,无需mutations
- 完整的TypeScript支持
- 模块化设计,无需嵌套
- 更小的体积
- 支持Vue2和Vue3

练习

  1. 创建一个待办事项Store,实现增删改查功能
  2. 创建一个主题Store,实现深色/浅色主题切换
  3. 创建一个用户Store,实现登录、登出和用户信息管理
  4. 实现Store持久化,将数据保存到localStorage