从入门到实战:Pinia 完全指南(Vue3 状态管理新选择)

3 阅读12分钟

在 Vue3 项目开发中,状态管理是绕不开的话题。无论是简单的全局计数器,还是复杂的多组件数据共享,都需要一个高效、简洁的状态管理方案。而 Pinia,作为 Vue 官方推荐的状态管理库,凭借其简洁的 API、完善的 TypeScript 支持和对 Vue3 Composition API 的完美适配,逐渐取代 Vuex 成为主流选择。

今天这篇博客,将从新手视角出发,带你全面认识 Pinia:它是什么、为什么要使用它、如何快速上手,以及实战中如何运用它解决实际问题,全程干货,新手也能轻松拿捏!

一、Pinia 是什么?

Pinia(发音 /ˈpiːnjə/)是 Vue 官方团队开发的状态管理库,于 2021 年正式发布,旨在替代 Vuex(Vue2 时代的官方状态管理库),成为 Vue3 生态中首选的状态管理方案。

简单来说,Pinia 就是一个“全局数据仓库”,可以将项目中需要跨组件共享的数据(比如用户信息、主题设置、购物车数据等)统一管理起来,让不同组件都能轻松访问和修改这些数据,避免了组件间繁琐的 props 传值、事件派发,让代码更简洁、可维护。

官方对 Pinia 的定位是:“Vue 的存储库,用于跨组件共享状态”,它的核心设计理念是 简洁、高效、类型安全,彻底解决了 Vuex 中模块化繁琐、TypeScript 支持不佳、API 冗余等痛点。

二、为什么选择 Pinia?(对比 Vuex 的优势)

如果你用过 Vuex,可能会对它的 mutations、actions、modules、getters 分层感到繁琐,尤其是在 TypeScript 项目中,类型定义更是让人头疼。而 Pinia 则彻底简化了这些流程,带来了以下核心优势:

1. 极简 API,上手成本极低

Pinia 取消了 Vuex 中的 mutations(突变),只保留了 state、getters、actions 三个核心概念,代码结构更简洁。不需要像 Vuex 那样嵌套 modules,一个 store 就是一个独立的模块,定义和使用都非常直观。

举个直观对比:Vuex 中修改状态需要通过 mutations(同步)或 actions(异步),而 Pinia 中可以直接在 actions 中同步或异步修改 state,无需额外的 mutations 层,减少了代码冗余。

2. 完善的 TypeScript 支持

Pinia 是基于 TypeScript 开发的,天生对 TypeScript 有完美支持,不需要像 Vuex 那样编写大量的类型声明文件,就能实现自动类型推导。无论是定义 state、调用 actions,还是访问 getters,都能获得清晰的类型提示,有效避免类型错误,提升开发效率。

3. 与 Vue3 Composition API 完美适配

Pinia 可以无缝集成 Vue3 的 Composition API(如 ref、reactive、computed 等),你可以在 store 中直接使用 Composition API 编写逻辑,也可以在组件中通过 setup 语法轻松访问 store,代码风格统一,更符合 Vue3 的开发习惯。

4. 无模块嵌套,灵活的模块化管理

Vuex 中,当项目规模较大时,需要通过 modules 嵌套来划分模块,容易出现命名冲突、代码结构臃肿的问题。而 Pinia 中,每个 store 都是一个独立的模块,不需要嵌套,通过 import 即可跨模块访问,模块化更灵活,维护成本更低。

5. 轻量体积,性能优秀

Pinia 的体积非常小(仅约 1KB),没有多余的依赖,加载速度快。同时,它对 Vue3 的响应式系统进行了优化,状态更新时只会触发相关组件的重新渲染,性能优于 Vuex。

6. 官方推荐,生态完善

Pinia 是 Vue 官方推荐的状态管理库,与 Vue Router、Vue DevTools 等工具深度集成,支持时间旅行调试、状态快照等功能,开发体验更佳。同时,社区活跃,遇到问题能快速找到解决方案。

三、Pinia 快速上手(环境搭建 + 基础使用)

接下来,我们通过一个简单的案例,快速掌握 Pinia 的基础使用流程。本次演示基于 Vue3 + TypeScript + Vite 环境,如果你使用的是 Vue3 + JavaScript 环境,操作基本一致,只是去掉类型声明即可。

