Pinia 状态管理实战:从 Vuex 迁移到 Pinia

Pinia 状态管理实战:从 Vuex 迁移到 Pinia

admin
2026-06-28 / 0 评论 / 4 阅读
Pinia 是 Vue 官方推荐的状态管理库,以其简洁的 API 和完整的 TypeScript 支持取代了 Vuex。本文系统讲解 Pinia 的核心概念、模块化设计、持久化方案以及从 Vuex 迁移的完整指南。

一、为什么选择 Pinia

Pinia vs Vuex

特性Vuex 4Pinia
API 风格Mutation + Action只有 Action(同步异步统一)
TypeScript支持较弱完整类型推导
模块化嵌套模块 + namespace扁平 Store,天然模块化
代码量较多样板代码极简
组合式 API适配一般原生设计
DevTools支持支持(时间旅行更佳)
SSR支持支持
热更新部分完整支持

Pinia 移除了什么

  • Mutations:直接修改 state 或在 action 中修改
  • 嵌套模块:每个 Store 独立扁平
  • 命名空间:Store 天然隔离
  • 辅助函数mapState/mapGetters 不再必要(组合式写法)

二、快速上手

安装与初始化

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

const app = createApp(App)
app.use(createPinia())
app.mount('#app')

定义第一个 Store

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

// 选项式写法(类似组件的 data/computed/methods)
export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: '计数器'
  }),
  getters: {
    double: (state) => state.count * 2,
    doublePlusOne() {
      // 通过 this 访问其他 getter
      return this.double + 1
    }
  },
  actions: {
    increment() {
      this.count++
    },
    async incrementAsync() {
      // 直接写异步逻辑,无需 dispatch
      await new Promise(r => setTimeout(r, 1000))
      this.count++
    }
  }
})

组合式写法(推荐)

// stores/counter.js — Setup Store 写法
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'

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

  // getters
  const double = computed(() => count.value * 2)
  const doublePlusOne = computed(() => double.value + 1)

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

  async function incrementAsync() {
    await new Promise(r => setTimeout(r, 1000))
    count.value++
  }

  // 必须返回所有使用的属性
  return { count, name, double, doublePlusOne, increment, incrementAsync }
})

在组件中使用

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

const counter = useCounterStore()

// ⚠️ 解构会丢失响应式,必须用 storeToRefs
const { count, double } = storeToRefs(counter)

// action 可以直接解构(它是函数)
const { increment } = counter

// 也可以不解构直接用
console.log(counter.count)
counter.increment()
</script>

<template>
  <div>
    <p>{{ counter.name }}: {{ count }}</p>
    <p>双倍: {{ double }}</p>
    <button @click="increment">+1</button>
  </div>
</template>

三、核心概念详解

State

// stores/user.js
export const useUserStore = defineStore('user', {
  state: () => ({
    user: null,
    token: '',
    permissions: [],
    settings: {
      theme: 'light',
      language: 'zh-CN'
    }
  }),

  actions: {
    // 重置整个 state 到初始值
    resetState() {
      this.$reset()
    },

    // 批量修改(替代 Vuex 的 mutation)
    setUser(user) {
      this.user = user
      this.permissions = user.permissions
    },

    // $patch 批量更新,性能更好
    updateSettings(partial) {
      this.$patch({
        settings: { ...this.settings, ...partial }
      })
      // 函数式写法
      this.$patch((state) => {
        state.settings.theme = 'dark'
      })
    }
  }
})

Getters

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: []
  }),

  getters: {
    // 基础 getter
    itemCount: (state) => state.items.length,

    // 带参数的 getter(返回函数,无缓存优势)
    itemById: (state) => {
      return (id) => state.items.find(item => item.id === id)
    },

    // 访问其他 getter
    subtotal() {
      return this.items.reduce((sum, item) => sum + item.price * item.qty, 0)
    },

    // 访问其他 Store
    total() {
      const userStore = useUserStore()
      const discount = userStore.user?.discount ?? 1
      return this.subtotal * discount
    }
  }
})

Actions

// stores/products.js
import { defineStore } from 'pinia'
import api from '@/api'

export const useProductStore = defineStore('products', () => {
  const products = ref([])
  const loading = ref(false)
  const error = ref(null)

  // 获取产品列表
  async function fetchProducts(params) {
    loading.value = true
    error.value = null
    try {
      const { data } = await api.get('/products', { params })
      products.value = data
    } catch (e) {
      error.value = e.message
    } finally {
      loading.value = false
    }
  }

  // 跨 Store 调用
  async function purchase(productId) {
    const cartStore = useCartStore()
    const product = products.value.find(p => p.id === productId)
    await api.post(`/cart/items`, { productId, qty: 1 })
    cartStore.addItem(product)
  }

  return { products, loading, error, fetchProducts, purchase }
})

四、插件系统

持久化插件

// plugins/persist.js
import { watch } from 'vue'

