前言

在 Vue 3 生态逐渐成为主流的当下,状态管理工具也迎来了新的 “官方推荐”——Pinia。作为 Vuex 的继任者,Pinia 不仅简化了 Vuex 中繁琐的Mutations设计,还带来了更友好的 TypeScript 支持、更灵活的 Store 组织方式,以及更轻量的体积。无论是新手入门 Vue 状态管理,还是老项目从 Vuex 迁移,Pinia 都是现阶段的最优解之一。本文将从基础安装到高级特性,带你一步步掌握 Pinia 的核心用法,所有代码均经过实战验证,可直接复制到项目中使用。

一、Pinia 基础:从安装到挂载

1. 环境要求

Pinia 支持 Vue 2 和 Vue 3,但更推荐在 Vue 3 项目中使用(能更好发挥 Composition API 优势),同时需要 Node.js 14.0 + 环境。

2. 安装 Pinia

打开项目终端,执行以下命令安装:

bash

# npm 安装
npm install pinia --save

# yarn 安装(若使用yarn)
yarn add pinia

3. 全局挂载 Pinia

在项目入口文件(通常是main.jsmain.ts)中,创建 Pinia 实例并挂载到 Vue 应用上:

javascript

// main.js(Vue 3 + Options API)
import { createApp } from 'vue'
import { createPinia } from 'pinia' // 引入Pinia
import App from './App.vue'
import router from './router' // 若有路由,可正常引入

// 1. 创建Pinia实例
const pinia = createPinia()
// 2. 创建Vue应用
const app = createApp(App)

// 3. 挂载Pinia、路由到应用
app.use(pinia)
app.use(router)
app.mount('#app')

如果你的项目使用 TypeScript,代码结构基本一致,仅需注意类型声明:

typescript

// main.ts(Vue 3 + TypeScript)
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'

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

二、核心用法:定义与使用 Store

Store 是 Pinia 的核心概念,相当于 “状态容器”,每个 Store 负责管理一块独立的业务状态(如用户信息、购物车、计数器等)。Pinia 中定义 Store 的方式非常灵活,支持 Options API 风格和 Composition API 风格,本文先从更易上手的 Options API 风格讲起。

1. 定义第一个 Store(Options API 风格)

推荐在项目根目录下创建stores文件夹,统一管理所有 Store 文件(如counter.jsuser.js),避免状态分散。

以 “计数器” Store 为例,创建stores/counter.js

javascript

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

// 关键:通过defineStore定义Store,第一个参数是Store的唯一ID(必须唯一)
export const useCounterStore = defineStore('counter', {
  // 1. state:存储状态(类似Vue组件的data),必须是函数返回对象(避免跨实例污染)
  state: () => ({
    count: 0, // 计数器初始值
    title: 'Pinia计数器', // 额外状态示例
    isLoading: false // 加载状态示例
  }),

  // 2. getters:计算属性(类似Vue组件的computed),基于state派生新值
  getters: {
    // 基础用法:接收state参数,返回计算后的值
    doubleCount: (state) => state.count * 2,

    // 进阶用法:通过this访问其他getters(注意:此时不能用箭头函数)
    doubleCountPlusOne() {
      // this指向当前Store实例,可访问state和其他getters
      return this.doubleCount + 1
    },

    // 带参数的getters:返回一个函数(本质是方法,但仍具有缓存特性)
    getCountWithAdd: (state) => (num) => state.count + num
  },

  // 3. actions:修改状态的方法(类似Vuex的actions,但支持同步+异步)
  actions: {
    // 同步修改:直接通过this操作state
    increment() {
      this.count++ // 无需像Vuex那样通过commit调用Mutation
    },

    // 带参数的同步方法
    incrementBy(num) {
      this.count += num
    },

    // 异步操作:支持async/await(如请求接口、定时器等)
    async incrementAsync() {
      this.isLoading = true // 开启加载状态
      // 模拟异步请求(如接口调用)
      await new Promise((resolve) => setTimeout(resolve, 1000))
      this.increment() // 异步完成后修改状态
      this.isLoading = false // 关闭加载状态
    }
  }
})

2. 在 Vue 组件中使用 Store

在组件中使用 Store 的步骤非常简单:引入 Store 函数 → 调用函数获取实例 → 直接使用 state、getters、actions

以 Vue 3 的<script setup>语法为例(最简洁的方式):

vue

<!-- components/Counter.vue -->
<template>
  <div class="counter-container">
    <h2>{{ counterStore.title }}</h2>
    <!-- 直接使用state -->
    <p>当前计数:{{ counterStore.count }}</p>
    <!-- 使用getters -->
    <p>计数的2倍:{{ counterStore.doubleCount }}</p>
    <p>计数的2倍+1:{{ counterStore.doubleCountPlusOne }}</p>
    <p>计数+5:{{ counterStore.getCountWithAdd(5) }}</p>

    <!-- 调用actions -->
    <button @click="counterStore.increment">+1</button>
    <button @click="counterStore.incrementBy(2)">+2</button>
    <button @click="counterStore.incrementAsync" :disabled="counterStore.isLoading">
      {{ counterStore.isLoading ? '加载中...' : '异步+1' }}
    </button>
  </div>