1. 安装 Pinia

首先,在 Vue3 项目中安装 Pinia:

# npm 安装
npm install pinia

# yarn 安装
yarn add pinia

# pnpm 安装(推荐)
pnpm add pinia

2. 初始化 Pinia(全局注册)

安装完成后,需要在 main.ts 中初始化 Pinia,并将其挂载到 Vue 实例上:

// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia' // 导入 createPinia
import App from './App.vue'

const app = createApp(App)
const pinia = createPinia() // 创建 Pinia 实例

app.use(pinia) // 挂载 Pinia 到 Vue 实例
app.mount('#app')

这一步非常简单,只需导入 createPinia,创建实例后通过 app.use() 挂载即可,无需额外配置。

3. 创建第一个 Pinia Store

Pinia 中,每个 store 都是一个独立的模块,我们可以在 src 目录下创建一个 stores 文件夹,专门存放所有的 store 文件。

例如,创建一个管理用户信息的 store(src/stores/userStore.ts):

// src/stores/userStore.ts
import { defineStore } from 'pinia' // 导入 defineStore 用于定义 store

// 定义并导出 store,第一个参数是 store 的唯一标识(必须唯一),第二个参数是配置对象
export const useUserStore = defineStore('user', {
  // state:存储全局状态,类似组件中的 data
  state: () => ({
    name: '张三',
    age: 20,
    isLogin: false,
    token: ''
  }),
  // getters:计算属性,类似组件中的 computed,用于对 state 进行加工处理
  getters: {
    // 计算用户是否成年
    isAdult: (state) => state.age >= 18,
    // 可以访问其他 getters
    userInfo: (state) => {
      return {
        name: state.name,
        age: state.age,
        isAdult: state.age >= 18
      }
    }
  },
  // actions:用于修改 state,支持同步和异步操作,类似组件中的 methods
  actions: {
    // 同步修改用户信息
    updateUserInfo(name: string, age: number) {
      this.name = name // 直接通过 this 访问 state 中的属性
      this.age = age
    },
    // 异步操作(例如模拟接口请求登录)
    async login(token: string) {
      // 模拟接口请求延迟
      await new Promise(resolve => setTimeout(resolve, 1000))
      this.token = token
      this.isLogin = true
    },
    // 退出登录
    logout() {
      this.token = ''
      this.isLogin = false
    }
  }
})

核心说明:

  • defineStore:Pinia 用于定义 store 的核心函数,第一个参数是 store 的唯一 ID(字符串),必须保证整个项目中唯一,否则会出现状态冲突;
  • state:返回一个函数,函数内部返回存储的状态数据,这样可以避免多个实例共享同一状态;
  • getters:对象形式,每个属性都是一个函数,参数是 state,返回加工后的值,支持访问其他 getters;
  • actions:对象形式,每个属性都是一个函数,支持同步和异步操作,通过 this 可以直接访问和修改 state 中的属性,无需像 Vuex 那样通过 commit 提交 mutations。

4. 在组件中使用 Store

创建好 store 后,就可以在任意组件中导入并使用它了。Vue3 中推荐使用 setup 语法,使用起来非常简洁:

<template>
  <div class="user-info">
    <h3>用户信息</h3>
    <p>姓名:{{ userStore.name }}</p>
    <p>年龄:{{ userStore.age }}</p>
    <p>是否成年:{{ userStore.isAdult ? '是' : '否' }}</p>
    <p>登录状态:{{ userStore.isLogin ? '已登录' : '未登录' }}</p>
    
    <button @click="updateUser">修改用户信息</button>
    <button @click="login" v-if="!userStore.isLogin">登录</button>
    <button @click="logout" v-if="userStore.isLogin">退出登录</button>
  </div>
</template>

<script lang="ts" setup>
// 导入创建好的 store
import { useUserStore } from '@/stores/userStore'

// 实例化 store(必须实例化后才能使用)
const userStore = useUserStore()

// 调用 store 中的 actions 修改状态
const updateUser = () => {
  userStore.updateUserInfo('李四', 22)
}

const login = () => {
  userStore.login('token123456') // 模拟登录,传入 token
}

