目录
- 前言
- 一、为什么需要状态管理?
- 1.1 组件通信的困境
- 1.2 状态管理的本质
- 二、Pinia 是什么?
- 2.1 Pinia vs Vuex
- 2.2 Pinia 的核心概念
- 三、安装与配置
- 3.1 安装
- 3.2 挂载到应用
- 四、定义 Store
- 4.1 基本结构
- 4.2 组合式 API 风格(推荐)
- 五、使用 Store
- 5.1 在组件中使用
- 5.2 解构 Store
- 5.3 直接修改 State
- 5.4 订阅 State 变化
- 六、实战案例:用户认证
- 6.1 用户 Store
- 6.2 登录页面
- 6.3 布局组件(显示用户信息)
- 6.4 路由守卫(使用 Store)
- 七、多个 Store
- 7.1 学生 Store
- 7.2 在组件中使用多个 Store
- 八、Pinia 持久化
- 8.1 安装插件
- 8.2 配置插件
- 8.3 在 Store 中启用持久化
- 九、Pinia vs 直接使用 localStorage
- 十、总结
- Vue3 学习路径总结
前言
在vue-router:单页面应用路由管理中,我们掌握了如何管理多个页面。但还有一个问题:不同组件之间如何共享数据?
比如用户信息:登录后获取用户信息,侧边栏显示用户名,右上角显示头像,个人中心页展示详细信息。这些组件都需要访问同一份数据。
传统做法是用 props 一层层传递,或者用 provide/inject,但这些方式都不够优雅。Pinia是 Vue3 官方推荐的状态管理库,专门解决跨组件数据共享问题。
本文将讲解 Pinia 的核心概念:Store 的定义与使用、数据的响应式更新、以及与组件的结合。
一、为什么需要状态管理?
1.1 组件通信的困境
假设有三个组件需要共享用户信息:
App.vue ├── Header.vue (需要用户名) ├── Sidebar.vue (需要用户权限) └── Profile.vue (需要完整用户信息)| 方式 | 问题 |
|---|---|
| props 传递 | 需要层层传递,繁琐 |
| provide/inject | 祖先向后代传递,但数据来源不清晰 |
| localStorage | 需要手动同步,不是响应式的 |
1.2 状态管理的本质
状态管理就是把多个组件共享的数据抽取到一个独立的地方管理:
┌─────────────────────────────────────┐ │ Store(仓库) │ │ ┌─────────────────────────────┐ │ │ │ state: 用户信息、权限等 │ │ │ │ actions: 获取、修改数据的方法 │ │ │ └─────────────────────────────┘ │ └─────────────────────────────────────┘ ↑ ↑ ↑ │ │ │ Header.vue Sidebar.vue Profile.vue类比 Java:类似于 Spring 的单例 Bean,全局共享,所有组件都可以访问。
二、Pinia 是什么?
2.1 Pinia vs Vuex
Pinia 是 Vue3 官方推荐的状态管理库,相比 Vuex(Vue2 时代的方案):
| 特性 | Vuex | Pinia |
|---|---|---|
| 代码量 | 多(mutations、actions) | 少(只有 state、actions) |
| TypeScript | 支持较弱 | 完美支持 |
| Composition API | 不友好 | 完美支持 |
| 模块化 | 需要手动分模块 | 每个 Store 独立文件 |
| 调试工具 | Vue DevTools 支持 | Vue DevTools 支持 |
结论:Vue3 项目推荐使用 Pinia。
2.2 Pinia 的核心概念
| 概念 | 说明 | 类比 |
|---|---|---|
| Store | 存储共享数据的仓库 | Java 的 Service 类 |
| state | 数据本身 | 类的成员变量 |
| actions | 操作数据的方法 | 类的方法 |
| getters | 计算属性 | 类的 getter 方法 |
注意:Pinia 没有 Vuex 的 mutations,直接在 actions 中修改 state 即可。
三、安装与配置
3.1 安装
npminstallpinia3.2 挂载到应用
在main.ts中:
import{createApp}from'vue'import{createPinia}from'pinia'importAppfrom'./App.vue'importrouterfrom'./router'constapp=createApp(App)app.use(createPinia())app.use(router)app.mount('#app')四、定义 Store
4.1 基本结构
新建src/stores/user.ts:
import{defineStore}from'pinia'// 第一个参数是 store 的唯一标识exportconstuseUserStore=defineStore('user',{// state: 数据state:()=>({name:'',age:0,sex:'',token:''}),// getters: 计算属性getters:{isLogin:(state)=>!!state.token,userInfo:(state)=>`${state.name}(${state.sex},${state.age}岁)`},// actions: 方法actions:{setToken(token:string){this.token=token},clearToken(){this.token=''},asyncfetchUserInfo(){// 调用 API 获取用户信息constdata=awaitrequest.get('/api/user/info')this.name=data.namethis.age=data.agethis.sex=data.sex}}})类比 Java:
state类似于类的成员变量getters类似于 getter 方法actions类似于普通方法(可以修改 state)
4.2 组合式 API 风格(推荐)
如果习惯 Composition API,可以用setup语法:
import{defineStore}from'pinia'import{ref,computed}from'vue'exportconstuseUserStore=defineStore('user',()=>{// state(用 ref 包装)constname=ref('')constage=ref(0)consttoken=ref('')// getters(用 computed)constisLogin=computed(()=>!!token.value)// actions(普通函数)functionsetToken(newToken:string){token.value=newToken}functionclearToken(){token.value=''}asyncfunctionfetchUserInfo(){constdata=awaitrequest.get('/api/user/info')name.value=data.name age.value=data.age}// 返回需要暴露的内容return{name,age,token,isLogin,setToken,clearToken,fetchUserInfo}})两种风格对比:
| 特性 | Options API 风格 | Composition API 风格 |
|---|---|---|
| 语法 | 类似 Vuex | 更灵活,符合 Vue3 风格 |
| 复杂度 | 简单 | 适中 |
| 推荐场景 | 简单 Store | 复杂逻辑、需要组合其他 Hook |
五、使用 Store
5.1 在组件中使用
<script setup lang="ts"> import { useUserStore } from '@/stores/user' const userStore = useUserStore() // 读取 state console.log(userStore.name) console.log(userStore.token) // 读取 getters console.log(userStore.isLogin) // 调用 actions userStore.setToken('xxx') userStore.fetchUserInfo() </script> <template> <div> <p>用户名:{{ userStore.name }}</p> <p v-if="userStore.isLogin">已登录</p> <button @click="userStore.clearToken">退出登录</button> </div> </template>5.2 解构 Store
直接解构会失去响应式,需要用storeToRefs:
<script setup lang="ts"> import { storeToRefs } from 'pinia' import { useUserStore } from '@/stores/user' const userStore = useUserStore() // ❌ 错误:失去响应式 const { name, age } = userStore // ✅ 正确:保持响应式 const { name, age, isLogin } = storeToRefs(userStore) // actions 不需要 storeToRefs(它们本身就是函数) const { setToken, fetchUserInfo } = userStore </script> <template> <p>用户名:{{ name }}</p> <p>年龄:{{ age }}</p> </template>5.3 直接修改 State
<script setup lang="ts"> import { useUserStore } from '@/stores/user' const userStore = useUserStore() // 方式1:直接修改 userStore.name = '张三' // 方式2:批量修改($patch) userStore.$patch({ name: '张三', age: 18 }) // 方式3:$patch 函数式 userStore.$patch((state) => { state.name = '张三' state.age = 18 }) // 方式4:替换整个 state userStore.$reset() // 重置为初始值 </script>| 方法 | 说明 |
|---|---|
store.xxx = value | 直接修改单个属性 |
store.$patch(object) | 批量修改 |
store.$patch(function) | 批量修改(函数式) |
store.$reset() | 重置为初始值 |
推荐:简单修改直接赋值,复杂修改用
$patch。
5.4 订阅 State 变化
<script setup lang="ts"> import { useUserStore } from '@/stores/user' const userStore = useUserStore() // 监听 state 变化 userStore.$subscribe((mutation, state) => { console.log('变化类型:', mutation.type) // 'direct' | 'patch object' | 'patch function' console.log('新状态:', state) // 持久化到 localStorage localStorage.setItem('user', JSON.stringify(state)) }) </script>六、实战案例:用户认证
6.1 用户 Store
import{defineStore}from'pinia'import{ref,computed}from'vue'importrequestfrom'@/api/request'exportconstuseUserStore=defineStore('user',()=>{consttoken=ref(localStorage.getItem('token')||'')constname=ref('')constrole=ref('')constisLogin=computed(()=>!!token.value)asyncfunctionlogin(username:string,password:string){constdata=awaitrequest.post('/api/login',{username,password})token.value=data.token name.value=data.name role.value=data.role localStorage.setItem('token',data.token)}asyncfunctionfetchUserInfo(){if(!token.value)returnconstdata=awaitrequest.get('/api/user/info')name.value=data.name role.value=data.role}functionlogout(){token.value=''name.value=''role.value=''localStorage.removeItem('token')}return{token,name,role,isLogin,login,logout,fetchUserInfo}})6.2 登录页面
<script setup lang="ts"> import { ref } from 'vue' import { useRouter } from 'vue-router' import { ElMessage } from 'element-plus' import { useUserStore } from '@/stores/user' const router = useRouter() const userStore = useUserStore() const form = ref({ username: '', password: '' }) async function handleLogin() { try { await userStore.login(form.value.username, form.value.password) ElMessage.success('登录成功') router.push('/') } catch (error) { ElMessage.error('登录失败') } } </script> <template> <div class="login"> <h2>登录</h2> <el-form> <el-form-item> <el-input v-model="form.username" placeholder="用户名" /> </el-form-item> <el-form-item> <el-input v-model="form.password" type="password" placeholder="密码" /> </el-form-item> <el-button type="primary" @click="handleLogin">登录</el-button> </el-form> </div> </template>6.3 布局组件(显示用户信息)
<script setup lang="ts"> import { onMounted } from 'vue' import { useRouter } from 'vue-router' import { useUserStore } from '@/stores/user' const router = useRouter() const userStore = useUserStore() onMounted(() => { userStore.fetchUserInfo() }) function handleLogout() { userStore.logout() router.push('/login') } </script> <template> <header class="header"> <div class="logo">学生管理系统</div> <div class="user-info"> <span>{{ userStore.name }}</span> <span class="role">{{ userStore.role }}</span> <button @click="handleLogout">退出</button> </div> </header> </template>6.4 路由守卫(使用 Store)
import{useUserStore}from'@/stores/user'router.beforeEach((to,from,next)=>{constuserStore=useUserStore()if(to.meta.requiresAuth&&!userStore.isLogin){next('/login')}else{next()}})七、多个 Store
实际项目会有多个 Store,按功能划分:
src/stores/ ├── user.ts # 用户相关 ├── student.ts # 学生相关 └── app.ts # 应用配置(主题、语言等)7.1 学生 Store
import{defineStore}from'pinia'import{ref}from'vue'importrequestfrom'@/api/request'exportconstuseStudentStore=defineStore('student',()=>{conststudents=ref<Student[]>([])consttotal=ref(0)constloading=ref(false)asyncfunctionfetchStudents(params:QueryParams){loading.value=truetry{constdata=awaitrequest.get('/api/students/page',{params})students.value=data.list total.value=data.total}finally{loading.value=false}}asyncfunctiondeleteStudent(id:number){awaitrequest.delete(`/api/students/${id}`)// 刷新列表awaitfetchStudents({page:1,size:10})}return{students,total,loading,fetchStudents,deleteStudent}})7.2 在组件中使用多个 Store
<script setup lang="ts"> import { onMounted } from 'vue' import { useUserStore } from '@/stores/user' import { useStudentStore } from '@/stores/student' const userStore = useUserStore() const studentStore = useStudentStore() onMounted(() => { studentStore.fetchStudents({ page: 1, size: 10 }) }) </script> <template> <div> <p>当前用户:{{ userStore.name }}</p> <el-table :data="studentStore.students" v-loading="studentStore.loading"> <!-- ... --> </el-table> </div> </template>八、Pinia 持久化
默认情况下,Pinia 的数据保存在内存中,刷新页面会丢失。可以通过插件实现持久化。
8.1 安装插件
npminstallpinia-plugin-persistedstate8.2 配置插件
import{createApp}from'vue'import{createPinia}from'pinia'importpiniaPluginPersistedstatefrom'pinia-plugin-persistedstate'importAppfrom'./App.vue'constpinia=createPinia()pinia.use(piniaPluginPersistedstate)constapp=createApp(App)app.use(pinia)app.mount('#app')8.3 在 Store 中启用持久化
exportconstuseUserStore=defineStore('user',{state:()=>({token:'',name:''}),// 启用持久化persist:true})或者自定义持久化配置:
exportconstuseUserStore=defineStore('user',{state:()=>({token:'',name:'',role:''}),persist:{key:'my-user-store',// 存储 keystorage:localStorage,// 存储方式(localStorage 或 sessionStorage)paths:['token','name']// 只持久化部分字段}})九、Pinia vs 直接使用 localStorage
| 维度 | Pinia | localStorage |
|---|---|---|
| 响应式 | ✅ 自动更新 | ❌ 需要手动监听 |
| 类型安全 | ✅ TypeScript 支持 | ❌ 纯字符串 |
| 调试 | ✅ Vue DevTools 支持 | ❌ 不支持 |
| 持久化 | 需要插件 | ✅ 天然支持 |
| 适用场景 | 运行时状态 | 长期存储 |
推荐:运行时状态用 Pinia,需要持久化时配合插件使用。
十、总结
本文系统讲解了 Pinia 状态管理:
| 概念 | 作用 | 关键点 |
|---|---|---|
| Store | 管理共享数据 | defineStore定义 |
| state | 数据本身 | 用ref包装(Composition API) |
| getters | 计算属性 | 用computed |
| actions | 操作方法 | 普通函数,可修改 state |
| storeToRefs | 解构保持响应式 | 只用于 state 和 getters |
最佳实践:
- 每个 Store 独立文件,按功能划分
- 复杂 Store 使用 Composition API 风格
- 需要持久化时使用
pinia-plugin-persistedstate - 解构 Store 时用
storeToRefs保持响应式
Vue3 学习路径总结
通过这五篇博客,我们已经系统掌握了 Vue3 的核心知识:
| 序号 | 博客 | 核心内容 |
|---|---|---|
| 1 | Vue3入门:响应式数据与数据绑定 | 环境搭建、ref/reactive、数据绑定、计算属性、条件/列表渲染 |
| 2 | 与后端交互:axios与请求封装 | axios 基础、拦截器、环境变量、vue-request |
| 3 | Element Plus实战:快速构建增删改查 | 表格、分页、表单、校验、子组件通信 |
| 4 | vue-router:单页面应用路由管理 | 路由配置、嵌套路由、路由守卫 |
| 5 | pinia状态管理:跨组件数据共享 | Store 定义与使用、持久化 |
下一步学习建议:
- TypeScript 进阶:泛型约束、类型体操、工具类型
- Vue3 进阶:自定义 Hook、指令、插件
- 工程化:Vite 配置、打包优化、CI/CD
- 实战项目:完整的后台管理系统
作者:小张 | 转载请注明出处