Pinia
Vue 3 状态管理工具 Pinia
Pinia 简介与官方定位
在掌握 Vue 3 核心语法后,进入项目实战前需要引入状态管理工具。Pinia 是 Vue 官方目前最新推荐的状态管理库,已被正式纳入 Vue 官方生态系统。在 Vue 官方文档中,Pinia 已全面替代 Vuex 成为首选的状态管理方案。虽然 Pinia 与 Vuex 均可在 Vue 3 中进行状态管理,但官方强烈推荐使用 Pinia。
Pinia 与 Vuex 的核心优势对比
更简洁的 API 设计
相较于 Vuex,Pinia 提供了更清晰、简单的 API,大幅降低了学习成本。
- Vuex 的核心概念:包含
state、mutation、action、getter以及用于分模块的module,概念繁多。在 Vuex 中,异步操作需通过action处理,且action不能直接修改state,必须通过commit提交mutation来修改数据,逻辑较为繁琐。 - Pinia 的核心概念:仅保留了
state(提供数据)、action(处理业务逻辑)和getter(基于state的计算属性)三个核心概念。 - 核心改进:Pinia 移除了
mutation。在 Pinia 中,action既支持异步操作,又可以直接修改state。这相当于将 Vuex 中的mutation和action合二为一,使得代码逻辑更加清晰,页面中只需直接调用action中的方法即可完成数据修改。
完美支持组合式 API(Composition API)
为了与 Vue 3 的新语法保持统一,Pinia 提供了符合组合式风格的 API。
- 选项式 API 的局限性:传统的选项式写法会将所有的
state、action和getter分别集中放置。当项目规模扩大或数据量增加时,这种按类型聚合的代码结构会导致维护困难。 - 组合式 API 的优势:Pinia 支持将数据声明、数据修改方法以及相关的计算属性按业务逻辑进行组合。代码呈现为模块化的代码块,每个代码块包含独立的数据、方法和
getter,从而大幅提升了大型项目的可维护性。在实际开发中,推荐优先使用组合式 API 风格。
摒弃 Modules 概念,实现天然模块化
Pinia 移除了 modules 概念,无需再处理复杂的命名空间(namespaced)配置。
- 在 Pinia 中,每一个创建的仓库(
store)默认就是一个独立的模块。 - 各个
store拥有独立的数据空间,同时天然支持跨模块调用数据。这种设计赋予了状态管理极强的独立性,简化了多模块状态管理的配置过程。
卓越的 TypeScript 支持
相较于 Vuex 对 TypeScript 支持较弱的问题,Pinia 在设计之初就充分考虑了类型推导,提供了极其完善的 TypeScript 支持,能够为开发者提供更好的类型提示和代码检查体验。
Pinia 状态管理工具配置与使用
Pinia 项目初始化策略
Pinia 是 Vue 官方推荐的新一代状态管理工具,用于替代传统的 Vuex。在实际开发中,Pinia 的配置可以在创建 Vue 项目时自动添加。但为了深入理解其底层运行机制,本教程将从零开始,手动创建一个空的 Vue 3 项目,并严格按照官方文档的说明进行安装与配置。
掌握手动配置流程后,在后续的实际项目中,可直接在脚手架初始化阶段勾选 Pinia 选项,由工具自动完成相关配置的初始化。
创建与清理 Vue 3 基础项目
初始化项目
在终端中进入目标目录(如 E 盘根目录),使用 npm 创建最新版本的 Vue 项目。
执行以下命令:
1 | npm create vue@latest |
根据终端提示,进行如下项目配置选择:
- Project name:输入项目名称,例如
pinia-demo。 - Add TypeScript:选择
No(本阶段暂不需要)。 - Add JSX Support:选择
No。 - Add Vue Router:选择
No(暂不添加单页面应用路由)。 - Add Pinia:选择
No(本次旨在演示手动配置过程)。 - Add Vitest / Testing Solution:选择
No(暂不需要单元测试)。 - Add ESLint:选择
Yes(开启代码质量检查)。 - Add Prettier:根据团队规范选择,本示例中暂不配置。
项目创建完成后,进入项目目录并安装依赖:
1 | cd pinia-demo |
看到终端输出成功提示及本地运行地址,即表示基础项目创建成功。
清理默认模板
通过代码编辑器(如 VS Code)打开项目,对默认生成的模板内容进行清理,以构建纯净的实验环境。
- 删除
src/assets目录及其内容。 - 删除
src/components目录及其内容。 - 打开
src/main.js,移除默认引入的样式文件。 - 打开
src/App.vue,移除默认的模板结构与样式,仅保留最基础的根组件结构。
搭建多组件数据共享演示环境
Pinia 的核心作用是解决多组件共享数据的问题。为了演示该特性,需要在根组件下创建两个子组件,并实现三个组件间的数据联动。
创建子组件
在 src 目录下新建 components 文件夹,并创建两个子组件文件:Son1.vue 和 Son2.vue。
在 Son1.vue 中编写基础模板:
1 | <script setup> |
在 Son2.vue 中编写基础模板:
1 | <script setup> |
配置根组件与组件通信需求
在 src/App.vue 中导入并使用这两个子组件:
1 | <script setup> |
业务需求设定:
在 App.vue 根组件中定义一个共享状态 count(初始值为 0)。Sub1 和 Sub2 不仅需要渲染该数字,还需要提供按钮来修改该数字。点击“加”或“减”按钮时,三个组件中的 count 值必须实现实时联动更新。
Pinia 的安装与核心配置
安装 Pinia 依赖
参考 Vue 官方文档或 Pinia 官方文档,使用包管理器安装 Pinia:
1 | npm install pinia |
配置 Pinia 实例
安装完成后,需要在项目的入口文件 src/main.js 中创建 Pinia 实例,并将其挂载到 Vue 应用上。
Vue 3 环境下的标准配置
在 Vue 3 中,通过 createPinia 创建实例,并使用 app.use() 进行挂载。
1 | import { createApp } from 'vue' |
知识点强调:
- createPinia:用于生成 Pinia 的核心实例对象。
- app.use(pinia):将状态管理插件注入到 Vue 实例中,使其在整个组件树中生效。
- 上述代码支持链式调用(如
createApp(App).use(pinia).mount('#app')),但为了代码的可读性,建议拆分为多行并添加注释。
Vue 2 环境下的处理方案
针对 Vue 2 项目,Pinia 同样提供了完善的支持,但需要额外安装组合式 API 插件,并在配置方式上有所区别。
安装依赖:
1 | npm install pinia @vue/composition-api |
Vue 2 配置代码:
1 | import Vue from 'vue' |
注意:在 Vue 2 中,必须通过 Vue.use(PiniaVuePlugin) 启用组合式 API 支持,并将 pinia 实例作为选项传入 new Vue() 的配置对象中。
总结
本节完成了 Pinia 状态管理工具的前期准备工作,核心内容包括:
- 环境搭建:从零创建并清理了一个纯净的 Vue 3 项目,构建了包含一个根组件和两个子组件的演示环境。
- 需求明确:确立了多组件共享同一状态(
count)并实现双向联动修改的业务需求。 - 插件配置:按照官方文档规范,完成了 Pinia 依赖的安装,并在
main.js中成功创建了 Pinia 实例并完成挂载。同时补充了 Vue 2 环境下的差异化配置方案。
接下来,将深入探讨 Pinia 的核心概念(如 Store 仓库、State 状态、Getters 计算属性 和 Actions 操作方法),并正式实现多组件数据联动的具体功能。
Pinia 状态管理核心概念
在 Pinia 中,状态管理的核心在于维护独立的状态模块。为了管理特定的数据状态(例如计数器的加减操作),需要创建专门的 Store(仓库)模块。组件通过引入并使用该 Store,即可实现状态的读取与修改。所有独立的 Store 模块最终都会挂载到同一个全局状态树上。
定义 Store 的两种语法风格
在 Pinia 中定义 Store,需要导入 defineStore 函数。该函数接收两个核心参数:
- 唯一标识(ID):作为仓库的唯一名称,用于区分不同的 Store 模块。
- 配置项:用于定义 Store 的具体内容,支持选项式 API 和 组合式 API 两种风格。
选项式 API (Option Store)
第二个参数传入一个配置对象,包含 state、getters 和 actions 选项:
- state:通过箭头函数返回一个对象来提供初始数据。
- getters:提供基于 state 派生的计算属性,第一个参数为
state,用于访问内部数据。 - actions:提供操作数据的方法。与 Vuex 不同,Pinia 取消了 mutations,将同步与异步操作统一合并至
actions中。在 actions 内部,通过this直接访问和修改 state 中的数据,且原生支持异步操作。
组合式 API (Setup Store)
第二个参数传入一个 Setup 函数,其语法与 Vue 的组合式 API 高度一致:
- 使用
ref或reactive定义 state(状态)。 - 使用
computed定义 getters(计算属性)。 - 使用普通
function定义 actions(操作方法)。 - 最终通过
return将需要暴露的状态和方法返回。
风格选择建议:
官方推荐使用组合式 API (Setup Store)。选项式 API 强制将同类选项(如所有 state、所有 actions)集中放置,不利于复杂业务逻辑的维护;而组合式 API 允许将同一业务逻辑的状态、计算属性和操作方法聚合在一起,代码结构更清晰,且与组件内的组合式 API 编码风格保持统一。两者底层实现一致,可根据团队规范灵活选择。
实战:创建与定义 Store
创建 Store 文件
在项目的 src 目录下新建 store 文件夹,并创建具体的仓库文件(如 counter.js)。Pinia 支持创建任意多个子仓库,且各仓库之间相互独立。
编写 Store 代码
在 counter.js 中引入 defineStore 并定义仓库:
1 | import { defineStore } from 'pinia' |
在组件中使用 Store
引入与获取 Store 实例
在组件中导入对应的 Store 函数并调用,即可获取当前仓库的实例对象:
1 | import { useCounterStore } from '@/store/counter' |
访问状态与计算属性
通过仓库实例直接访问暴露的状态和计算属性:
1 | <p>当前计数:{{ counterStore.count }}</p> |
重要警告:禁止对 Store 实例进行解构赋值。直接解构会导致数据丢失响应式特性。必须通过 实例名.属性名 的方式进行访问。
修改状态 (调用 Actions)
页面中不应直接修改 Store 内的状态,而应通过调用实例上的 Actions 方法来修改数据:
1 | <button @click="counterStore.addCount">增加</button> |
Actions 方法内部不仅支持同步操作,也完全支持异步操作(如发送网络请求后修改状态)。
跨组件状态共享
Pinia 的核心优势在于跨组件状态共享。在任意组件(如组件 A、组件 B)中,只需重复上述引入和调用 useCounterStore() 的步骤,即可获取同一个 Store 实例,实现状态的实时同步与共享。
Pinia 与 Vuex 的核心差异总结
相较于传统的 Vuex,Pinia 在架构设计和 API 调用上进行了大幅简化:
- 移除 Mutations:不再区分同步的 Mutations 和异步的 Actions,所有数据修改逻辑统一在 Actions 中处理。
- 简化调用方式:在组件中调用 Actions 时,直接通过
实例名.方法名()调用,无需记忆并使用commit或dispatch等辅助触发函数。 - 完美的 TypeScript 支持:组合式 API 的写法使得类型推导更加自然和准确。
- 扁平化模块设计:每个 Store 都是独立的扁平化模块,避免了 Vuex 中复杂的嵌套模块与命名空间问题。
Pinia 异步 Action 的实现与数据渲染
异步 Action
在 Pinia 状态管理中,Action 不仅支持同步操作,同样原生支持异步操作。
- 在 Action 函数内部,无需刻意区分同步或异步逻辑。
- 可以直接修改状态数据,也可以先发送异步请求,在获取数据后再更新状态。
- 其编写方式与在组件中获取异步数据的逻辑完全一致。
需求与接口说明
业务需求
在 Pinia 中获取频道列表数据,并将获取到的数据渲染到 App 组件的模板中。
接口数据结构
接口用于获取新闻类频道列表(如财经、体育等)。返回的数据结构如下:
- 根节点包含
data对象。 data中包含channels数组。channels数组中的每个对象包含id和name属性。
环境准备
由于需要发送网络请求,需在项目中安装 axios 库。
在终端执行以下命令进行安装:
1 | npm add axios |
安装完成后,重新启动项目以使依赖生效。
创建与配置 Store 模块
新建 Store 文件
在 store 目录下新建 channel.js 文件,用于管理频道相关状态。
定义 Store 模块
使用 defineStore 定义仓库,并遵循 Pinia 的命名规范(以 use 开头,Store 结尾,中间为仓库名)。
1 | import { defineStore } from 'pinia' |
核心知识点解析:
- defineStore:用于定义 Pinia 仓库,第一个参数为仓库名称,第二个参数为 Setup 函数。
- ref:用于声明响应式状态数据。
- async/await:在 Action 中直接使用异步函数处理网络请求。
- 数据解构:根据 axios 和后端接口的数据包装层级,正确解构出目标数据。
在组件中调用与渲染数据
在 Vue 2 环境中(Vue 2.7+ 或配合 @vue/composition-api 插件),可通过组件的 setup 选项引入并调用 Store。
组件逻辑编写
在 App.vue 中引入 useChannelStore,并配置点击事件与数据渲染逻辑。
1 | import { useChannelStore } from '@/store/channel' |
模板渲染编写
在模板中使用 v-for 指令遍历 channelList,并绑定点击事件触发异步请求。
1 | <template> |
核心知识点解析:
- Store 实例化:在
setup函数中调用useChannelStore()获取 Store 实例。 - 状态访问:在模板中直接通过
channelStore.channelList访问响应式数据,无需额外解构。 - v-for 渲染:使用
v-for遍历数组时,必须绑定唯一的 :key 属性(此处使用item.id)。
总结与验证
运行验证
- 启动项目并打开浏览器开发者工具。
- 点击“获取频道数据”按钮,观察控制台(Console)是否打印出对应的频道数据。
- 检查 Pinia 开发者工具(Devtools),确认
channel仓库中的channelList状态已更新。 - 确认页面成功渲染出对应的频道名称列表。
核心总结
- Pinia 的 Action 天然支持异步操作,只需在方法内部正常编写异步请求逻辑即可。
- 相比于 Vuex,Pinia 在处理异步操作时无需区分
mutations和actions,代码结构更加扁平、简洁。 - 将网络请求封装在 Store 的 Action 中,有利于实现业务逻辑与视图组件的解耦。
Vue 状态管理:Store 解构与响应式原理
问题引入:Store 解构导致响应式丢失
在实际开发中,直接对 Store 对象进行解构以获取所需状态时,会引发响应式丢失的问题。
问题现象演示
假设存在一个 counterStore,从中直接解构出 count 和 message 状态:
1 | const counterStore = useCounterStore(); |
在页面初始渲染时,数据能够正常显示。但当触发状态更新操作(如点击增加按钮)时,Store 内部的底层数据已发生改变,而页面中通过解构获取的 count 和 message 并未同步更新。此现象表明,直接解构 Store 对象会导致状态丢失响应式特性。
解决方案:引入 storeToRefs
为了解决解构导致的响应式丢失问题,Pinia 提供了 storeToRefs 方法。在解构 Store 时,使用该方法包裹 Store 实例,即可保持状态的响应式:
1 | import { storeToRefs } from 'pinia'; |
经过 storeToRefs 处理后,再次触发状态更新,页面数据能够正常响应并同步更新。
官方文档解析与底层原理
Store 的懒加载与模块化管理
根据官方文档说明,在调用 useStore 之前,Store 实例不会被创建。这意味着即使在项目中定义了大量的 Store 模块,只要某个 Store 未被任何组件导入和使用,该 Store 就不会被初始化。这种懒加载机制有助于优化应用性能。
此外,官方建议在不同的文件中定义各个 Store。将状态、计算属性和操作按业务模块分门别类地存放在独立文件中,能够显著提升代码的可维护性。
Store 的底层包装与 Proxy 劫持
官方文档明确指出:Store 是一个使用 reactive 包装的对象。因此,在访问 Store 内部的属性时,无需像使用 ref 那样添加 .value 后缀。
然而,文档同时强调不能直接解构 Store 对象,因为这会破坏其响应性。其底层原理在于:reactive 的响应式实现依赖于 ES6 的 Proxy 代理机制。Proxy 是针对整个对象及其嵌套属性进行拦截和监听的。
解构破坏响应式的根本原因
当对 Store 对象进行解构时(例如 const { name, doubleCount } = store),实际上是声明了新的独立变量,并将 Store 对象中对应属性的当前值直接赋给了这些变量。
这一赋值操作导致新变量与原始的响应式对象完全脱离了关联,相当于进行了一次基本数据类型的值拷贝。因此,后续对 Store 内部状态的修改,无法触发这些独立变量的更新,从而导致响应式失效。
storeToRefs 实践
状态与方法的解构规范
为了从 Store 中提取属性并保持响应式,必须使用 storeToRefs 方法。该方法会为每一个响应式属性创建一个 ref 引用,从而确保解构后的变量依然与 Store 保持响应式连接。
在实际开发中,Store 通常包含状态(State/Getters)和动作(Actions)。针对这两类属性,解构的最佳实践如下:
- 解构状态(属性):必须使用
storeToRefs进行包裹,以维持数据的响应式。 - 解构动作(方法):直接解构即可。因为方法本身是固定的函数,不需要响应式处理,直接解构不会影响其正常调用。
1 | const store = useCounterStore(); |
直接访问与解构访问的场景选择
除了使用解构语法,开发者也可以始终通过 store.propertyName 的方式直接访问状态和方法。
- 直接访问(
store.xxx):语法统一,无需引入额外方法,适用于 Store 内部属性和方法较少的场景。 - 解构访问:当 Store 内部定义了海量的状态和方法时,频繁使用
store.xxx会导致代码冗长。此时采用解构语法可以简化代码结构,提升可读性。
开发者应根据实际业务场景和代码复杂度,灵活选择最合适的访问方式。
实战演练:综合应用 storeToRefs
以下通过一个具体的业务场景,演示如何结合使用直接解构与 storeToRefs。假设当前组件需要使用 channelStore 中的 channelList(状态)和 getList(动作):
1 | import { storeToRefs } from 'pinia'; |
通过上述改造,组件能够成功发送异步请求,并将获取到的数据正确渲染至页面。同时,由于 channelList 经过了 storeToRefs 处理,后续若该状态发生变更,页面视图也会自动进行响应式更新。
store 解构总结
在 Vue 状态管理中,处理 Store 数据解构时需严格遵循响应式规则。直接解构 Store 对象会破坏 Proxy 代理,导致响应式丢失。通过引入并合理使用 storeToRefs,可以有效提取状态并维持响应式连接。同时,需明确区分状态与动作的解构方式,结合项目实际情况选择最优的代码组织形式,以编写出严谨、高效且易于维护的前端代码。
Pinia 状态管理与数据持久化教程
Pinia 调试工具的使用
在开发过程中,可以通过 Vue Devtools 浏览器插件对 Pinia 进行调试。
- 打开浏览器开发者工具,切换至 Vue Devtools 面板。
- 选择 Pinia 选项卡,即可实时查看和调试 Store 中的状态数据。
- 此调试方式与 Vue 2 时代调试 Vuex 的体验基本一致,能够直观地追踪状态变化。
二、 Pinia 数据持久化插件简介
在传统的 Vue 2 与 Vuex 开发中,实现数据本地持久化通常需要手动封装 localStorage.getItem 和 localStorage.setItem 方法。为了提升开发效率,Pinia 提供了官方的持久化插件 pinia-plugin-persistedstate。
- 该插件能够自动处理状态数据的本地存储与读取。
- 其 API 设计与 Vuex 生态中的
vuex-persistedstate高度相似。掌握该插件后,也可将其思路平移至 Vue 2 的 Vuex 项目中。 - 在实际企业级开发中,使用成熟插件替代手动封装是标准规范。
持久化插件的安装与基础配置
安装插件
使用 npm 或其他包管理器安装持久化插件:
1 | npm i pinia-plugin-persistedstate |
注:请确保当前项目使用的 Pinia 版本高于 2.0.0。
在 main.js 中注册插件
持久化插件需要挂载到 Pinia 实例上,而不是直接挂载到 Vue App 实例上。
1 | import { createPinia } from 'pinia' |
在 Store 模块中开启持久化
在需要持久化的 Store 模块中,添加 persist 配置项。
组合式 API (Setup 语法):
在 defineStore 的第三个参数(配置对象)中添加 persist: true。
1 | import { defineStore } from 'pinia' |
选项式 API (Options 语法):
在与 state、getters、actions 同级的位置添加 persist: true。
1 | export const useCounterStore = defineStore('counter', { |
配置完成后,刷新页面时,插件会自动优先从本地存储中读取数据,并在数据变更时自动同步至本地存储。
持久化插件的高级配置
更多配置可以查看官网配置
默认情况下,插件使用 localStorage 进行存储,以 Store 的 id 作为存储的 key,并对整个 state 进行 JSON.stringify 序列化与 JSON.parse 反序列化。如需自定义,可将 persist 配置为一个对象。
自定义存储键名 (key)
修改本地存储中的键名,避免多项目或模块间的命名冲突。
1 | persist: { |
更改存储方式 (storage)
默认使用 localStorage。若需更改为 sessionStorage,可直接赋值。
1 | persist: { |
注:传入的对象必须具备 getItem 和 setItem 方法。
指定持久化状态 (paths)
默认情况下,整个 state 都会被持久化。若只需持久化部分数据,可通过 paths 数组指定具体的属性名,支持嵌套属性(如 'user.name')。
1 | persist: { |
Pinia 核心知识点总结
- 核心定位:Pinia 是 Vue 官方推荐的新一代状态管理工具,旨在替代 Vue 2 时代的 Vuex,同时完美兼容 Vue 2 与 Vue 3。
- 移除 Mutations:Pinia 彻底移除了
mutations概念。所有的状态修改逻辑统一在actions中处理,且actions同时支持同步与异步操作。 - Getters 的实现:在组合式 API (Setup) 中,
getters直接通过 Vue 的computed计算属性来实现。 - 保持响应式解构:从 Pinia Store 中解构赋值时,为保持数据的响应式特性,必须使用
storeToRefs函数进行包裹。 - 数据持久化:通过引入并配置
pinia-plugin-persistedstate插件,可快速、优雅地实现状态数据的本地持久化,无需手动操作 Web Storage API。