const logout = () => {
  userStore.logout()
}
</script>

效果说明:点击“修改用户信息”,会同步更新用户的姓名和年龄;点击“登录”,会模拟接口请求,1秒后更新登录状态和 token;点击“退出登录”,会重置登录状态和 token,所有使用该 store 的组件都会自动响应状态变化。

四、Pinia 核心特性详解

上面的基础案例已经能满足大部分简单场景的需求,但 Pinia 还有很多强大的特性,接下来我们逐一详解。

1. State 详解

(1)直接修改 State

Pinia 允许直接修改 state 中的属性,无需像 Vuex 那样通过 mutations,非常灵活:

// 直接修改单个属性
userStore.name = '王五'

// 直接修改多个属性
userStore.$patch({
  name: '王五',
  age: 25,
  isLogin: true
})

// 函数式修改(适合复杂逻辑)
userStore.$patch((state) => {
  state.age += 1
  state.token = 'newToken789'
})

推荐使用 $patch 方法修改多个属性,这样可以减少状态更新的次数,提升性能。

(2)重置 State

如果需要将 state 重置为初始值,可以使用 store 的 $reset() 方法:

userStore.$reset() // 所有 state 都会恢复到定义时的初始值

2. Getters 详解

Getters 用于对 state 进行加工处理,类似组件中的 computed,具有缓存特性——只有当依赖的 state 发生变化时,getters 才会重新计算。

(1)访问其他 Getters

在 getters 中,可以通过 this 访问其他 getters(注意:不能使用箭头函数,否则 this 会丢失):

getters: {
  isAdult: (state) => state.age >= 18,
  // 访问其他 getters
  userDesc: function(state) {
    return `${state.name}${this.isAdult ? '成年' : '未成年'}`
  }
}

(2)传递参数给 Getters

Getters 本身不能直接传递参数,但可以返回一个函数,通过函数传递参数:

getters: {
  // 返回一个函数,接收参数
  getAgeRange: (state) => (min: number, max: number) => {
    return state.age >= min && state.age <= max
  }
}

// 组件中使用
const isTeen = userStore.getAgeRange(13, 17)

3. Actions 详解

Actions 是修改 state 的唯一推荐方式(虽然可以直接修改 state,但在实际开发中,建议将所有修改 state 的逻辑放在 actions 中,便于维护和调试),支持同步和异步操作。

(1)异步 Actions

Actions 中可以使用 async/await 语法,处理异步逻辑(如接口请求),修改 state 时直接通过 this 操作即可:

actions: {
  // 模拟请求用户信息接口
  async fetchUserInfo() {
    try {
      const res = await fetch('/api/user/info')
      const data = await res.json()
      // 异步修改 state
      this.name = data.name
      this.age = data.age
      this.token = data.token
      this.isLogin = true
    } catch (err) {
      console.error('获取用户信息失败:', err)
    }
  }
}

// 组件中使用
await userStore.fetchUserInfo()

(2)调用其他 Actions

在一个 action 中,可以通过 this 调用其他 action,实现逻辑复用:

actions: {
  updateUserInfo(name: string, age: number) {
    this.name = name
    this.age = age
  },
  // 调用其他 action
  async fetchAndUpdateUser() {
    const res = await fetch('/api/user/info')
    const data = await res.json()
    this.updateUserInfo(data.name, data.age) // 调用当前 store 的 action
  }
}

4. 跨 Store 访问

当项目中有多个 store 时,一个 store 可以访问另一个 store 的 state、getters、actions,只需在当前 store 中导入并实例化目标 store 即可:

// src/stores/cartStore.ts
import { defineStore } from 'pinia'
import { useUserStore } from './userStore' // 导入其他 store

export const useCartStore = defineStore('cart', {
  state: () => ({
    goods: []
  }),
  actions: {
    // 访问 userStore 的状态和方法
    addToCart(goods) {
      const userStore = useUserStore() // 实例化目标 store
      if (userStore.isLogin) { // 访问 userStore 的 state
        this.goods.push(goods)
      } else {
        alert('请先登录!')
        userStore.login('') // 调用 userStore 的 action
      }
    }
  }
})

五、Pinia 实战案例(购物车功能)

