Pinia 完整使用指南

Pinia 是 Vue 官方推荐的状态管理库,用于跨组件、跨页面共享状态,其功能定位与 Vuex 一致,但具备更简洁的 API 设计、更好的 TypeScript 支持,且移除了 Vuex 中繁琐的 mutations 概念,大幅降低了学习与使用成本。作为 Vuex 的替代方案,Pinia 更轻量、灵活,是 Vue 3 项目状态管理的首选方案。

二、安装 Pinia

支持 yarn、npm、pnpm 三种包管理器安装,选择任意一种即可:

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

安装完成后,重启项目使依赖生效。

三、Pinia 封装

3.1 封装目的

将 Pinia 实例创建、持久化插件引入等统一管理,避免在 main.js 中写入过多冗余代码,提升项目结构的可维护性(后续持久化插件的配置将在此基础上扩展)。

3.2 封装步骤

  1. src 目录下创建 stores 文件夹(约定俗成的状态管理目录),并在该文件夹下创建 index.js 文件;

  2. index.js 中创建并导出 Pinia 实例:

    `// src/stores/index.js

    import { createPinia } from "pinia"; // 引入 Pinia 核心方法

const pinia = createPinia(); // 创建 Pinia 实例

export default pinia; // 导出实例,供 main.js 注册`

  1. 在 main.js 中引入并使用 Pinia:
    `// src/main.js
    import { createApp } from 'vue';
    import App from './App.vue';
    import router from './router';
    import './assets/reset.css';
    import ElementPlus from 'element-plus';
    import pinia from './stores'; // 引入封装好的 Pinia 实例

// 注意:此处修正原文重复 use(router) 的错误

createApp(App)

.use(pinia) // 注册 Pinia

.use(router) // 注册路由

.use(ElementPlus) // 注册 ElementPlus

.mount('#app');`

四、定义 Store

4.1 核心规则

  • Store 通过 defineStore() 函数定义,第一个参数为唯一名称(id),用于连接开发者工具,不可重复;

  • 约定将返回的 Store 函数命名为 useXxxStore 格式(如 useUserInfoStore),符合 Vue 可组合项的命名规范;

  • Store 文件建议放在 stores 目录下,文件名见名知意(如用户相关状态用 user.js)。

4.2 具体实现(以用户状态为例)

  1. stores 目录下创建 user.js 文件;

  2. 编写 Store 定义代码:

    `// src/stores/user.js

    import { defineStore } from "pinia";

// 定义并导出用户信息 Store

const useUserInfoStore = defineStore('userInfo', {

// state:存储核心状态数据,必须是函数(避免跨组件共享引用问题)

state: () => ({

username: '赫赫',

age: 30,

like: 'girl',

obj: { money: 100, friend: 10 },

hobby: [

{ id: 1, name: '篮球', level: 1 },

{ id: 2, name: 'rap', level: 10 }

]

})

});

export default useUserInfoStore;`

五、Store 三大核心概念

Pinia 的核心功能围绕 State、Getters、Actions 展开,分别对应「状态数据」「派生数据」「业务逻辑」三大核心场景。

5.1 State:核心状态数据

State 类似于组件中的 data,用于存储全局共享的原始数据。

5.1.1 访问 State 数据

Store 实例化后,可直接通过实例访问 State 数据,但需注意:不可直接解构 Store 实例 ,否则会丢失数据的响应式特性。若需解构,需使用 Pinia 提供的 storeToRefs() 方法(推荐)或 Vue 的 toRefs() 方法。

vue 复制代码
<template>
  <!-- 直接使用响应式数据 -->
  <div>我叫 {{ username }},今年 {{ age }} 岁,喜欢 {{ like }}</div>
  <div>爱好列表:</div>
  <div v-for="item in hobby" :key="item.id">
    {{ item.name }}(熟练度:{{ item.level }})
  </div>
  <button @click="editPiniaHandler">逐个修改数据</button>
  <button @click="editAll">批量修改数据</button>
  <button @click="replaceAll">替换整个 State</button>
  <button @click="resetBtn">重置 State</button>
</template>

<script setup>
import { storeToRefs } from "pinia"; // 引入 Pinia 响应式解构工具
import useUserInfoStore from "@/stores/user"; // 引入用户 Store

// 实例化 Store(Store 会在首次调用时创建)
const userInfoStore = useUserInfoStore();

// 响应式解构 State 数据(推荐使用 storeToRefs)
const { username, age, like, hobby } = storeToRefs(userInfoStore);

// 错误示范:直接解构会丢失响应式
// const { username, age } = userInfoStore;
</script>

5.1.2 修改 State 数据

