为什么选择 Pinia 而不是 Vuex

Pinia 已经成为 Vue 3 官方推荐的状态管理方案。与 Vuex 相比,Pinia 去掉了 mutation 的概念,简化了 API 设计。它天然支持 TypeScript 类型推断,不需要手写复杂的类型声明文件。Pinia 支持 multiple store,每个 store 独立管理,不再需要巨型单一状态树。此外,Pinia 的体积仅约 1KB(gzip 后),模块化方式也支持懒加载。

定义 Store:Options API 风格

Options Store 的结构类似于 Vue 组件选项式 API,通过 defineStore 传入配置对象来定义 state、getters 和 actions。

import { defineStore } from 'pinia'

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

  getters: {
    doubleCount: (state) => state.count * 2,
    // 访问其他 getter 时使用 this
    formattedName(): string {
      return `【${this.name}】当前值:${this.doubleCount}`
    }
  },

  actions: {
    increment() {
      this.count++ // 直接修改,无需 mutation
    },
    async fetchCount() {
      const res = await fetch('/api/count')
      this.count = await res.json()
    }
  }
})

定义 Store:Setup 语法

Setup Store 使用 refcomputed 和普通函数,与组合式 API 风格一致。对于习惯 Composition API 的开发者来说更加直观,也更容易实现逻辑复用。

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCartStore = defineStore('cart', () => {
  // state
  const items = ref<Array<{ id: number; name: string; price: number }>>([])

  // getters
  const total = computed(() =>
    items.value.reduce((sum, item) => sum + item.price, 0)
  )
  const itemCount = computed(() => items.value.length)

  // actions
  function addItem(item: { id: number; name: string; price: number }) {
    items.value.push(item)
  }

  function removeItem(id: number) {
    items.value = items.value.filter(item => item.id !== id)
  }

  async function loadCart() {
    const res = await fetch('/api/cart')
    items.value = await res.json()
  }

  return { items, total, itemCount, addItem, removeItem, loadCart }
})

在组件中使用 Store

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

const counter = useCounterStore()

// 解构时保持响应式
const { count, doubleCount } = storeToRefs(counter)
// actions 直接解构即可
const { increment } = counter
</script>

<template>
  <div>
    <p>计数:{{ count }},双倍:{{ doubleCount }}</p>
    <button @click="increment">+1</button>
  </div>
</template>

TypeScript 类型支持

Pinia 在设计之初就充分考虑了 TypeScript 的集成。Setup Store 中的 refcomputed 自动推导类型;Options Store 也通过泛型推断 state 和 getters 的类型。使用 storeToRefs 解构时,类型信息完整保留。

// 为 store 的 state 补充类型
interface UserState {
  name: string
  email: string
  role: 'admin' | 'editor' | 'viewer'
}

const useUserStore = defineStore('user', {
  state: (): UserState => ({
    name: '',
    email: '',
    role: 'viewer'
  }),
  actions: {
    setRole(role: UserState['role']) {
      this.role = role // 完整的类型检查
    }
  }
})

插件系统

Pinia 插件可以扩展 store 的功能,常见的用途包括持久化存储、路由权限控制、全局错误处理等。插件通过 pinia.use() 注册,每个 store 创建时都会执行插件函数。

import { createPinia } from 'pinia'

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))
  })
}

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

Store 组合与复用

在一个 store 中可以引用另一个 store,实现逻辑的组合。这在 Setup Store 中尤为自然,直接调用另一个 store 的组合函数即可。

import { defineStore } from 'pinia'
import { useUserStore } from './user'

export const useCartStore = defineStore('cart', () => {
  const userStore = useUserStore()

  const items = ref([])

  const canCheckout = computed(() => {
    return items.value.length > 0 && userStore.role !== 'viewer'
  })

  function checkout() {
    if (!canCheckout.value) return
    // 结算逻辑
  }

  return { items, canCheckout, checkout }
})

从 Vuex 迁移

从 Vuex 迁移到 Pinia 可以渐进式进行。核心映射关系为:Vuex 的 state 对应 Pinia 的 stategetters 对应 gettersmutationsactions 统一合并为 actions。Pinia 不再需要 dispatchcommit,直接调用 store 实例上的方法即可。模块命名空间也被扁平化的独立 store 取代,每个 store 通过唯一 ID 区分。

// Vuex 模块
const vuexModule = {
  namespaced: true,
  state: () => ({ count: 0 }),
  mutations: { INCREMENT(state) { state.count++ } },
  actions: { increment({ commit }) { commit('INCREMENT') } },
  getters: { double: state => state.count * 2 }
}

// 对应的 Pinia Store
export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  getters: { double: state => state.count * 2 },
  actions: { increment() { this.count++ } }
})