news 2026/6/11 20:31:06

Vue.js从零到精通系列(五):全局状态管理——Pinia 核心与实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue.js从零到精通系列(五):全局状态管理——Pinia 核心与实践

摘要:在上一篇中,我们借助 Vue Router 把待办应用改造为多视图单页应用(SPA),拆分出列表、详情、设置三大页面,通过手写useTodoStore组合式函数完成全局数据共享与本地存储持久化。但随着业务迭代、项目体量变大,这种原生手写全局状态方案短板逐步暴露:无法在 Vue Devtools 可视化调试状态流转、无标准化插件扩展能力、TS 类型推导繁琐、多模块状态难以规范化管理。

本篇正式接入 Vue3 官方指定状态管理库Pinia,先拆解手写 Store 的局限性与 Pinia 的核心优势;完整迁移重构原有 Todo 项目,手把手掌握defineStore组合式写法、State/Getters/Actions 三大核心模块;搭配pinia-plugin-persistedstate一行配置实现自动持久化,彻底剔除手动操作localStorage的冗余代码。重构后完整保留原有全部业务功能,同时获得状态调试、模块化拆分、精准类型推断、插件扩展等工程化能力,熟练掌握多页面组件间安全、规范、可追溯的数据流转方案。


一、手写 Store 真的够用吗?为什么要引入 Pinia

1.1 回顾上一版原生全局状态实现

上一章我们用 Vue 原生reactive+computed封装组合式函数,实现全局 Todo 数据管理,核心代码精简如下:

// src/store/todoStore.ts(原生手写版本) import { reactive, computed, watch } from 'vue' import type { Todo } from '../types' ​ export function useTodoStore() { const state = reactive({ todos: [] as Todo[], nextId: 1 }) ​ // 计算属性 const total = computed(() => state.todos.length) const activeCount = computed(() => state.todos.filter(item => !item.done).length) ​ // 增删改业务方法 function addTodo(text: string) { const val = text.trim() if (!val) return state.todos.push({ id: state.nextId++, text: val, done: false }) } ​ // 手动监听数据变化,写入本地存储 watch(() => state.todos, (val) => { localStorage.setItem('todo-data', JSON.stringify(val)) }, { deep: true, flush: 'post' }) ​ // 页面初始化读取本地缓存 const loadStorage = () => {/* 读取、解析localStorage */} loadStorage() ​ return { state, total, activeCount, addTodo, /* 其余方法 */ } }

组件内直接调用const store = useTodoStore()就能拿到状态和操作方法,小型 Demo 完全够用,但中大型项目会暴露出不可忽视的缺陷。

1.2 原生手写方案的四大硬伤

无 Devtools 调试能力Vue 开发者工具无法识别自定义组合式 Store,不能查看状态快照、无法回溯每一次数据修改的调用来源、不能在线手动修改状态调试界面,排查 Bug 只能靠console.log

模块化无统一规范项目拆分用户、购物车、配置等多套全局状态时,只能依靠文件夹、文件名人工区分,无内置命名隔离机制,多人协作极易出现变量、方法重名冲突。

缺少插件扩展体系持久化、全局请求拦截、操作日志打印、权限拦截等通用逻辑,只能在每一个 Store 里重复复制粘贴,无法全局一次性注册复用,代码冗余度极高。

TypeScript 类型体验差响应式对象、返回值需要频繁手动类型断言,无法自动推导完整类型,编辑器智能提示残缺,长期开发类型隐患多。

1.3 Pinia 诞生背景与核心定位

Pinia 最初是 Vuex 5 的实验原型,由 Vue 核心团队成员 Eduardo 开发,2022 年正式成为 Vue3 官方默认状态管理库。 它彻底舍弃了 Vuex 繁琐的Mutations强制同步更新规则,全面兼容组合式 API,本质就是标准化、带官方调试工具、支持插件扩展、TS 原生友好的升级版组合式 Store

对比表格直观区分两种方案:

特性原生手写 Composable StorePinia 标准 Store
Devtools 调试不识别,无状态追踪原生支持,时间线回溯、在线改值
持久化实现手动 watch + localStorage插件一行配置自动完成
模块隔离靠人工文件划分,无隔离defineStore 唯一 ID 天然隔离
TS 类型推导频繁手动断言,提示不全全自动推导,无需额外类型定义
插件机制无,逻辑重复拷贝全局注册插件,一次配置多处生效

二、Pinia 安装与项目初始化接入

2.1 项目前置说明

基于上一篇完整可运行的vue-todo-spa项目迭代,原有目录结构不变,仅重构状态管理层:

