vue3 Pinia 全解析:从入门到实战。

1 阅读12分钟

在 Vue3 生态中,Pinia 已经成为官方推荐的状态管理库,彻底替代了 Vue2 时代的 Vuex。相比于 Vuex,Pinia 移除了 mutations、modules 等繁琐概念,简化了语法,同时完美适配 Vue3 组合式 API,支持 TypeScript 类型推断,上手成本极低,成为中小型项目乃至大型项目的首选状态管理方案。

本文将从新手视角出发,用「通俗讲解 + 可直接复制的实战代码」,覆盖 Pinia 从环境搭建、基础使用(定义Store、存取状态、修改状态),到进阶技巧(Getters、Actions、持久化),再到实战场景的全流程,同时兼容非TS和TS两种写法,新手看完就能上手,老手也能查漏补缺,彻底掌握 Pinia 的核心用法。

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

在学习 Pinia 之前,先搞清楚一个问题:为什么 Vue3 官方推荐用 Pinia 替代 Vuex?核心优势总结为 5 点,看完你就明白它有多香:

  1. 语法更简洁:移除了 Vuex 中繁琐的 mutations(提交修改)、modules(模块)嵌套,直接用 Actions 修改状态,代码量减少 30%+;
  2. 完美适配组合式 API:和 Vue3
  3. 原生支持 TypeScript:自带类型推断,无需手动编写大量类型声明,TS 开发体验拉满(非TS项目也能正常使用);
  4. 无需手动注册:创建 Store 后可直接在组件中使用,无需像 Vuex 那样在 main.js 中注册 Store 实例;
  5. 轻量无依赖:体积极小(仅 1KB 左右),无需引入额外依赖,不增加项目负担。

一句话总结:Pinia 就是 Vue3 时代「更简单、更高效、更友好」的 Vuex 替代方案,无论你是新手还是老手,都能快速上手。

二、环境搭建(Vue3 + Pinia,非TS/TS通用)

首先我们完成 Pinia 的环境搭建,分为「新项目初始化」和「已有项目集成」两种场景,步骤简单,全程复制命令即可。

1. 前提条件

确保你的项目是 Vue3 项目(Vue2 不支持 Pinia),如果是 Vue2 项目,需先升级到 Vue3,或继续使用 Vuex。

2. 安装 Pinia

打开终端,进入项目根目录,执行以下命令(npm 和 yarn 二选一):

# npm 安装(推荐)
npm install pinia

# yarn 安装
yarn add pinia

3. 初始化 Pinia(main.js 配置)

安装完成后,需要在 main.js 中创建 Pinia 实例,并挂载到 Vue 应用上,这一步是全局唯一的。

非TS版(Vue3)

// main.js
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')

TS版(Vue3 + TS )

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

const app = createApp(App)
app.use(createPinia()) // 可简化写法,无需单独定义 pinia 变量
app.mount('#app')

至此,Pinia 环境搭建完成,接下来就可以创建 Store,开始状态管理了。

三、Pinia 核心基础:定义Store(最关键一步)

在 Pinia 中,Store 是核心,可以理解为「一个全局的响应式数据容器」,用于存储和管理全局共享的状态(比如用户信息、购物车数据、主题配置等)。

每个 Store 都是独立的,互不干扰,我们可以根据业务需求,创建多个 Store(比如用户 Store、购物车 Store、设置 Store),实现状态的模块化管理(无需像 Vuex 那样嵌套 modules)。

1. Store 创建规范

推荐在项目根目录下创建 stores 文件夹(注意是复数),用于存放所有 Store 文件,每个 Store 单独创建一个 js/ts 文件,命名规范:xxxStore.js/ts(比如 userStore.js、cartStore.ts)。

2. 基础 Store 定义(非TS版,新手首选)

以「用户 Store」为例,创建 stores/userStore.js,代码如下(核心包含 3 部分:state、getters、actions):

// stores/userStore.js
import { defineStore } from 'pinia' // 引入 Pinia 内置的 defineStore