Pinia 支持 4 种修改 State 的方式,根据场景灵活选择:

  1. 直接修改 :适用于单个数据的简单修改
    // 逐个修改数据 const editPiniaHandler = () => { userInfoStore.username += "嘎"; userInfoStore.age += 1; userInfoStore.like = "boy"; };

  2. **KaTeX parse error: Expected '}', got 'EOF' at end of input: ... userInfoStore.patch({

    username: "鸭蛋",

    age: 21,

    obj: { money: 200, friend: 15 } // 注意:对象会被完整替换

    });

// 若需修改对象/数组中的部分属性,可使用函数形式

userInfoStore.$patch((state) => {

state.obj.money += 50;

state.hobby.push({ id: 3, name: '游泳', level: 5 });

});

};`

  1. **KaTeX parse error: Expected '}', got 'EOF' at end of input: ... userInfoStore.state = {

    username: '狗子',

    age: 22,

    like: 'boy',

    obj: { money: 10, friend: 1 },

    hobby: []

    };

    };`

  2. **KaTeX parse error: Expected '}', got 'EOF' at end of input: ... userInfoStore.reset();

    };`

5.2 Getters:派生状态数据

Getters 类似于组件中的 computed,用于基于 State 派生出新数据,具备缓存特性(依赖的 State 未变化时,多次访问会复用缓存结果)。

5.2.1 定义 Getters

在 defineStore 中与 state 同级定义,支持两种写法:接收 state 参数访问状态,或通过 this 访问其他 Getters。

javascript 复制代码
// src/stores/user.js
import { defineStore } from "pinia";

const useUserInfoStore = defineStore('userInfo', {
  state: () => ({
    username: '赫赫',
    age: 30,
    like: 'girl',
    obj: { money: 100, friend: 10 },
    hobby: [
      { id: 1, name: '篮球', level: 1 },
      { id: 2, name: 'rap', level: 10 }
    ]
  }),
  // 定义 Getters
  getters: {
    // 基础用法:通过 state 参数访问状态
    doubleAge: (state) => state.age * 2,
    
    // 进阶用法:通过 this 访问其他 Getters(注意:不可使用箭头函数,否则 this 指向异常)
    addOneAge() {
      return this.doubleAge + 1;
    },
    
    // 传参用法:返回函数实现动态传参(此时缓存失效,每次调用都会重新计算)
    getHobbyById: (state) => (id) => {
      return state.hobby.find(item => item.id === id) || '未找到该爱好';
    },
    
    // 复杂派生:计算总资产(假设 money 为现金,friend 为好友数量,此处仅为示例)
    totalAssets() {
      return this.obj.money + this.obj.friend * 10;
    }
  }
});

export default useUserInfoStore;

5.2.2 使用 Getters

直接通过 Store 实例访问 Getters,传参时直接调用函数并传入参数:

vue 复制代码
<template>
  <div>年龄翻倍:{{ userInfoStore.doubleAge }}</div>
  <div>年龄翻倍+1:{{ userInfoStore.addOneAge }}</div>
  <div>ID=1 的爱好:{{ userInfoStore.getHobbyById(1) }}</div>
  <div>总资产:{{ userInfoStore.totalAssets }}</div>
</template>

<script setup>
import useUserInfoStore from "@/stores/user";
const userInfoStore = useUserInfoStore();
</script>

5.3 Actions:业务逻辑封装

Actions 类似于组件中的 methods,用于封装业务逻辑,支持同步和异步操作,是修改 State 的推荐方式(集中管理逻辑,便于维护)。与 Vuex 不同,Pinia 的 Actions 可直接修改 State,无需通过 mutations。

5.3.1 定义 Actions

在 defineStore 中与 state、getters 同级定义,可通过 this 访问 Store 实例的 state、getters 和其他 actions。

javascript 复制代码
// src/stores/user.js
import { defineStore } from "pinia";

const useUserInfoStore = defineStore('userInfo', {
  state: () => ({ /* 省略 state 定义 */ }),
  getters: { /* 省略 getters 定义 */ },
  // 定义 Actions
  actions: {
    // 同步 Action:接收外部参数
    addAge(step) {
      console.log('接收的步长参数:', step);
      this.age += step; // 直接修改 State
      this.updateAssets(step); // 调用其他 Action
    },
    
    // 辅助 Action:更新资产(内部调用,不对外暴露)
    updateAssets(step) {
      this.obj.money += step * 10;
    },
    
    // 异步 Action:模拟 API 请求
    async fetchUserInfo() {
      try {
        // 模拟网络请求(2 秒延迟)
        const res = await new Promise((resolve) => {
          setTimeout(() => {
            resolve({
              username: '小明',
              age: 25,
              like: 'coding'
            });
          }, 2000);
        });
        
        // 更新 State
        this.$patch(res);
        return res; // 向外返回结果
      } catch (error) {
        console.error('获取用户信息失败:', error);
        throw error; // 抛出错误,供组件捕获
      }
    },
    
    // 复杂异步场景:调用其他异步 Action
    async loadUserAndHobby() {
      const userInfo = await this.fetchUserInfo();
      // 模拟加载爱好列表
      const hobbies = await this.fetchHobbies(userInfo.username);
      this.hobby = hobbies;
    },
    
    // 模拟加载爱好列表
    fetchHobbies(username) {
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve([
            { id: 1, name: '编程', level: 8 },
            { id: 2, name: '健身', level: 6 }
          ]);
        }, 1000);
      });
    }
  }
});

export default useUserInfoStore;

5.3.2 使用 Actions

直接通过 Store 实例调用 Actions,同步 Action 直接调用,异步 Action 可使用 await 捕获结果或处理错误:

javascript 复制代码
<script setup>
import useUserInfoStore from "@/stores/user";
const userInfoStore = useUserInfoStore();

// 调用同步 Action
const add = () => {
  userInfoStore.addAge(5);
};

// 调用异步 Action
const loadUser = async () => {
  try {
    const res = await userInfoStore.fetchUserInfo();
    console.log('获取用户信息成功:', res);
  } catch (error) {
    console.error('获取失败:', error);
  }
};

// 调用依赖其他异步 Action 的方法
const loadAll = async () => {
  await userInfoStore.loadUserAndHobby();
  console.log('用户信息和爱好加载完成');
};
</script>

六、Pinia 持久化存储

默认情况下,Pinia 的 State 存储在内存中,页面刷新后会丢失。通过 pinia-plugin-persistedstate 插件可实现 State 持久化,支持 localStorage、sessionStorage 等存储方式。

6.1 安装持久化插件

bash 复制代码
npm install pinia-plugin-persistedstate
# 或 yarn
yarn add pinia-plugin-persistedstate
# 或 pnpm
pnpm add pinia-plugin-persistedstate

6.2 注册插件

在之前封装的src/stores/index.js 中引入并注册插件:

javascript 复制代码
// src/stores/index.js
import { createPinia } from "pinia";
// 引入持久化插件
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';

const pinia = createPinia();
// 注册插件
pinia.use(piniaPluginPersistedstate);

export default pinia;

6.3 持久化配置

在定义 Store 时,通过 persist 选项配置持久化规则,支持基础配置和高级配置两种方式。

6.3.1 基础配置(默认规则)

persist 设为 true,使用默认配置:

  • 存储方式:localStorage

  • 存储键名:Store 的 id(如 userInfo)

  • 序列化/反序列化:JSON.stringify / JSON.parse

  • 持久化范围:整个 State

javascript 复制代码
// src/stores/user.js
import { defineStore } from "pinia";

const useUserInfoStore = defineStore('userInfo', {
  state: () => ({
    username: '赫赫',
    age: 30,
    like: 'girl'
  }),
  getters: { /* 省略 */ },
  actions: { /* 省略 */ },
  // 开启持久化(默认配置)
  persist: true
});

export default useUserInfoStore;

6.3.2 高级配置(自定义规则)

通过对象形式配置 persist,支持自定义存储键名、存储方式、持久化字段等,核心配置项如下:

  • key:自定义存储键名(默认是 Store 的 id);

  • storage:指定存储方式(localStorage / sessionStorage);

  • paths:指定需要持久化的字段(数组形式,不指定则全量持久化);

  • serializer:自定义序列化/反序列化方法(默认 JSON 处理)。

javascript 复制代码
// src/stores/user.js
import { defineStore } from "pinia";

const useUserInfoStore = defineStore('userInfo', {
  state: () => ({
    username: '赫赫',
    age: 30,
    like: 'girl',
    obj: { money: 100, friend: 10 },
    hobby: []
  }),
  getters: { /* 省略 */ },
  actions: { /* 省略 */ },
  // 高级持久化配置
  persist: {
    key: 'user_info_storage', // 自定义存储键名
    storage: sessionStorage, // 存储方式改为 sessionStorage(页面关闭后丢失)
    paths: ['username', 'age', 'obj.money'], // 仅持久化 username、age、obj.money 字段
    // 自定义序列化/反序列化(可选)
    serializer: {
      serialize: (value) => JSON.stringify(value),
      deserialize: (value) => JSON.parse(value)
    }
  }
});

export default useUserInfoStore;

6.3.3 多 Store 独立配置

每个 Store 可单独配置持久化规则,互不影响。例如,用户信息用 localStorage 持久化,购物车用 sessionStorage 持久化:

javascript 复制代码
// 购物车 Store(src/stores/cart.js)
import { defineStore } from "pinia";

const useCartStore = defineStore('cart', {
  state: () => ({
    items: [],
    total: 0
  }),
  persist: {
    key: 'cart_storage',
    storage: sessionStorage, // 购物车仅在当前会话有效
    paths: ['items'] // 仅持久化商品列表
  }
});

export default useCartStore;

七、Pinia 两种定义方式对比

Pinia 支持两种 Store 定义方式,分别对应 Vue 的 Options API 和 Composition API 范式,可根据项目需求选择:

7.1 两种定义方式示例

javascript 复制代码
// 1. Options API 风格(本文默认方式,结构化清晰,适合 Vuex 迁移)
const useUserStore = defineStore('user', {
  state: () => ({ username: '' }),
  getters: { fullName: (state) => `Mr. ${state.username}` },
  actions: { setUsername(name) { this.username = name } }
});

// 2. Composition API 风格(灵活度高,类型推断更好,适合复杂逻辑复用)
import { ref, computed } from 'vue';
import { defineStore } from 'pinia';

const useUserStore = defineStore('user', () => {
  // 状态:对应 state
  const username = ref('');
  
  // 计算属性:对应 getters
  const fullName = computed(() => `Mr. ${username.value}`);
  
  // 方法:对应 actions
  const setUsername = (name) => {
    username.value = name;
  };
  
  // 必须返回需要暴露的属性和方法
  return { username, fullName, setUsername };
});

7.2 核心区别对比

对比维度 Options API 风格 Composition API 风格
状态定义 state 函数返回对象 使用 ref/reactive 定义
计算属性 getters 对象定义 使用 computed 函数定义
方法定义 actions 对象定义 直接定义函数
类型推断 一般 优秀
逻辑复用 较困难 容易(可抽离独立函数)
适用场景 简单状态、Vuex 迁移项目 复杂逻辑、TypeScript 项目

八、常见问题与最佳实践

8.1 常见问题

  • 问题 1:解构 Store 后数据丢失响应式?

    解答:使用 storeToRefs() 替代直接解构,该方法会为 State 数据创建响应式引用。

  • 问题 2:页面刷新后 State 丢失?

    解答:开启持久化存储,通过 pinia-plugin-persistedstate 插件配置 localStorage 存储。

  • 问题 3:Actions 中 this 指向异常?

    解答:Actions 中的方法不可使用箭头函数,否则 this 无法指向 Store 实例。

  • 问题 4:多个组件调用同一 Action 会重复请求?

    解答:在 Action 中添加缓存逻辑,例如通过标志位判断请求是否正在进行,避免重复调用。

8.2 最佳实践

  • Store 拆分原则:按业务模块拆分(如 user.js、cart.js、setting.js),避免单个 Store 过于庞大。

  • 命名规范 :Store 文件名、id 均使用小写+下划线命名(如 user_info),Store 函数名遵循 useXxxStore 格式。

  • 异步请求位置:需要跨组件共享的数据,异步请求放在 Actions 中;仅组件内部使用的请求,直接在组件内处理。

  • 状态修改规范:复杂逻辑的状态修改统一放在 Actions 中,便于维护和调试;简单修改可直接操作 State。

  • 开发工具使用:Pinia 内置支持 Vue DevTools,可通过开发者工具查看 State 变化、回溯 Actions 调用记录,提升调试效率。

九、参考资料

相关推荐
安_3 小时前
<style scoped>跟<style>有什么区别
前端·vue
辛-夷6 小时前
TS封装axios
前端·vue.js·typescript·vue·axios
@AfeiyuO18 小时前
Vue3 矩形树图
vue·echarts
weixin_422555421 天前
ezuikit-js官网使用示例
前端·javascript·vue·ezuikit-js
zhz52141 天前
代码之恋(第十五篇:分布式心跳与网络延迟)
网络·分布式·ai·重构·vue·结对编程
我看刑1 天前
【已解决】el-table 前端分页多选、跨页全选等
前端·vue·element
sg_knight2 天前
拥抱未来:ECMAScript Modules (ESM) 深度解析
开发语言·前端·javascript·vue·ecmascript·web·esm
汝生淮南吾在北2 天前
SpringBoot3+Vue3小区物业报修系统+微信小程序
微信小程序·小程序·vue·毕业设计·springboot·课程设计·毕设
苏打水com2 天前
第十九篇:Day55-57 前端工程化进阶——从“手动低效”到“工程化高效”(对标职场“规模化”需求)
前端·css·vue·html