引言:从页面导航到状态共享的必然演进

在掌握了 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:多个组件需要共享的状态

总结:从路由到状态管理的完整解决方案

技术演进路径:

  1. Vue Router → 解决页面导航和URL管理

  2. Pinia → 解决跨组件状态共享和业务逻辑复用

核心设计理念:

  • 关注点分离:UI组件只负责展示,业务逻辑集中在Store中

  • 单一数据源:每个业务领域对应一个Store,避免状态分散

  • 类型安全:完整的TypeScript支持,减少运行时错误

  • 开发体验:优秀的DevTools集成,提升调试效率

企业级应用建议:

选择策略

  • 简单赋值 → 直接修改

  • 批量更新 → $patch

  • 业务逻辑 → Actions

架构原则

  • 重要的业务逻辑必须通过Actions处理

  • 复杂的状态计算使用getters封装

  • 相关的状态集中在同一个Store中管理

Pinia 让状态管理变得简单而强大,是现代 Vue 应用不可或缺的架构基石。在下一章中,我们将深入探讨 Pinia 的高级特性和最佳实践!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