// 1. 定义并导出 Store(参数1:Store唯一标识,参数2:Store配置对象)
// 唯一标识(id):整个应用中唯一,不能重复,建议和文件名对应
export const useUserStore = defineStore('user', {
  // 2. state:存储全局状态(类似组件中的 data),返回一个对象
  state: () => ({
    username: '游客', // 初始用户名
    token: '', // 用户token
    isLogin: false, // 登录状态
    userInfo: {} // 用户详细信息(对象类型)
  }),

  // 3. getters:处理状态(类似组件中的 computed),用于对 state 进行计算或过滤
  getters: {
    // 示例1:简单计算(获取用户名的大写形式)
    upperUsername: (state) => {
      return state.username.toUpperCase()
    },
    // 示例2:依赖其他 getters(判断是否是管理员,假设 userInfo 中有 role 字段)
    isAdmin: (state, getters) => {
      // getters 可以访问当前 Store 中的其他 getters
      return state.userInfo.role === 'admin' && getters.upperUsername
    }
  },

  // 4. actions:修改状态(类似组件中的 methods),支持同步和异步
  actions: {
    // 示例1:同步修改状态(登录,修改用户信息)
    login(data) {
      // 直接修改 state 中的数据(无需像 Vuex 那样提交 mutation)
      this.username = data.username
      this.token = data.token
      this.isLogin = true
      this.userInfo = data.userInfo
    },

    // 示例2:异步修改状态(退出登录,模拟接口请求)
    async logout() {
      // 模拟接口请求(比如调用后端退出接口)
      await new Promise((resolve) => {
        setTimeout(() => {
          resolve()
        }, 500)
      })

      // 接口请求成功后,重置状态
      this.username = '游客'
      this.token = ''
      this.isLogin = false
      this.userInfo = {}
    },

    // 示例3:修改单个状态(单独修改用户名)
    updateUsername(name) {
      this.username = name
    }
  }
})

3. 基础 Store 定义(TS版,类型安全)

TS版会对 state、getters、actions 的参数和返回值进行类型约束,避免数据类型错误,代码如下:

// stores/userStore.ts
import { defineStore } from 'pinia'

// 定义 UserInfo 类型接口(约束 userInfo 的结构)
interface UserInfo {
  id?: number
  name?: string
  role?: 'admin' | 'user' // 角色只能是 admin 或 user
}

// 定义 State 类型接口(约束 state 的结构)
interface UserState {
  username: string
  token: string
  isLogin: boolean
  userInfo: UserInfo
}

// 定义并导出 Store
export const useUserStore = defineStore('user', {
  // 对 state 进行类型约束
  state: (): UserState => ({
    username: '游客',
    token: '',
    isLogin: false,
    userInfo: {}
  }),

  getters: {
    upperUsername: (state: UserState): string => {
      return state.username.toUpperCase()
    },
    isAdmin: (state: UserState, getters): boolean => {
      return state.userInfo.role === 'admin' && getters.upperUsername
    }
  },

  actions: {
    // 对参数 data 进行类型约束
    login(data: { username: string; token: string; userInfo: UserInfo }): void {
      this.username = data.username
      this.token = data.token
      this.isLogin = true
      this.userInfo = data.userInfo
    },

    // 异步 action,返回 Promise
    async logout(): Promise<void> {
      await new Promise((resolve) => {
        setTimeout(() => {
          resolve()
        }, 500)
      })

      this.username = '游客'
      this.token = ''
      this.isLogin = false
      this.userInfo = {}
    },

    updateUsername(name: string): void {
      this.username = name
    }
  }
})