vue-todo-spa/ ├── src/ │ ├── main.ts // 项目入口,注册Pinia+路由 │ ├── types.ts // Todo TS类型定义 │ ├── router/index.ts // Vue Router路由配置 │ ├── store/todoStore.ts // 待替换的旧手写Store │ ├── components/ // 所有Todo子UI组件 │ ├── views/ // 列表/详情/设置页面组件 │ └── App.vue // 根布局+全局导航 └── package.json

2.2 安装依赖(核心库+持久化插件)

执行 pnpm 安装命令:

# 安装Pinia核心库 pnpm add pinia ​ # 安装持久化插件,自动把状态存入localStorage pnpm add pinia-plugin-persistedstate

2.3 在入口 main.ts 全局注册 Pinia

import { createApp } from 'vue' import { createPinia } from 'pinia' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' import App from './App.vue' import router from './router' import './style.css' ​ const app = createApp(App) ​ // 1. 创建Pinia实例 const pinia = createPinia() // 2. 挂载持久化插件 pinia.use(piniaPluginPersistedstate) // 3. Pinia注入Vue全局应用 app.use(pinia) ​ // 注册路由 app.use(router) app.mount('#app')

注册顺序无强制要求,Pinia 在 Router 前后注册均可;推荐前置注册,避免路由守卫内部无法调用 Store。

启动项目pnpm dev,浏览器打开 Vue Devtools(如若没有,需要先获取扩展),会新增独立的Pinia标签页,此时暂无自定义仓库,接入完成。


三、重构 Todo 全局仓库:Pinia 组合式标准写法

社区约定多仓库统一放在stores文件夹(复数形式),新建src/stores/todo.ts,替代旧的store/todoStore.ts

3.1 完整 Pinia Todo Store 代码

import { ref, computed } from 'vue' import { defineStore } from 'pinia' import type { Todo } from '../types' ​ // defineStore(仓库唯一ID, 组合式逻辑函数, 配置项) export const useTodoStore = defineStore('todo', () => { // ========== State:响应式状态,等同于组件ref/reactive ========== const todos = ref<Todo[]>([]) const nextId = ref(1) ​ // ========== Getters:计算属性,依赖State自动缓存 ========== // 总任务数量 const total = computed(() => todos.value.length) // 未完成任务数 const activeCount = computed(() => todos.value.filter(t => !t.done).length) // 是否全部任务勾选完成 const allDone = computed(() => todos.value.length > 0 && activeCount.value === 0) // 根据ID查询单个任务(返回查询函数) const getTodoById = computed(() => (id: number) => todos.value.find(t => t.id === id)) ​ // ========== Actions:同步/异步业务方法,修改State唯一入口 ========== // 新增待办 function addTodo(text: string) { const trimmed = text.trim() if (!trimmed) return todos.value.push({ id: nextId.value++, text: trimmed, done: false }) } ​ // 切换任务完成状态 function toggleTodo(id: number) { const target = todos.value.find(item => item.id === id) if (target) target.done = !target.done } ​ // 删除单个任务 function removeTodo(id: number) { todos.value = todos.value.filter(item => item.id !== id) } ​ // 清空已完成任务 function clearCompleted() { todos.value = todos.value.filter(item => !item.done) } ​ // 一键全部勾选完成 function checkAll() { todos.value.forEach(item => item.done = true) } ​ // 一键取消全部勾选 function unCheckAll() { todos.value.forEach(item => item.done = false) } ​ // 向外导出所有状态、计算属性、方法 return { // 原始状态 todos, nextId, // 计算属性Getters total, activeCount, allDone, getTodoById, // 操作方法Actions addTodo, toggleTodo, removeTodo, clearCompleted, checkAll, unCheckAll } }, { // 开启插件自动持久化 persist: true })

3.2 核心语法要点拆解

defineStore('todo', setupFn, options)

  • 第一个参数:仓库唯一ID,全局不可重复,Devtools 以此区分不同仓库;

  • 第二个参数:和<script setup>写法完全一致,直接使用ref/computed

  • 第三个配置项:开启持久化、自定义序列化规则等。

三层结构分工清晰

  • State:原始响应式数据,不能在组件内随意直接批量改写;

  • Getters:封装派生数据(统计、筛选、查询),自带缓存,重复调用不会重复计算;

  • Actions:所有修改状态的逻辑统一封装在这里,方便统一日志、校验、拦截。