为了让大家更好地掌握 Pinia 的实际应用,我们来实现一个常见的购物车功能,涵盖 state、getters、actions 的核心用法,以及跨组件共享状态。

1. 创建购物车 Store(src/stores/cartStore.ts)

import { defineStore } from 'pinia'

// 定义商品类型
interface Goods {
  id: number
  name: string
  price: number
  count: number
  checked: boolean
}

export const useCartStore = defineStore('cart', {
  state: () => ({
    goodsList: [] as Goods[] // 购物车商品列表
  }),
  getters: {
    // 购物车商品总数
    totalCount: (state) => state.goodsList.reduce((total, goods) => total + goods.count, 0),
    // 购物车商品总价
    totalPrice: (state) => state.goodsList.reduce((total, goods) => total + goods.price * goods.count, 0),
    // 选中的商品列表
    checkedGoods: (state) => state.goodsList.filter(goods => goods.checked),
    // 选中商品的总价
    checkedPrice: (state) => state.goodsList.filter(goods => goods.checked).reduce((total, goods) => total + goods.price * goods.count, 0)
  },
  actions: {
    // 添加商品到购物车(如果已存在,增加数量)
    addGoods(goods: Omit<Goods, 'count' | 'checked'>) {
      const existingGoods = this.goodsList.find(item => item.id === goods.id)
      if (existingGoods) {
        existingGoods.count += 1
      } else {
        this.goodsList.push({ ...goods, count: 1, checked: true })
      }
    },
    // 修改商品数量
    updateGoodsCount(id: number, count: number) {
      const goods = this.goodsList.find(item => item.id === id)
      if (goods) {
        goods.count = count < 1 ? 1 : count // 数量不能小于1
      }
    },
    // 切换商品选中状态
    toggleGoodsChecked(id: number) {
      const goods = this.goodsList.find(item => item.id === id)
      if (goods) {
        goods.checked = !goods.checked
      }
    },
    // 全选/取消全选
    toggleAllChecked(checked: boolean) {
      this.goodsList.forEach(goods => {
        goods.checked = checked
      })
    },
    // 删除购物车商品
    deleteGoods(id: number) {
      this.goodsList = this.goodsList.filter(item => item.id !== id)
    },
    // 清空购物车
    clearCart() {
      this.goodsList = []
    }
  }
})

2. 在组件中使用购物车 Store

(1)商品列表组件(添加商品到购物车)

<template>
  <div class="goods-list">
    <div class="goods-item" v-for="goods in goodsList" :key="goods.id">
      <h4>{{ goods.name }}</h4>
      <p>价格:{{ goods.price }} 元</p>
      <button @click="addToCart(goods)">加入购物车</button>
    </div>
  </div>
</template>

<script lang="ts" setup>
import { useCartStore } from '@/stores/cartStore'

const cartStore = useCartStore()

// 模拟商品列表
const goodsList = [
  { id: 1, name: 'Vue3 实战教程', price: 99 },
  { id: 2, name: 'Pinia 入门指南', price: 69 },
  { id: 3, name: 'TypeScript 进阶', price: 89 }
]

// 加入购物车
const addToCart = (goods) => {
  cartStore.addGoods(goods)
  alert('加入购物车成功!')
}
</script>

(2)购物车组件(展示购物车、修改数量、删除商品等)

<template>
  <div class="cart">
    <h3>我的购物车</h3>
    <div class="cart-empty" v-if="cartStore.goodsList.length === 0">
      购物车为空,快去添加商品吧!
    </div>
    <div class="cart-list" v-else>
      <div class="cart-item" v-for="goods in cartStore.goodsList" :key="goods.id">
        <input type="checkbox" v-model="goods.checked" @change="cartStore.toggleGoodsChecked(goods.id)">
        <span>{{ goods.name }}</span>
        <span>{{ goods.price }} 元</span>
        <div class="count-btn">
          <button @click="cartStore.updateGoodsCount(goods.id, goods.count - 1)">-</button>
          <span>{{ goods.count }}</span>
          <button @click="cartStore.updateGoodsCount(goods.id, goods.count + 1)">+</button>
        </div>
        <button @click="cartStore.deleteGoods(goods.id)" class="delete-btn">删除</button>
      </div>
      <div class="cart-footer">
        <input type="checkbox" v-model="allChecked" @change="cartStore.toggleAllChecked(allChecked)">
        <span>全选</span>
        <span>商品总数:{{ cartStore.totalCount }}</span>
        <span>选中总价:{{ cartStore.checkedPrice.toFixed(2) }} 元</span>
        <button @click="cartStore.clearCart" class="clear-btn">清空购物车</button>
      </div>
    </div>
  </div>