</template>

<script setup>
// 1. 引入定义好的Store函数
import { useCounterStore } from '@/stores/counter'

// 2. 调用函数,获取Store实例(无需new,Pinia会自动管理)
const counterStore = useCounterStore()
</script>

<style scoped>
.counter-container {
  margin: 20px;
  padding: 20px;
  border: 1px solid #eee;
  border-radius: 8px;
}
button {
  margin: 0 8px;
  padding: 6px 12px;
  cursor: pointer;
}
</style>

如果你的项目仍在使用 Options API,使用方式如下:

vue

<!-- components/CounterOptions.vue -->
<template>
  <div>
    <p>当前计数:{{ counterStore.count }}</p>
    <button @click="counterStore.increment">+1</button>
  </div>
</template>

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

export default {
  // 通过computed获取Store实例(确保响应式)
  computed: {
    counterStore() {
      return useCounterStore()
    }
  }
}
</script>

三、Pinia 高级特性:解锁更多实用功能

掌握基础用法后,再来看看 Pinia 的高级特性,这些功能能帮你应对复杂业务场景(如批量修改状态、监听状态变化、Store 间通信等)。

1. 状态修改的 3 种方式

除了通过actions修改状态,Pinia 还支持直接修改和批量修改,灵活度更高。

(1)直接修改状态(简单场景推荐)

适合简单的状态修改,无需封装到actions(Pinia 不强制要求所有修改都通过actions):

javascript

const counterStore = useCounterStore()
// 直接修改单个状态
counterStore.count = 10
// 直接修改多个状态
counterStore.title = '新的计数器标题'
counterStore.isLoading = true
(2)$patch批量修改(推荐)

当需要修改多个状态时,使用$patch更高效(内部会批量更新,减少响应式触发次数),支持对象形式和函数形式。

  • 对象形式(适合固定字段修改):

javascript

counterStore.$patch({
  count: 20, // 直接赋值
  title: '批量修改后的标题',
  isLoading: false // 覆盖原有值
})
  • 函数形式(适合复杂逻辑修改,如数组操作):

javascript

// 假设state中有一个数组:list: [1, 2, 3]
counterStore.$patch((state) => {
  state.list.push(4) // 直接操作数组
  state.count = state.list.length // 基于数组长度修改count
})
(3)actions修改(复杂 / 异步场景推荐)

当修改逻辑复杂(如需要判断、计算)或涉及异步操作(如接口请求)时,必须通过actions封装,保证代码可维护性:

javascript

// stores/user.js
export const useUserStore = defineStore('user', {
  state: () => ({
    userInfo: {},
    token: ''
  }),
  actions: {
    // 封装登录逻辑(异步+复杂状态修改)
    async login(username, password) {
      this.isLoading = true
      try {
        // 调用登录接口
        const res = await axios.post('/api/login', { username, password })
        // 批量修改状态
        this.$patch({
          userInfo: res.data.user,
          token: res.data.token
        })
        // 额外操作:存储token到localStorage
        localStorage.setItem('token', res.data.token)
      } catch (err) {
        console.error('登录失败:', err)
        throw err // 抛出错误,让组件捕获
      } finally {
        this.isLoading = false
      }
    }
  }
})

2. 监听状态变化

Pinia 提供了两种监听状态的方式:$subscribe(监听整个 Store)和 Vue 的watch(监听单个 / 多个状态)。

(1)$subscribe监听整个 Store

适合需要监听 Store 中所有状态变化的场景(如状态持久化、日志记录),且支持detached选项(组件卸载后仍继续监听):

javascript

const counterStore = useCounterStore()

// 监听整个Store的状态变化
counterStore.$subscribe((mutation, state) => {
  // mutation:包含状态变化的信息(如storeId、type、payload)
  // state:变化后的完整状态
  console.log('Store状态变化:', mutation, state)
  
  // 示例:将状态持久化到localStorage
  localStorage.setItem('counterState', JSON.stringify(state))
}, {
  detached: true // 组件卸载后,监听仍生效(可选)
})
(2)watch监听单个 / 多个状态

适合精准监听某几个状态的变化,用法与 Vue 组件中的watch一致:

javascript

import { watch } from 'vue'
import { useCounterStore } from '@/stores/counter'

const counterStore = useCounterStore()

// 1. 监听单个状态
watch(
  () => counterStore.count, // 监听的状态(必须是函数返回值,确保响应式)
  (newVal, oldVal) => {
    console.log(`count从${oldVal}变成了${newVal}`)
  },
  { immediate: true } // 立即执行一次(可选)
)