核心细节说明(新手必看)

  • defineStore 是 Pinia 内置宏,无需导入额外依赖,第一个参数(id)必须全局唯一,否则会导致状态混乱;
  • state 必须是一个「函数」,返回一个对象,目的是避免多个组件复用 Store 时,出现状态污染;
  • getters 支持两种写法:箭头函数(推荐,简洁)和普通函数,箭头函数的第一个参数是 state,第二个参数是 getters(可访问当前 Store 的其他 getters);
  • actions 中可以直接通过 this 访问和修改 state 中的数据,支持同步和异步(async/await),无需像 Vuex 那样区分 mutations(同步)和 actions(异步);
  • Store 命名规范:函数名以 useXXXStore 开头(比如 useUserStore),符合 Vue3 组合式 API 的命名习惯。

四、Pinia 基础使用:组件中存取/修改状态

创建好 Store 后,就可以在任意组件中使用它了——核心步骤:引入 Store → 创建 Store 实例 → 存取/修改状态,步骤简单,无需注册,直接使用。

1. 组件中使用 Store(非TS版)

<template>
  <div class="demo">
    <h3>Pinia 组件使用示例(非TS版)</h3>
    <!-- 直接使用 state 中的数据 -->
    <p>当前用户:{{ userStore.username }}</p>
    <p>用户名大写:{{ userStore.upperUsername }}</p>
    <p>登录状态:{{ userStore.isLogin ? '已登录' : '未登录' }}</p>

    <!-- 调用 actions 中的方法,修改状态 -->
    <button @click="handleLogin">模拟登录</button>
    <button @click="handleLogout" style="margin-left: 10px;">模拟退出</button>
    <button @click="handleUpdateName" style="margin-left: 10px;">修改用户名</button>
  </div>
</template>

<script setup>
// 1. 引入创建好的 Store
import { useUserStore } from '@/stores/userStore'

// 2. 创建 Store 实例(必须调用 useUserStore(),不能直接赋值)
const userStore = useUserStore()

// 3. 调用 actions 中的方法,修改状态
const handleLogin = () => {
  // 模拟登录数据
  const loginData = {
    username: '掘金用户',
    token: '1234567890abcdef',
    userInfo: {
      id: 1001,
      name: '掘金用户',
      role: 'user'
    }
  }
  // 调用同步 action
  userStore.login(loginData)
}

const handleLogout = () => {
  // 调用异步 action(注意:异步方法需要加 await)
  userStore.logout()
}

const handleUpdateName = () => {
  // 调用 action 修改单个状态
  userStore.updateUsername('新的用户名')
}
</script>

2. 组件中使用 Store(TS版)

TS版和非TS版写法基本一致,唯一区别是 TS 会自动进行类型推断,无需手动声明类型,代码更安全:

<template>
  <div class="demo">
    <h3>Pinia 组件使用示例(TS版)</h3>
    <p>当前用户:{{ userStore.username }}</p>
    <p>用户名大写:{{ userStore.upperUsername }}</p>
    <button @click="handleLogin">模拟登录</button>
  </div>
</template>

<script setup lang="ts">
// 引入 Store(TS 会自动推断类型)
import { useUserStore } from '@/stores/userStore'

const userStore = useUserStore()

// 调用 action 时,TS 会自动校验参数类型
const handleLogin = () => {
  const loginData = {
    username: 'TS用户',
    token: 'ts123456',
    userInfo: {
      id: 1002,
      name: 'TS用户',
      role: 'admin' // 符合 UserInfo 接口的 role 类型
    }
  }
  userStore.login(loginData)
}
</script>

3. 状态修改的3种方式(实战常用)

Pinia 提供了 3 种修改 state 状态的方式,根据场景灵活选择,推荐优先使用前两种(符合规范):

方式1:调用 actions 中的方法(推荐,最规范)

这是最推荐的方式,将状态修改逻辑封装在 actions 中,便于维护和复用,尤其是复杂状态修改和异步操作:

// 组件中
const userStore = useUserStore()
// 调用 actions 方法修改状态
userStore.login(loginData)
userStore.updateUsername('新用户名')

方式2:直接修改 state 中的数据(简单场景可用)

如果是简单的单个状态修改,可直接通过 Store 实例修改,无需封装 actions(简化代码):