export function createPersistPlugin() {
  return ({ store }) => {
    // 从 localStorage 恢复
    const saved = localStorage.getItem(`pinia-${store.$id}`)
    if (saved) {
      store.$patch(JSON.parse(saved))
    }

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

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

使用官方持久化插件

npm install pinia-plugin-persistedstate
// stores/user.js
export const useUserStore = defineStore('user', {
  state: () => ({
    user: null,
    token: ''
  }),

  // 持久化配置
  persist: {
    key: 'my-app-user',
    storage: localStorage,      // 或 sessionStorage
    // 只持久化部分字段
    pick: ['token', 'user.id', 'user.name']
  },

  actions: {
    login(credentials) { /* ... */ },
    logout() {
      this.user = null
      this.token = ''
    }
  }
})

自定义日志插件

// plugins/logger.js
export function createLoggerPlugin() {
  return ({ store }) => {
    store.$subscribe((mutation, state) => {
      console.log(
        `[Pinia] ${mutation.storeId} ${mutation.type}:`,
        mutation.type === 'patch'
          ? mutation.events
          : `${mutation.payload}`
      )
    })

    store.$onAction(({ name, args, after, onError }) => {
      const startTime = Date.now()
      console.log(`[Action] ${store.$id}/${name} 开始`, args)

      after((result) => {
        console.log(`[Action] ${store.$id}/${name} 完成 (${Date.now() - startTime}ms)`)
      })

      onError((error) => {
        console.error(`[Action] ${store.$id}/${name} 失败:`, error)
      })
    })
  }
}

五、Store 组合与模块化

Store 之间相互调用

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

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

  const userStore = useUserStore()  // 在 setup 内调用

  const total = computed(() => {
    const discount = userStore.user?.discount ?? 1
    return items.value.reduce(
      (sum, item) => sum + item.price * item.qty, 0
    ) * discount
  })

  return { items, total }
})

组合式 Store 设计模式

// stores/composables/useEntityCRUD.js
// 通用的 CRUD Store 工厂
export function createCrudStore(entityName, apiEndpoint) {
  return defineStore(entityName, () => {
    const items = ref([])
    const current = ref(null)
    const loading = ref(false)

    async function fetchAll(params = {}) {
      loading.value = true
      try {
        const { data } = await api.get(apiEndpoint, { params })
        items.value = data
      } finally {
        loading.value = false
      }
    }

    async function fetchOne(id) {
      const { data } = await api.get(`${apiEndpoint}/${id}`)
      current.value = data
    }

    async function create(payload) {
      const { data } = await api.post(apiEndpoint, payload)
      items.value.unshift(data)
      return data
    }

    async function update(id, payload) {
      const { data } = await api.put(`${apiEndpoint}/${id}`, payload)
      const index = items.value.findIndex(i => i.id === id)
      if (index > -1) items.value[index] = data
      return data
    }

    async function remove(id) {
      await api.delete(`${apiEndpoint}/${id}`)
      items.value = items.value.filter(i => i.id !== id)
    }

    return { items, current, loading, fetchAll, fetchOne, create, update, remove }
  })
}

// 使用工厂创建具体的 Store
export const usePostStore = createCrudStore('posts', '/api/posts')
export const useCategoryStore = createCrudStore('categories', '/api/categories')

六、从 Vuex 迁移

对照转换

// === Vuex 4 ===
// store/index.js
export default createStore({
  state: () => ({
    count: 0
  }),
  mutations: {
    increment(state) {
      state.count++
    }
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => commit('increment'), 1000)
    }
  },
  getters: {
    double: (state) => state.count * 2
  }
})

// === Pinia 等价写法 ===
export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }),
  // mutation 直接变成 action
  actions: {
    increment() {
      this.count++
    },
    incrementAsync() {
      setTimeout(() => this.increment(), 1000)
    }
  },
  getters: {
    double: (state) => state.count * 2
  }
})
// === 组件中使用对比 ===

// Vuex
// this.$store.state.count
// this.$store.commit('increment')
// this.$store.dispatch('incrementAsync')
// mapState('counter', ['count'])
// mapGetters('counter', ['double'])

// Pinia
const counter = useCounterStore()
counter.count
counter.increment()        // 同步异步统一调用
counter.incrementAsync()
const { count } = storeToRefs(counter)

渐进式迁移策略

  1. 同时安装两个库,逐模块迁移
  2. 从叶子模块开始(无其他模块依赖的)
  3. 用组合函数桥接
// 桥接层:让旧 Vuex 代码访问新 Pinia
// stores/bridge.js
export function useCounterCompat() {
  const counter = useCounterStore()
  return {
    get count() { return counter.count },
    increment: counter.increment
  }
}
  1. 迁移完成移除 Vuex

七、SSR 中的注意事项

// Nuxt 3 中使用
// stores/counter.js
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  return { count }
})

// 组件中(Nuxt 自动导入 Pinia)
const counter = useCounterStore()

// SSR 中跨请求状态污染防范
// ✅ 在 setup 内调用 useStore(),而不是模块顶层
// ❌ const store = useCounterStore() // 模块顶层会共享状态

八、小结

Pinia 以极简的 API 提供了完整的状态管理能力:去掉 mutation 减少样板代码,扁平 Store 天然模块化,完整的 TypeScript 和 DevTools 支持。从 Vuex 迁移成本低,新项目直接上 Pinia 是唯一正确选择。


本文由 inspirecl.asia 原创,转载请注明出处。
0

评论 (0)

取消
0:00