// 2. 监听多个状态
watch(
  [() => counterStore.count, () => counterStore.title], // 数组形式传入多个状态
  ([newCount, newTitle], [oldCount, oldTitle]) => {
    console.log(`count变化:${oldCount}→${newCount}`)
    console.log(`title变化:${oldTitle}→${newTitle}`)
  }
)

3. Store 之间的通信

在复杂项目中,经常需要多个 Store 之间相互调用(如用户 Store 需要用到购物车 Store 的方法)。Pinia 中无需像 Vuex 那样通过rootGettersmodules嵌套,直接在一个 Store 中引入并使用另一个 Store 即可。

示例:用户登录后,清空购物车未登录状态

javascript

// 1. 定义购物车Store(stores/cart.js)
export const useCartStore = defineStore('cart', {
  state: () => ({
    list: [] // 购物车列表
  }),
  actions: {
    // 清空购物车方法
    clearCart() {
      this.list = []
    }
  }
})

// 2. 在用户Store中使用购物车Store(stores/user.js)
import { useCartStore } from './cart' // 引入购物车Store

export const useUserStore = defineStore('user', {
  actions: {
    async login(username, password) {
      // 登录逻辑...(省略)
      
      // 登录成功后,调用购物车Store的clearCart方法
      const cartStore = useCartStore()
      cartStore.clearCart()
    }
  }
})

4. 状态持久化(pinia-plugin-persistedstate)

Pinia 默认不支持状态持久化(页面刷新后状态会丢失),可通过第三方插件pinia-plugin-persistedstate实现,无需手动写localStorage逻辑。

(1)安装插件

bash

npm install pinia-plugin-persistedstate --save
(2)配置插件

main.js中注册插件:

javascript

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).mount('#app')
(3)开启 Store 持久化

在需要持久化的 Store 中,添加persist: true配置:

javascript

// stores/user.js(持久化用户信息)
export const useUserStore = defineStore('user', {
  state: () => ({
    token: '',
    userInfo: {}
  }),
  // 开启持久化(默认存储到localStorage,key为Store的id)
  persist: true,
  
  // 进阶配置:自定义持久化规则(如指定存储字段、存储位置)
  // persist: {
  //   key: 'my-user-store', // 自定义存储key
  //   storage: sessionStorage, // 存储到sessionStorage(默认localStorage)
  //   paths: ['token'] // 只持久化token字段(默认持久化所有state)
  // }
})

四、Pinia vs Vuex:为什么选 Pinia?

很多同学会问:“我已经会 Vuex 了,为什么要迁移到 Pinia?” 这里用一张表对比两者的核心差异,帮你理解 Pinia 的优势:

特性 Pinia Vuex 3/Vuex 4
官方推荐 Vue 3 官方推荐 Vue 2 官方推荐(Vue 3 中已被 Pinia 替代)
API 复杂度 简洁(无 Mutations,直接用 actions) 繁琐(需区分 Actions、Mutations、Commit)
TypeScript 支持 原生支持,类型推导完善 需手动写类型声明,支持较差
Store 组织方式 平级组织,无需嵌套 Modules 需通过 Modules 嵌套,复杂项目易冗余
体积 轻量(约 1KB) 较笨重(Vuex 4 约 20KB)
响应式原理 基于 Vue 3 的 Proxy(支持所有数据类型) Vuex 3 基于 Object.defineProperty(有局限性)

结论:新项目直接用 Pinia,老 Vuex 项目可逐步迁移到 Pinia——Pinia 不仅兼容 Vuex 的核心功能,还解决了 Vuex 的诸多痛点,学习成本低,迁移成本也低。

五、常见问题与注意事项

  1. Store 实例是否需要缓存?
    不需要!useCounterStore()每次调用返回的都是同一个实例,Pinia 内部会自动缓存,无需手动用refreactive包裹。

  2. 为什么 getters 中不能用箭头函数访问 this?
    箭头函数没有自己的this,会绑定外层作用域的this,导致无法指向 Store 实例。若需访问其他 getters 或 state,必须用普通函数。

  3. 持久化插件不生效怎么办?
    检查 3 点:① 插件是否在pinia上注册;② Store 中是否配置persist: true;③ 存储的字段是否是可序列化的(如 Function、Symbol 无法被JSON.stringify,会丢失)。

你在使用 Pinia 时遇到过哪些问题?比如状态修改不生效、持久化异常,或是 Store 通信的复杂场景?欢迎在评论区留言讨论,一起解决问题、深化对 Pinia 的理解!

Logo

纵情码海钱塘涌,杭州开发者创新动! 属于杭州的开发者社区!致力于为杭州地区的开发者提供学习、合作和成长的机会;同时也为企业交流招聘提供舞台!

更多推荐