</template>

<script lang="ts" setup>
import { useCartStore } from '@/stores/cartStore'
import { computed } from 'vue'

const cartStore = useCartStore()

// 全选状态(计算属性,同步选中商品的状态)
const allChecked = computed({
  get() {
    return cartStore.goodsList.length > 0 && cartStore.goodsList.every(goods => goods.checked)
  },
  set(checked) {
    cartStore.toggleAllChecked(checked)
  }
})
</script>

这个实战案例涵盖了购物车的核心功能,通过 Pinia 统一管理购物车状态,实现了商品添加、数量修改、选中状态切换、全选、删除、清空等功能,并且状态在不同组件间同步更新,代码简洁、可维护。

六、Pinia 常见问题与注意事项

1. 如何在 Options API 中使用 Pinia?

虽然 Pinia 推荐配合 Composition API 使用,但也支持 Options API(Vue2 风格),只需通过 mapStores、mapState、mapActions 等辅助函数即可:

<script>
import { mapStores, mapState, mapActions } from 'pinia'
import { useUserStore } from '@/stores/userStore'

export default {
  computed: {
    ...mapStores(useUserStore), // 映射整个 store
    ...mapState(useUserStore, ['name', 'age']), // 映射 state 中的属性
  },
  methods: {
    ...mapActions(useUserStore, ['updateUserInfo', 'login']) // 映射 actions 中的方法
  }
}
</script>

2. Pinia 如何持久化存储?

Pinia 本身不支持持久化存储(刷新页面后 state 会重置),如果需要实现持久化(如刷新后保留购物车、用户信息),可以使用 pinia-plugin-persistedstate 插件:

# 安装插件
pnpm add pinia-plugin-persistedstate
// main.ts 中配置插件
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate) // 安装持久化插件

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

然后在定义 store 时,添加 persist: true 即可实现持久化:

export const useUserStore = defineStore('user', {
  state: () => ({ /* ... */ }),
  getters: { /* ... */ },
  actions: { /* ... */ },
  persist: true // 开启持久化,默认存储在 localStorage
})

3. 如何调试 Pinia 状态?

Pinia 与 Vue DevTools 深度集成,只需安装最新版的 Vue DevTools,即可在调试面板中查看 Pinia 的 state、getters、actions,支持时间旅行调试(回退到之前的状态),非常方便。

4. 注意事项

  • store 的唯一 ID 必须全局唯一,否则会出现状态冲突;
  • state 必须是一个函数,返回一个对象,这样可以避免多个实例共享同一状态;
  • 虽然可以直接修改 state,但建议将所有修改 state 的逻辑放在 actions 中,便于维护和调试;
  • 在 TypeScript 项目中,建议给 state、actions 的参数添加类型声明,提升开发体验;
  • 跨 store 访问时,需要在当前 store 中导入并实例化目标 store,不能直接使用未实例化的 store。

七、总结

Pinia 作为 Vue3 官方推荐的状态管理库,凭借其简洁的 API、完善的 TypeScript 支持、与 Composition API 的完美适配,以及轻量高效的特点,成为了 Vue3 项目状态管理的首选方案。

本文从 Pinia 的基本概念、核心优势出发,逐步讲解了 Pinia 的环境搭建、基础使用、核心特性,最后通过一个购物车实战案例,帮助大家掌握 Pinia 在实际项目中的应用。无论是新手还是有 Vuex 经验的开发者,都能快速上手 Pinia,用它来解决项目中的状态管理问题。

如果你正在开发 Vue3 项目,还在纠结使用什么状态管理库,不妨试试 Pinia,它一定会给你带来简洁、高效的开发体验!

最后,附上 Pinia 官方文档地址,有需要的可以查阅更多细节:Pinia 官方文档