Vue 状态管理选型:Pinia 完整实战,对比 Vuex,模块化持久化

Hi,我是前端人类学

在 Vue 应用中,状态管理从"多组件共享数据"到"复杂业务逻辑协调",常常是项目架构中最具挑战性的一环。随着 Vue3 的普及,Pinia 已取代 Vuex 成为官方推荐的状态管理库。

本文将从前端状态管理的核心问题出发,通过完整的实战案例,对比 Vuex 与 Pinia 的差异,并深入讲解模块化设计与数据持久化的最佳实践。

文章目录

    • [一、Vuex vs Pinia:为什么官方转向了 Pinia?](#一、Vuex vs Pinia:为什么官方转向了 Pinia?)
      • [1.1 核心差异一览](#1.1 核心差异一览)
      • [1.2 Pinia 的核心优势](#1.2 Pinia 的核心优势)
      • [1.3 何时仍用 Vuex?](#1.3 何时仍用 Vuex?)
    • [二、Pinia 完整实战:从零构建状态管理](#二、Pinia 完整实战:从零构建状态管理)
      • [2.1 安装与初始化](#2.1 安装与初始化)
      • [2.2 创建第一个 Store(Options Store 风格)](#2.2 创建第一个 Store(Options Store 风格))
      • [2.3 在组件中使用](#2.3 在组件中使用)
      • [2.4 Setup Store 风格(推荐)](#2.4 Setup Store 风格(推荐))
    • 三、模块化设计:组织大型应用的状态
      • [3.1 推荐的目录结构](#3.1 推荐的目录结构)
      • [3.2 跨模块引用](#3.2 跨模块引用)
      • [3.3 模块组合与统一导出](#3.3 模块组合与统一导出)
    • 四、数据持久化:刷新不丢失
      • [4.1 安装与配置](#4.1 安装与配置)
      • [4.2 Store 级配置](#4.2 Store 级配置)
      • [4.3 精细控制](#4.3 精细控制)
      • [4.4 其他持久化方案](#4.4 其他持久化方案)
    • 五、最佳实践总结
      • [5.1 核心实践](#5.1 核心实践)
      • [5.2 性能注意事项](#5.2 性能注意事项)

一、Vuex vs Pinia:为什么官方转向了 Pinia?

1.1 核心差异一览

对比维度 Vuex Pinia
语法复杂度 需定义 state/mutations/actions/getters,样板代码多 直接使用 actions 修改 state,无需 mutations
TypeScript 支持 需手动声明类型,繁琐易错 自动类型推导,天然友好
模块化 需手动配置 modules 嵌套,存在命名空间问题 每个 store 独立,天然模块化
性能 Object.defineProperty(Vue2)或 Proxy,中间件开销约 10-20% 基于 Vue3 Proxy,更精准的依赖追踪,性能提升约 30-40%
异步处理 需 action + mutation 组合,流程繁琐 action 直接返回 Promise,优雅简洁

1.2 Pinia 的核心优势

语法极简 :在 Vuex 中定义一个计数器需要编写 state、mutations、actions 等多个部分;而 Pinia 只需一个 defineStore,在 actions 中直接修改 this.count 即可。

TypeScript 一等公民:Pinia 在设计之初就充分考虑了对 TypeScript 的支持,几乎所有 API 都能自动推导类型,大幅减少手动类型声明的工作量。

轻量且性能优秀:在同样执行 1000 次同步 action 的场景下,Pinia 的延迟约为 5-8ms,而 Vuex 为 8-12ms,提升约 30%。高频触发场景(1000次/秒)下,Pinia 的吞吐量更是优于 Vuex 约 40-50%。

1.3 何时仍用 Vuex?

  • 项目为 Vue2 且已深度使用 Vuex,迁移成本过高
  • 依赖较复杂的 Vuex 中间件生态,如 vuex-persistedstate 的特殊用法
  • 对 Vuex 的严格单向数据流规范有强依赖

对于新项目,尤其是 Vue3 项目,Pinia 无疑是更优的选择。

二、Pinia 完整实战:从零构建状态管理

2.1 安装与初始化

bash 复制代码
pnpm add pinia
# 或 npm install pinia

main.ts 中注册 Pinia:

typescript 复制代码
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.2 创建第一个 Store(Options Store 风格)

typescript 复制代码
// stores/counter.ts
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: '计数器'
  }),
  getters: {
    // 自动推导返回类型
    doubleCount: (state) => state.count * 2,
    // 使用 this 访问其他 getter
    formattedCount(): string {
      return `${this.name}: ${this.count}`
    }
  },
  actions: {
    increment() {
      this.count++
    },
    incrementBy(amount: number) {
      this.count += amount
    },
    // 异步 action 直接返回 Promise
    async fetchInitialCount() {
      const res = await api.get('/count')
      this.count = res.data
    }
  }
})

2.3 在组件中使用

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

const counterStore = useCounterStore()

// ✅ 使用 storeToRefs 解构 state 和 getters,保持响应性
const { count, doubleCount, formattedCount } = storeToRefs(counterStore)

// ❌ 直接解构会丢失响应性
// const { count, doubleCount } = counterStore

// ✅ actions 可以直接解构
const { increment, incrementBy } = counterStore
</script>

<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Double: {{ doubleCount }}</p>
    <p>{{ formattedCount }}</p>
    <button @click="increment()">+1</button>
    <button @click="incrementBy(5)">+5</button>
  </div>
</template>

2.4 Setup Store 风格(推荐)

Pinia 同时支持 Composition API 风格的 Store,更灵活且与 Vue3 生态融合更好:

typescript 复制代码
// stores/user.ts
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'

export const useUserStore = defineStore('user', () => {
  // state
  const userInfo = ref<UserInfo | null>(null)
  const token = ref('')

  // getters
  const isLoggedIn = computed(() => !!token.value)
  const userName = computed(() => userInfo.value?.username || '游客')

  // actions
  function setToken(newToken: string) {
    token.value = newToken
  }

  async function login(credentials: LoginParams) {
    const res = await api.login(credentials)
    token.value = res.token
    userInfo.value = res.user
  }

  function logout() {
    token.value = ''
    userInfo.value = null
  }

  return { userInfo, token, isLoggedIn, userName, setToken, login, logout }
})

三、模块化设计:组织大型应用的状态

随着应用规模增长,将状态按业务领域拆分是保持代码可维护性的关键。Pinia 天然支持模块化------每个 defineStore 定义的就是一个独立的模块。

3.1 推荐的目录结构

text 复制代码
stores/
├── index.ts              # Pinia 实例导出
├── modules/
│   ├── user.ts           # 用户模块
│   ├── cart.ts           # 购物车模块
│   ├── app.ts            # 应用全局配置
│   └── permission.ts     # 权限模块
└── types/
    └── index.ts          # 类型定义

3.2 跨模块引用

不同 Store 之间可以相互引用,只需在 action 中调用其他 Store 的实例:

typescript 复制代码
// stores/modules/cart.ts
import { defineStore } from 'pinia'
import { useUserStore } from './user'

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [] as CartItem[]
  }),
  actions: {
    async checkout() {
      const userStore = useUserStore()
      if (!userStore.isLoggedIn) {
        throw new Error('请先登录')
      }
      // 执行下单逻辑...
    }
  }
})

3.3 模块组合与统一导出

stores/index.ts 中统一导出所有模块:

typescript 复制代码
// stores/index.ts
import { createPinia } from 'pinia'

const pinia = createPinia()
export default pinia

export * from './modules/user'
export * from './modules/cart'
export * from './modules/app'

四、数据持久化:刷新不丢失

页面刷新后状态重置是单页应用最常见的问题之一。Pinia 本身不提供持久化能力,但可通过插件轻松实现。最成熟方案是 pinia-plugin-persistedstate,它受到 vuex-persistedstate 启发,API 简洁且功能完备。

4.1 安装与配置

bash 复制代码
pnpm add pinia-plugin-persistedstate

main.ts 中注册:

typescript 复制代码
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

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

4.2 Store 级配置

在需要持久化的 Store 中添加 persist 选项:

typescript 复制代码
// stores/user.ts
export const useUserStore = defineStore('user', {
  state: () => ({
    token: '',
    userInfo: null
  }),
  actions: { /* ... */ },
  persist: {
    key: 'user-storage',      // 存储 key,默认使用 store id
    storage: localStorage,    // 默认 localStorage
    pick: ['token', 'userInfo'] // 仅持久化指定字段
  }
})

4.3 精细控制

typescript 复制代码
// 使用 sessionStorage,且排除敏感字段
persist: {
  storage: sessionStorage,
  omit: ['tempData'],   // 排除不需要持久化的字段
}

// 在 Setup Store 中使用
export const useCounterStore = defineStore(
  'counter',
  () => {
    const count = ref(0)
    return { count }
  },
  {
    persist: true   // 简单启用全部持久化
  }
)

4.4 其他持久化方案

  • @erlihs/pinia-plugin-storage:支持 localStorage、sessionStorage、cookies、indexedDB 多适配器,并具备跨标签页实时同步、防抖、命名空间等高级特性
  • pinia-plugin-persist-uni:专为 uni-app 设计的持久化方案,采用 uniAppStorage 存储

五、最佳实践总结

5.1 核心实践

实践 说明
模块化拆分 按业务领域拆分 Store,避免单一 Store 膨胀
使用 storeToRefs 解构 state/getters 时保持响应性
Actions 修改 State 即使 Pinia 允许直接修改,仍建议通过 action 统一变更,便于调试
Getters 计算派生状态 避免在模板中编写复杂逻辑
合理持久化 仅持久化必要字段,避免存储敏感信息和大数据

5.2 性能注意事项

  • Pinia 基于 Vue3 的 Proxy 响应式系统,在深度嵌套对象和大规模状态变更场景下,性能显著优于 Vuex(约 20-50%)
  • 单个 Store 的 state 不宜过大,否则会影响依赖追踪效率
  • 高频触发场景下,Pinia 的吞吐量比 Vuex 高约 40-50%

Pinia 凭借简洁的语法、优秀的 TypeScript 支持和灵活的设计,已成为 Vue 生态中状态管理的事实标准。从 Vuex 到 Pinia 的迁移不仅是技术升级,更是开发体验和工程效率的跃升。在实际项目中,结合模块化设计与数据持久化方案,Pinia 能够胜任从中小型应用到大型复杂系统的所有场景。

相关推荐
神王宝宝 王者小学1 小时前
面向领域驱动架构的查询实现方式
前端·python·架构
小林ixn1 小时前
React Router 从入门到实战:一篇搞定路由配置、懒加载与嵌套路由
前端·react.js·前端框架
labixiong1 小时前
async/await 到底是不是 Generator 的语法糖?手写执行器,Babel 编译产物里藏着答案
前端·javascript·babel
windliang1 小时前
Claude Code 源码分析(六):上下文的发现、注入与压缩
前端·javascript·人工智能
张龙6871 小时前
10 万条数据不卡顿:不定高虚拟列表从原理到生产实现
前端·javascript·性能优化
小锋java12342 小时前
【技术专题】Vue3 - 条件渲染
vue.js·vite
玉鸯2 小时前
界面用完即消失:Agent 生成式 UI 的短暂性哲学与前端工程的未来
前端·llm·agent
妙码生花2 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(五十四):管理员个人资料页面、管理员日志优化
前端·后端·go