无需手动处理localStoragepersist: true开启后,插件自动监听 State 变化存入本地存储,页面刷新自动恢复数据,原有手写的读取、解析、异常捕获代码全部删除。


四、批量修改页面组件,接入 Pinia Store

原有组件调用方式改动极小,仅修改导入路径 + 删除多余的.state层级,业务模板代码无需改动。

4.1 修改列表页 TodoListPage.vue

<script setup lang="ts"> import { useRouter } from 'vue-router' // 路径替换为新stores文件夹 import { useTodoStore } from '../stores/todo' import TodoHeader from '../components/TodoHeader.vue' import TodoInput from '../components/TodoInput.vue' import TodoList from '../components/TodoList.vue' import TodoItem from '../components/TodoItem.vue' import TodoFooter from '../components/TodoFooter.vue' ​ const router = useRouter() // 直接获取仓库实例,不再有.state嵌套层级 const store = useTodoStore() </script> ​ <template> <div class="page-wrap"> <div class="card"> <TodoHeader :active-count="store.activeCount" :total="store.total" /> <TodoInput @add="store.addTodo" /> ​ <!-- 直接访问store.todos,不再是store.state.todos --> <TodoList :todos="store.todos"> <template #item="{ todo }"> <TodoItem :todo="todo" :go-detail="() => router.push({name:'Detail', params:{id:todo.id}})" @toggle="store.toggleTodo" @remove="store.removeTodo" /> </template> </TodoList> ​ <TodoFooter v-if="store.todos.length" :is-all-checked="store.allDone" @check-all="store.checkAll" @un-check-all="store.unCheckAll" @clear-completed="store.clearCompleted" /> </div> </div> </template> ​ <style scoped>/* 样式完全不变,省略 */</style>

4.2 修改详情页 TodoDetailPage.vue

<script setup lang="ts"> import { computed, watch } from 'vue' import { useRouter, useRoute } from 'vue-router' import { useTodoStore } from '../stores/todo' ​ const router = useRouter() const route = useRoute() const store = useTodoStore() ​ const tid = computed(() => Number(route.params.id)) // Getters返回查询函数,调用传参获取单条任务 const todo = computed(() => store.getTodoById(tid.value)) ​ // 返回列表 const backList = () => router.push('/list') // 删除当前任务 const delCurrent = () => { if (todo.value) store.removeTodo(tid.value) backList() } ​ // 路由ID变更校验任务是否存在 watch( () => route.params.id, () => !todo.value && backList(), { immediate: true } ) </script> ​ <template>/* 模板无改动,直接沿用 */</template>

4.3 修改设置页 SettingPage.vue

<script setup lang="ts"> import { useRouter } from 'vue-router' import { useTodoStore } from '../stores/todo' ​ const router = useRouter() const store = useTodoStore() ​ // 清空全部任务 const clearAll = () => { if(confirm('确定清空所有任务?不可恢复!')) { // Pinia支持直接赋值修改ref类型state store.todos = [] store.nextId = 1 } } // 手动清空本地缓存(插件持久化数据) const clearStorage = () => { localStorage.removeItem('todo-data') alert('本地存储已清空,刷新页面生效') } </script> ​ <template>/* 模板无改动 */</template>

4.4 清理旧代码

所有组件导入路径全部替换完成后,直接删除src/store/todoStore.ts旧文件,项目结构彻底规范化。 运行pnpm dev,页面功能和重构前完全一致,无任何业务回归。


五、持久化插件高级配置(可选拓展)

上文persist: true是最简写法,支持对象形式精细化配置存储规则:

persist: { key: 'vue-todo-pinia-data', // 自定义localStorage存储key storage: sessionStorage, // 切换存储引擎(关闭标签页数据自动销毁) paths: ['todos'], // 仅持久化todos,nextId不存入本地 beforeRestore: (ctx) => { // 数据恢复前置钩子 console.log('开始读取本地缓存', ctx) } }

六、跨仓库互相调用(多Store协作实战)

真实项目会拆分用户、购物车、配置等多个独立仓库,Pinia 支持仓库间互相导入调用,无需复杂命名空间配置。

6.1 新建用户仓库 src/stores/user.ts

import { ref } from 'vue' import { defineStore } from 'pinia' ​ export const useUserStore = defineStore('user', () => { const username = ref('游客未登录') ​ // 登录方法 function login(name: string) { username.value = name } ​ return { username, login } })

6.2 在 Todo 仓库内调用用户仓库

import { ref, computed } from 'vue' import { defineStore } from 'pinia' // 导入用户仓库 import { useUserStore } from './user' import type { Todo } from '../types' ​ export const useTodoStore = defineStore('todo', () => { // 直接实例化另一个仓库 const userStore = useUserStore() ​ const todos = ref<Todo[]>([]) const nextId = ref(1) ​ // 新增任务增加登录校验 function addTodo(text: string) { // 读取用户仓库状态做权限拦截 if (userStore.username === '游客未登录') { alert('请登录后再添加待办任务!') return } const trimmed = text.trim() if (!trimmed) return todos.value.push({ id: nextId.value++, text: trimmed, done: false }) } ​ // 其余代码不变... }, { persist: true })

跨仓库调用语法简洁直观,无 Vuex 嵌套模块、根根访问器等复杂语法,多人协作维护成本极低。


七、全量功能回归测试

重构后完整复测所有原有业务功能,保证无改动、无Bug:

列表页:新增、勾选、删除、批量全选/取消、清空已完成、空列表提示全部正常;

详情页:路由ID匹配任务、修改状态、删除跳转、无效ID自动回退列表页;

设置页:一键清空所有任务、手动清除本地存储、返回列表功能正常;

持久化校验:新增多条任务,刷新浏览器页面,数据完整保留,插件自动完成存储恢复。


八、总结

通过本篇,我们在上一篇的项目基础上,将手写的组合式 Store 升级为 Pinia 官方状态管理库。我们不仅完成了功能等价的重构,还额外获得了 Devtools 调试能力和插件化的数据持久化。Pinia 并没有颠覆你之前对状态管理的认知,而是用更规范、更强大的工具把你已经熟悉的组合式 API 模式包装了起来,让你在面对大型项目时能更加从容。

  • Pinia 是 Vue 3 的默认状态管理库,可以看作是“带插件的 Composable Store”。

  • 使用defineStore(id, () => { ... })创建组合式 Store,State 用ref,Getter 用computed,Action 用普通函数。

  • 在组件中调用useXxxStore()获取 Store 实例,直接访问属性和方法,无需.value(在模板中)。

  • 借助pinia-plugin-persistedstate插件,可以一行配置实现自动持久化,告别手写watch+localStorage

  • Pinia 与 Vue Devtools 深度集成,提供状态检查和时间旅行调试。

  • Store 之间可以自由相互导入调用,模块化极其自然。


如果这篇文章帮你解决了实操上的困惑,别忘记点击点赞、分享,也可以留言告诉我你遇到的其它问题,我会尽快回复。动手练习是掌握编程最快的方法,请务必亲手敲一遍本文的所有示例代码,并截图保存你的成果。你的关注是我坚持原创和细节共享的力量来源,谢谢大家。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/11 20:31:05

LuckyLilliaBot 多协议QQ机器人实战指南:深度配置与高级应用

LuckyLilliaBot 多协议QQ机器人实战指南&#xff1a;深度配置与高级应用 【免费下载链接】LuckyLilliaBot 支持 OneBot 11、Satori 和 Milky 协议 项目地址: https://gitcode.com/gh_mirrors/li/LuckyLilliaBot LuckyLilliaBot是一款基于LiteLoaderQQNT的高性能QQ机器人…

作者头像 李华
网站建设 2026/6/11 20:25:09

如何使用LLM-as-Judge

LLM-as-Judge 是指用大语言模型来评估另一个模型/系统的输出质量&#xff0c;常用于 RAG、客服机器人、摘要、翻译、代码生成等场景的自动化评测。 下面给你一个实用的使用方法。1. LLM-as-Judge 适合评估什么 常见评估维度包括&#xff1a;场景评估指标问答系统正确性、完整性…

作者头像 李华
网站建设 2026/6/11 20:24:13

2026产品运营如何提升职场能力与核心竞争力

提升职场能力的核心方向能力维度具体措施与CDA证书关联性数据驱动决策掌握SQL/Python数据处理技能&#xff0c;定期输出运营分析报告CDA课程涵盖从数据清洗到可视化的全流程技能用户增长策略构建AARRR模型指标体系&#xff0c;设计AB测试框架认证考试包含增长黑客方法论及实战案…

作者头像 李华
网站建设 2026/6/11 20:21:57

Python开发工具链全解析:提升开发效率的利器

在当今快速发展的软件开发领域&#xff0c;Python凭借其简洁的语法、强大的库支持和广泛的应用场景&#xff0c;已经成为众多开发者的首选语言之一。为了充分发挥Python的潜力&#xff0c;构建一个高效、便捷的开发工具链至关重要。本文将深入解析Python开发工具链的各个组成部…

作者头像 李华