从 Vue Router 到 Pinia:现代 Vue 状态管理入门指南
引言:从页面导航到状态共享的必然演进
在掌握了 Vue Router 之后,我们已经能够构建功能丰富的多页面应用。但很快我们会发现一个新的挑战:
路由解决了页面间导航,但如何管理跨页面的共享状态?
想象这些真实场景:
-
用户登录后,头部导航、个人中心、权限按钮都需要显示用户信息
-
电商网站的购物车数据需要在商品列表、商品详情、结算页面间同步
-
主题色、语言设置等全局配置需要在所有组件中生效
传统方案的痛点:
-
Props 逐层传递:深度嵌套组件中极其繁琐
-
事件总线:难以调试和维护,类型支持差
-
本地存储:无法响应式更新,数据类型有限
-
全局变量:缺乏开发工具支持,容易混乱
这就是 Pinia 要解决的核心问题!
1. 什么是 Pinia?Vue 2 用户的平滑过渡
1.1 Pinia 的定位:Vuex 的现代继承者
Pinia 是 Vue 官方推荐的下一代状态管理库,可以看作是 Vuex 5 的正式实现。它专为 Vue 3 和组合式 API 设计。
1.2 Vue 2 用户的熟悉感
如果你熟悉 Vue 2 的 Vuex,会发现 Pinia 的概念一脉相承:
| Vuex (Vue 2) | Pinia (Vue 3) | 核心职责 |
|---|---|---|
state |
state |
定义响应式数据 |
getters |
getters |
计算派生状态 |
mutations + actions |
actions |
修改状态的业务逻辑 |
最大的改进:Pinia 移除了繁琐的 mutations,让状态修改更加直观!
1.3 为什么选择 Pinia?
// ❌ Vuex 的繁琐流程
dispatch('action') → commit('mutation') → update state
// ✅ Pinia 的简洁直接
call action() → update state directly
核心优势对比:
| 特性 | Vuex 4 | Pinia |
|---|---|---|
| TypeScript 支持 | 需要配置 | 原生完美支持 |
| API 复杂度 | 较高 | 极简直观 |
| 包体积 | 相对较大 | 轻量 (约 1KB) |
| 开发体验 | 良好 | 优秀(DevTools 深度集成) |
2. 快速上手:安装与配置
2.1 安装 Pinia
npm install pinia
2.2 在 main.ts 中配置
// src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
// 引入 createPinia
import { createPinia } from 'pinia'
// 创建 pinia 实例
const pinia = createPinia()
const app = createApp(App)
// 注册 pinia 插件
app.use(pinia)
app.mount('#app')
配置完成后,Vue DevTools 会自动显示 Pinia 面板,享受完整的时间旅行调试!
3. 创建第一个 Store:计数器实战
3.1 定义 Store
// src/store/count.ts
import { defineStore } from 'pinia'
// 定义并导出 store
export const useCountStore = defineStore('count', {
// 状态 - 相当于组件的 data
state: () => {
return {
sum: 6,
school: 'Vue3 实战学院',
history: [] as number[] // 操作历史记录
}
},
// 计算属性 - 相当于组件的 computed
getters: {
// 基于 state 的计算
doubleSum: (state) => state.sum * 2,
// 使用 this 访问整个 store
summary(): string {
return `${this.school} - 当前数值: ${this.sum}`
},
// 带参数的计算属性
multipliedBy: (state) => {
return (multiplier: number) => state.sum * multiplier
}
},
// 动作 - 相当于组件的 methods
actions: {
// 基础加法
increment(value: number) {
this.sum += value
this.recordHistory(`增加了 ${value}`)
},
// 带验证的减法
decrement(value: number) {
if (this.sum - value >= 0) {
this.sum -= value
this.recordHistory(`减少了 ${value}`)
} else {
console.warn('数值不能为负数')
}
},
// 重置操作
reset() {
this.sum = 0
this.recordHistory('重置为 0')
},
// 私有方法(业务逻辑封装)
private recordHistory(operation: string) {
this.history.push({
timestamp: new Date(),
operation,
currentValue: this.sum
})
// 只保留最近10条记录
if (this.history.length > 10) {
this.history.shift()
}
}
}
})
3.2 在组件中使用 Store
vue
<template>
<div class="counter-page">
<h2>计数器示例</h2>
<!-- 直接通过 store 实例访问状态 -->
<div class="display">
<p>当前数值: <strong>{{ countStore.sum }}</strong></p>
<p>双倍数值: {{ countStore.doubleSum }}</p>
<p>三倍数值: {{ countStore.multipliedBy(3) }}</p>
<p class="summary">{{ countStore.summary }}</p>
</div>
<!-- 操作按钮 -->
<div class="actions">
<button @click="addOne">+1</button>
<button @click="subtractOne">-1</button>
<button @click="addFive">+5</button>
<button @click="reset">重置</button>
</div>
<!-- 操作历史 -->
<div class="history">
<h4>操作历史:</h4>
<ul>
<li v-for="(record, index) in countStore.history" :key="index">
{{ record.timestamp.toLocaleTimeString() }} - {{ record.operation }} → {{ record.currentValue }}
</li>
</ul>
</div>
</div>
</template>
<script setup lang="ts">
// 引入 store
import { useCountStore } from '@/store/count'
// 获取 store 实例
const countStore = useCountStore()
// 调用 actions 中的方法
const addOne = () => countStore.increment(1)
const subtractOne = () => countStore.decrement(1)
const addFive = () => countStore.increment(5)
const reset = () => countStore.reset()
</script>
4. 修改数据的三种方式详解
4.1 方式一:直接修改(简单直接)
适用场景:简单的状态赋值,没有复杂业务逻辑
vue
<script setup lang="ts">
import { useCountStore } from '@/store/count'
const countStore = useCountStore()
// 直接对 state 属性赋值
const resetToTen = () => {
countStore.sum = 10 // 直接修改
}
const changeSchool = () => {
countStore.school = 'Pinia 大师班' // 直接修改
}
// 甚至可以直接操作数组
const clearHistory = () => {
countStore.history = [] // 直接替换数组
}
</script>
优点:
-
极其简单直观
-
适合快速原型开发
-
vuex不能直接修改值
缺点:
-
无法包含业务逻辑
-
分散在各个组件中,难以维护
-
无法进行统一的验证或副作用处理
4.2 方式二:批量修改($patch)
适用场景:需要同时修改多个状态属性,或者进行复杂的状态计算
4.2.1 对象形式的 $patch
vue
<script setup lang="ts">
import { useCountStore } from '@/store/count'
const countStore = useCountStore()
// 批量更新多个状态
const initializeState = () => {
countStore.$patch({
sum: 100,
school: '初始化状态',
history: [] // 同时重置历史
})
}
// 部分更新
const updatePartial = () => {
countStore.$patch({
sum: countStore.sum + 10, // 可以基于当前状态计算
school: `更新于 ${new Date().toLocaleTimeString()}`
})
}
</script>
4.2.2 函数形式的 $patch(更强大)
vue
<script setup lang="ts">
import { useCountStore } from '@/store/count'
const countStore = useCountStore()
// 使用函数形式进行复杂的状态更新
const complexUpdate = () => {
countStore.$patch((state) => {
// 在函数内可以访问整个 state
state.sum *= 2
state.school = `翻倍操作: ${state.school}`
// 添加操作记录
state.history.push({
timestamp: new Date(),
operation: '复杂翻倍操作',
currentValue: state.sum
})
// 保持历史记录长度
if (state.history.length > 5) {
state.history = state.history.slice(-5)
}
})
}
// 基于条件的批量更新
const conditionalUpdate = (threshold: number) => {
countStore.$patch((state) => {
if (state.sum > threshold) {
state.sum = threshold
state.school = `已达到阈值 ${threshold}`
}
})
}
</script>
$patch 的优势:
-
性能优化:多个状态变更在一次反应性更新中完成
-
原子性操作:要么全部成功,要么全部失败
-
复杂逻辑支持:函数形式支持任意复杂的状态计算
4.3 方式三:通过 Actions
4.3.1 核心优势:业务逻辑的集中管理
问题场景:用户下单操作
分散在各组件中的逻辑(难以维护):
vue
<!-- 组件A -->
<script setup>
const handleOrder = async () => {
// 验证库存
const stockCheck = await api.checkStock(cartItems)
if (!stockCheck.valid) {
alert('库存不足')
return
}
// 计算价格
const total = calculateTotal(cartItems)
// 调用API
const result = await api.createOrder({
items: cartItems,
total: total
})
// 更新状态
orderStore.orders.push(result.order)
cartStore.clearCart()
// 记录日志
await api.logActivity('order_created')
}
</script>
使用 Actions 集中管理:
// store/order.ts
actions: {
async createOrder(orderData) {
// 所有业务逻辑集中在一个地方
await this.validateStock(orderData.items)
const total = this.calculateTotal(orderData.items)
const order = await this.submitOrder({ ...orderData, total })
this.updateState(order)
await this.recordActivity(order)
}
}
4.3.2 选项式 API vs 组合式 API 对比
当然,比如前面的actions那些全部都是选项式api,也可以使用组合式api,我们来看看
4.3.2.1 选项式 API(熟悉 Vue 2 的开发者)
export const useOrderStore = defineStore('order', {
state: () => ({
orders: [],
loading: false,
error: null
}),
getters: {
pendingOrders: (state) => state.orders.filter(o => o.status === 'pending')
},
actions: {
async createOrder(orderData) {
this.loading = true
try {
const order = await api.createOrder(orderData)
this.orders.push(order)
return order
} catch (error) {
this.error = error.message
throw error
} finally {
this.loading = false
}
}
}
})
4.3.2.2 组合式 API(Vue 3 推荐方式)
export const useOrderStore = defineStore('order', () => {
// State - 使用 ref 或 reactive
const orders = ref<Order[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
// Getters - 使用 computed
const pendingOrders = computed(() =>
orders.value.filter(order => order.status === 'pending')
)
const totalRevenue = computed(() =>
orders.value.reduce((sum, order) => sum + order.total, 0)
)
// Actions - 普通函数
const createOrder = async (orderData: CreateOrderData) => {
loading.value = true
error.value = null
try {
const order = await api.createOrder(orderData)
orders.value.push(order)
return order
} catch (err: any) {
error.value = err.message
throw err
} finally {
loading.value = false
}
}
const cancelOrder = async (orderId: number) => {
const order = orders.value.find(o => o.id === orderId)
if (!order) throw new Error('订单不存在')
await api.cancelOrder(orderId)
order.status = 'cancelled'
}
// 返回 state 和 actions
return {
orders,
loading,
error,
pendingOrders,
totalRevenue,
createOrder,
cancelOrder
}
})
4.3.2.3 两种风格的对比
| 方面 | 选项式 API | 组合式 API |
|---|---|---|
| 学习成本 | 较低(Vue 2 风格) | 需要学习组合式概念 |
| 类型推断 | 良好 | 优秀 |
| 代码组织 | 按选项分组 | 按功能组织 |
| 灵活性 | 一般 | 很高 |
| 复用逻辑 | 需要 mixins | 直接提取函数 |
5. 三种修改方式的对比总结
| 特性 | 直接修改 | $patch | Actions |
|---|---|---|---|
| 代码位置 | 分散在组件中 | 分散在组件中 | 集中在 Store |
| 业务逻辑 | 无法包含 | 有限支持 | 完整支持 |
| 数据验证 | 无 | 有限 | 完整验证 |
| 错误处理 | 无 | 有限 | 完整处理 |
| 副作用管理 | 无 | 有限 | 统一管理 |
| 可测试性 | 困难 | 一般 | 容易测试 |
| 可维护性 | 差 | 一般 | 优秀 |
| 适用场景 | 简单赋值 | 批量更新 | 企业级业务 |
让我们看一个真实的企业级应用示例:
6. 与 Vue 3 Hooks 的对比
注意:前面讲了hooks,有的小伙伴跟hooks有点混淆下面我们来看看
6.1 传统的 Composables 方式
vue
<template>
<div>
<p>计数: {{ count }}</p>
<button @click="increment">增加</button>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// 使用 Composables - 仅限于当前组件
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
const increment = () => {
count.value++
}
</script>
6.2 Pinia Store 方式
// store/counter.ts
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++
}
}
})
vue
<template>
<div>
<p>计数: {{ counter.count }}</p>
<p>双倍: {{ counter.doubleCount }}</p>
<button @click="counter.increment">增加</button>
</div>
</template>
<script setup>
// 在多个组件中共享同一状态
import { useCounterStore } from '@/store/counter'
const counter = useCounterStore()
</script>
5.3 核心区别
| 方面 | Composables (Hooks) | Pinia Store |
|---|---|---|
| 作用范围 | 单个组件或通过参数传递 | 全局共享 |
| 状态持久化 | 组件卸载即销毁 | 应用生命周期内持久化 |
| DevTools | 有限支持 | 完整的时间旅行调试 |
| TypeScript | 需要手动定义类型 | 自动类型推断 |
| 代码组织 | 逻辑关注点分离 | 业务逻辑集中管理 |
选择建议:
-
使用 Composables:组件内部复杂逻辑抽离
-
使用 Pinia:多个组件需要共享的状态
总结:从路由到状态管理的完整解决方案
技术演进路径:
-
Vue Router → 解决页面导航和URL管理
-
Pinia → 解决跨组件状态共享和业务逻辑复用
核心设计理念:
-
关注点分离:UI组件只负责展示,业务逻辑集中在Store中
-
单一数据源:每个业务领域对应一个Store,避免状态分散
-
类型安全:完整的TypeScript支持,减少运行时错误
-
开发体验:优秀的DevTools集成,提升调试效率
企业级应用建议:
选择策略:
-
简单赋值 → 直接修改
-
批量更新 → $patch
-
业务逻辑 → Actions
架构原则:
-
重要的业务逻辑必须通过Actions处理
-
复杂的状态计算使用getters封装
-
相关的状态集中在同一个Store中管理
Pinia 让状态管理变得简单而强大,是现代 Vue 应用不可或缺的架构基石。在下一章中,我们将深入探讨 Pinia 的高级特性和最佳实践!
更多推荐
所有评论(0)