const userStore = useUserStore()
// 直接修改单个状态
userStore.username = '直接修改用户名'
userStore.isLogin = true
// 直接修改对象中的属性
userStore.userInfo.name = '修改用户姓名'

方式3:使用 $patch 批量修改状态(批量修改可用)

如果需要同时修改多个状态,使用$patch 方法,批量修改,代码更简洁:

const userStore = useUserStore()
// 批量修改状态(对象写法)
userStore.$patch({
  username: '批量修改用户名',
  isLogin: true,
  token: 'patch123456'
})

// 批量修改状态(函数写法,适合复杂逻辑)
userStore.$patch((state) => {
  state.username = '批量修改(函数写法)'
  state.userInfo.role = 'admin'
  state.isLogin = true
})

五、Pinia 进阶技巧:实战必备功能

掌握基础用法后,再学习几个进阶技巧,覆盖实际开发中的高频需求,让 Pinia 用起来更高效。

1. Getters 进阶:缓存与依赖

Pinia 的 getters 具有「缓存特性」——只要依赖的 state 数据不变化,getters 的计算结果就会被缓存,多次访问不会重复计算,提升性能。

// stores/userStore.js
getters: {
  // 缓存示例:只有 state.username 变化时,才会重新计算
  upperUsername: (state) => {
    console.log('getters 计算执行了') // 仅在 username 变化时打印
    return state.username.toUpperCase()
  },

  // 依赖其他 Store 的 getters(跨 Store 访问)
  // 假设还有一个 settingStore,用于存储主题配置
  themeAndUser: (state, getters) => {
    const settingStore = useSettingStore() // 引入其他 Store
    return `${getters.upperUsername} - ${settingStore.theme}`
  }
}

2. Actions 进阶:异步操作与跨 Store 调用

Actions 支持 async/await 异步操作(比如调用后端接口),同时也能调用其他 Store 的 actions 或修改其他 Store 的状态,实现跨 Store 交互。

// stores/userStore.js
import { useCartStore } from './cartStore' // 引入其他 Store

actions: {
  // 异步 action:调用后端登录接口(实战常用)
  async loginByApi(formData) {
    try {
      // 调用后端登录接口(模拟)
      const res = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify(formData)
      })
      const data = await res.json()

      // 登录成功后,修改当前 Store 状态
      this.username = data.username
      this.token = data.token
      this.isLogin = true

      // 跨 Store 调用:调用 cartStore 的 actions(比如同步购物车数据)
      const cartStore = useCartStore()
      cartStore.syncCart(data.token) // 传递 token,同步购物车

      return data // 可返回数据,供组件使用
    } catch (err) {
      console.error('登录失败:', err)
      throw err // 抛出错误,供组件捕获处理
    }
  }
}

3. 状态持久化(实战必备)

Pinia 的状态默认存储在内存中,页面刷新后会丢失(比如登录状态、购物车数据),因此需要做「状态持久化」——将状态存储到 localStorage/sessionStorage 中,页面刷新后自动恢复。

推荐使用第三方插件 pinia-plugin-persistedstate,配置简单,一键实现持久化。

步骤1:安装插件

npm install pinia-plugin-persistedstate

# 或 yarn 安装
yarn add pinia-plugin-persistedstate

步骤2:main.js 中配置插件

// main.js(非TS版)
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' // 引入插件
import App from './App.vue'

const app = createApp(App)
const pinia = createPinia()

pinia.use(piniaPluginPersistedstate) // 挂载持久化插件
app.use(pinia)
app.mount('#app')

步骤3:给指定 Store 开启持久化

在 defineStore 中添加 persist: true,即可开启当前 Store 的持久化(默认存储到 localStorage):

// stores/userStore.js
export const useUserStore = defineStore('user', {
  state: () => ({ /* ... */ }),
  getters: { /* ... */ },
  actions: { /* ... */ },
  persist: true // 开启持久化,页面刷新后状态不丢失
})

进阶配置:自定义持久化规则

可自定义持久化方式(localStorage/sessionStorage)、存储的键名、需要持久化的状态字段:

persist: {
  key: 'userStore', // 存储到本地的键名(默认是 Store 的 id)
  storage: sessionStorage, // 存储方式:sessionStorage(页面关闭后丢失)
  paths: ['username', 'token', 'isLogin'] // 只持久化这3个字段,其他字段不持久化
}

4. 解构 Store 数据(避免响应式丢失)

如果直接解构 Store 实例中的 state 数据,会导致数据丢失响应式(修改数据后,页面不更新),解决方案:使用 Pinia 内置的storeToRefs 方法。

// 错误写法:直接解构,丢失响应式
const { username, isLogin } = useUserStore()
username = '新用户名' // 页面不更新

// 正确写法:使用 storeToRefs 解构,保留响应式
import { storeToRefs } from 'pinia' // 引入 storeToRefs

const userStore = useUserStore()
const { username, isLogin } = storeToRefs(userStore)

// 此时修改数据(需通过 Store 实例,解构后的变量是只读的)
userStore.username = '新用户名' // 页面正常更新

说明:storeToRefs 只会解构 state 中的数据,并不会解构 getters 和 actions,getters 和 actions 仍需通过 Store 实例访问。

六、实战场景:Pinia 实现购物车功能

结合实际开发中的「购物车」场景,完整演示 Pinia 的使用流程(非TS版),代码可直接复制到项目中使用。

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

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

export const useCartStore = defineStore('cart', {
  state: () => ({
    cartList: [] // 购物车列表,每个项包含 id、name、price、count、checked
  }),
  getters: {
    // 计算购物车总数量
    cartTotalCount: (state) => {
      return state.cartList.reduce((total, item) => total + item.count, 0)
    },
    // 计算购物车总价(只算选中的商品)
    cartTotalPrice: (state) => {
      return state.cartList
        .filter(item => item.checked)
        .reduce((total, item) => total + item.price * item.count, 0)
    },
    // 判断是否全选
    isAllChecked: (state) => {
      return state.cartList.length > 0 && state.cartList.every(item => item.checked)
    }
  },
  actions: {
    // 1. 添加商品到购物车(重复商品数量+1)
    addToCart(goods) {
      const existGoods = state.cartList.find(item => item.id === goods.id)
      if (existGoods) {
        existGoods.count += goods.count
      } else {
        state.cartList.push({ ...goods, checked: true }) // 默认选中
      }
    },

    // 2. 减少商品数量(数量为1时删除商品)
    reduceCartCount(id) {
      const existGoods = state.cartList.find(item => item.id === id)
      if (existGoods) {
        if (existGoods.count === 1) {
          this.removeFromCart(id) // 调用当前 Store 的其他 action
        } else {
          existGoods.count--
        }
      }
    },

    // 3. 从购物车删除商品
    removeFromCart(id) {
      state.cartList = state.cartList.filter(item => item.id !== id)
    },

    // 4. 切换商品选中状态
    toggleChecked(id) {
      const existGoods = state.cartList.find(item => item.id === id)
      if (existGoods) {
        existGoods.checked = !existGoods.checked
      }
    },

    // 5. 全选/取消全选
    toggleAllChecked(checked) {
      state.cartList.forEach(item => {
        item.checked = checked
      })
    },

    // 6. 清空购物车
    clearCart() {
      state.cartList = []
    }
  },
  // 开启持久化,避免页面刷新后购物车数据丢失
  persist: {
    key: 'cartStore',
    paths: ['cartList']
  }
})

2. 组件中使用购物车 Store

<template>
  <div class="cart-page">
    <h3>购物车页面</h3>
    <div class="cart-header">
      <input 
        type="checkbox" 
        v-model="isAllChecked"
        @change="handleToggleAllChecked"
      />
      <span>全选</span>
      <span class="total">总数量:{{ cartStore.cartTotalCount }}</span>
      <span class="total-price">总价:¥{{ cartStore.cartTotalPrice.toFixed(2) }}</span>
      <button @click="cartStore.clearCart" class="clear-btn">清空购物车</button>
    </div>

    <div class="cart-list">
      <div class="cart-item" v-for="item in cartStore.cartList" :key="item.id">
        <input 
          type="checkbox" 
          v-model="item.checked"
          @change="cartStore.toggleChecked(item.id)"
        />
        <span class="goods-name">{{ item.name }}</span>
        <span class="goods-price">¥{{ item.price.toFixed(2) }}</span>
        <div class="count-btn">
          <button @click="cartStore.reduceCartCount(item.id)">-</button>
          <span>{{ item.count }}</span>
          <button @click="cartStore.addToCart({ ...item, count: 1 })">+</button>
        </div>
        <button @click="cartStore.removeFromCart(item.id)" class="delete-btn">删除</button>
      </div>
    </div>

    <div class="add-goods">
      <button @click="handleAddGoods">添加商品到购物车</button>
    </div>
  </div>
</template>

<script setup>
import { useCartStore } from '@/stores/cartStore'
import { storeToRefs } from 'pinia'

const cartStore = useCartStore()
// 解构 getters,保留响应式
const { isAllChecked } = storeToRefs(cartStore)

// 全选/取消全选
const handleToggleAllChecked = () => {
  cartStore.toggleAllChecked(isAllChecked.value)
}

// 模拟添加商品
const handleAddGoods = () => {
  const goods = {
    id: Math.floor(Math.random() * 1000),
    name: `商品${Math.floor(Math.random() * 100)}`,
    price: Math.floor(Math.random() * 100) + 10,
    count: 1
  }
  cartStore.addToCart(goods)
}
</script>

七、常见坑点避坑指南(新手必看)

很多新手在使用 Pinia 时,会遇到「响应式丢失」「状态刷新丢失」「Store 调用错误」等问题,以下是最常见的 5 个坑点,帮你快速避坑。

坑点1:直接解构 Store 实例,丢失响应式

错误写法:const { username } = useUserStore()

正确写法:使用 storeToRefs 解构,const { username } = storeToRefs(useUserStore())

坑点2:忘记开启状态持久化,页面刷新后状态丢失

解决方案:安装 pinia-plugin-persistedstate 插件,在 Store 中添加 persist: true

坑点3:创建 Store 实例时,未调用 useXXXStore()

错误写法:const userStore = useUserStore(未加括号,赋值的是函数本身,不是实例);

正确写法:const userStore = useUserStore()(必须加括号,调用函数创建实例)。

坑点4:actions 中使用 async/await 时,未加 await 调用

错误写法:userStore.logout()(异步方法未加 await,可能导致后续操作执行顺序错误);

正确写法:await userStore.logout()(在 async 函数中调用,等待异步操作完成)。

坑点5:多个 Store 的 id 重复,导致状态混乱

解决方案:Store 的 id 必须全局唯一,推荐和文件名对应(比如 userStore 的 id 为 'user',cartStore 的 id 为 'cart')。

八、总结:Pinia 核心要点回顾

Pinia 作为 Vue3 官方推荐的状态管理库,核心优势是「简单、高效、友好」,掌握以下核心要点,就能应对所有项目场景:

  1. 核心结构:每个 Store 包含 state(存储状态)、getters(计算状态)、actions(修改状态),无需 modules 和 mutations;
  2. 基础流程:安装 Pinia → 初始化挂载 → 创建 Store → 组件中引入并使用;
  3. 状态修改:优先调用 actions 方法,简单场景可直接修改,批量修改用 $patch;
  4. 进阶技巧:getters 有缓存特性,actions 支持异步和跨 Store 调用,状态持久化用 pinia-plugin-persistedstate;
  5. 避坑关键:用 storeToRefs 解构保留响应式,开启持久化避免刷新丢失,Store id 全局唯一。

相比于 Vuex,Pinia 的上手成本极低,即使是新手,也能在1-2小时内掌握核心用法,并且完美适配 Vue3 组合式 API 和 TypeScript,是当前 Vue3 项目状态管理的最优解。