Vue.js: Pinia 状态管理

最后更新:2026-08-26

Pinia 是 Vue 3 官方推荐的状态管理库——替代 Vuex。Pinia 提供了更简洁的 API、完整的 TypeScript 支持、模块化热更新、原生 DevTools 支持。Vue 官方已经将 Pinia 作为默认推荐(Vuex 不再维护)。

Pinia 让你管理跨组件共享状态(用户信息、购物车、全局设置)比 provide/inject 更结构化,比 Vuex 简洁 50%。

1. 你将学到


2. 一个购物车"组件 5 处不一致"的噩梦

(1) 痛点:5 个组件各管各的购物车数据

Alice 的电商后台有 5 个组件需要购物车数据:

JS
// ❌ 翻车版:5 个组件 5 份数据
// CartIcon.vue
const cartCount = ref(0)

// ProductCard.vue
const localCart = ref([])

// CartPage.vue
const cart = ref({ items: [], total: 0 })

// CheckoutPage.vue
const myCart = ref([])

// Header.vue
const cartItems = ref([])

产品经理 Charlie:

"Alice,我在 ProductCard 里添加商品,购物车图标没更新!图标显示'0',但页面显示'1 item'。5 个组件 5 份购物车数据——它们不同步!"

(2) Vue Pinia 解法:1 个 store,5 个组件共享

JS
// stores/cart.js
import { defineStore } from 'pinia'

export const useCartStore = defineStore('cart', {
  state: () => ({
    items: [],
    total: 0
  }),
  getters: {
    itemCount: (state) => state.items.length,
    totalPrice: (state) => state.items.reduce((sum, i) => sum + i.price, 0)
  },
  actions: {
    addItem(product) {
      this.items.push(product)
      this.total += product.price
    },
    removeItem(id) {
      this.items = this.items.filter(i => i.id !== id)
    }
  }
})
VUE
<!-- CartIcon.vue -->
<script setup>
import { useCartStore } from '@/stores/cart'
const cart = useCartStore()
// 自动响应:cart.itemCount 变了,icon 立即更新
</script>
<template>
  <span>🛒 {{ cart.itemCount }}</span>
</template>
VUE
<!-- ProductCard.vue -->
<script setup>
import { useCartStore } from '@/stores/cart'
const cart = useCartStore()
</script>
<template>
  <button @click="cart.addItem(product)">Add to Cart</button>
</template>

1 个 store,5 个组件实时同步

(3) 收益

使用 Pinia 后:


3. Pinia vs Vuex 5 大优势

维度 Vuex 4 (Vue 3) Pinia (Vue 3)
API 简洁 ⭐⭐⭐ 复杂(mutations / actions) ⭐⭐⭐⭐⭐ 简洁(state / getters / actions)
TypeScript ⭐⭐⭐ 需额外配置 ⭐⭐⭐⭐⭐ 原生支持
Composition API ⭐⭐ 不友好 ⭐⭐⭐⭐⭐ 一等公民
DevTools ⭐⭐⭐⭐ ⭐⭐⭐⭐⭐ 改进
包大小 ~10KB ~1KB

Vue 官方推荐 Pinia,Vuex 4 进入维护模式(不再添加新特性)。


4. defineStore 两种风格

(1) Options 风格(类似 Vuex)

JS
// stores/counter.js
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  // state:数据
  state: () => ({
    count: 0,
    name: 'Counter'
  }),
  
  // getters:派生值(类似 computed)
  getters: {
    doubleCount: (state) => state.count * 2,
    isZero: (state) => state.count === 0
  },
  
  // actions:方法(类似 methods)
  actions: {
    increment() {
      this.count++
    },
    async fetchData() {
      const res = await fetch('/api/count')
      this.count = await res.json()
    }
  }
})

(2) Setup 风格(推荐,组合式 API)

JS
// stores/auth.js
import { defineStore } from 'pinia'
const { ref, computed } = Vue

export const useAuthStore = defineStore('auth', () => {
  // 1. state(用 ref)
  const user = ref(null)
  const token = ref(localStorage.getItem('token') || '')
  
  // 2. getters(用 computed)
  const isLoggedIn = computed(() => !!token.value)
  const userName = computed(() => user.value?.name || 'Guest')
  
  // 3. actions(普通函数)
  function login(credentials) {
    // API call...
    user.value = { name: 'Alice' }
    token.value = 'xxx'
  }
  
  function logout() {
    user.value = null
    token.value = ''
  }
  
  return { user, token, isLoggedIn, userName, login, logout }
})

(3) Options vs Setup 对比

维度 Options 风格 Setup 风格
写法 state: () => ({}) const x = ref()
getters (state) => ... computed()
actions function() { this.x } 普通函数
TypeScript 需手动 自动推断
组合能力 较弱 强(可用其他 composable)
推荐度 熟悉 Vuex 用 新项目推荐

5. 5 大核心 API

(1) state:数据

JS
// Options 风格
state: () => ({
  count: 0,
  user: null,
  items: []
})

// Setup 风格
const count = ref(0)
const user = ref(null)
const items = ref([])

(2) getters:派生值

JS
// Options 风格
getters: {
  // 简单 getter
  doubleCount: (state) => state.count * 2,
  
  // 访问其他 getter(用 this)
  ratio(state) {
    return this.doubleCount / 100
  },
  
  // 返回函数(参数化 getter)
  getItemById: (state) => (id) => {
    return state.items.find(i => i.id === id)
  }
}

// Setup 风格
const doubleCount = computed(() => count.value * 2)
const getItemById = (id) => items.value.find(i => i.id === id)

(3) actions:方法

JS
// Options 风格
actions: {
  // 同步
  increment() {
    this.count++
  },
  
  // 异步
  async fetchData() {
    const res = await fetch('/api/data')
    this.data = await res.json()
  },
  
  // 访问其他 actions
  async loginAndFetch(credentials) {
    await this.login(credentials)
    await this.fetchUser()
  }
}

// Setup 风格
function increment() {
  count.value++
}
async function fetchData() {
  const res = await fetch('/api/data')
  data.value = await res.json()
}

(4) 组件中使用

VUE
<script setup>
import { useCartStore } from '@/stores/cart'
import { storeToRefs } from 'pinia'

const cart = useCartStore()

// 1. 直接访问 state(响应式)
console.log(cart.items)

// 2. 用 storeToRefs 解构(保持响应式)
const { items, total } = storeToRefs(cart)

// 3. 调用 action
cart.addItem(product)

// 4. 监听 state 变化
watch(() => cart.items, (newItems) => {
  console.log('Cart changed:', newItems)
})
</script>

(5) 5 大注意事项

JS
// ⚠️ 注意 1:解构 state 丢失响应式
const { items } = cart  // ❌ items 是普通值
const { items } = storeToRefs(cart)  // ✅ items 是 ref

// ⚠️ 注意 2:修改 state 必须用 action
cart.items.push(...)  // ❌ 不推荐(直接修改)
cart.addItem(...)     // ✅ 用 action

// ⚠️ 注意 3:action 内的 this 指向 store
actions: {
  increment() {
    this.count++  // ✅ this = store
  }
}

// ⚠️ 注意 4:getter 缓存
getters: {
  doubleCount() {
    console.log('recomputed')  // 只在依赖变化时打印
    return this.count * 2
  }
}

// ⚠️ 注意 5:useStore 在 setup 外使用要传 pinia 实例
import { getActivePinia } from 'pinia'
const cart = useCartStore(getActivePinia())  // 在 .js 文件中

6. Pinia 持久化

(1) 安装插件

BASH
npm install pinia-plugin-persistedstate

(2) main.js 配置

JS
// main.js
const { createApp } = Vue
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'
import App from './App.vue'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

const app = createApp(App)
app.use(pinia)
app.mount('#app')

(3) 5 种持久化配置

JS
// stores/cart.js
export const useCartStore = defineStore('cart', () => {
  const items = ref([])
  
  return { items }
}, {
  // 1. 默认:localStorage key 为 'cart'
  persist: true,
  
  // 2. 自定义 key
  persist: {
    key: 'my-cart',
    storage: localStorage
  },
  
  // 3. 只持久化部分 state
  persist: {
    paths: ['items']  // 只保存 items,不保存 total
  },
  
  // 4. sessionStorage(关闭浏览器就清除)
  persist: {
    storage: sessionStorage
  },
  
  // 5. 自定义序列化(加密等)
  persist: {
    serializer: {
      serialize: (value) => btoa(JSON.stringify(value)),
      deserialize: (value) => JSON.parse(atob(value))
    }
  }
})

7. 完整示例:电商后台 5 大 Pinia Store

▶ 示例:Pinia 风格全局状态(CDN 用 provide/inject 模拟)

HTML 📖 仅展示
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

<style>
button { padding: 6px 12px; margin: 4px; background: #42b883; color: white; border: none; border-radius: 4px; cursor: pointer; }
.panel { padding: 1rem; border: 1px solid #ddd; margin: 0.5rem 0; border-radius: 4px; }
</style>

<div id="app">
  <p style="color: #999; font-size: 0.85rem;">
    Pinia 风格全局 store(CDN 用 provide/inject 模拟,3 个组件共享同一份 state)
  </p>

  <div class="panel">
    <h4>1. 计数器组件 A</h4>
    <p>count: {{ store.count }} | double: {{ store.doubleCount }}</p>
    <button @click="store.increment">+1</button>
    <button @click="store.decrement">-1</button>
    <button @click="store.reset">Reset</button>
  </div>

  <div class="panel">
    <h4>2. 计数器组件 B(共享同一 store)</h4>
    <p>count: {{ store.count }} | double: {{ store.doubleCount }}</p>
    <button @click="store.increment">+1(点我看 A 是否同步)</button>
  </div>

  <div class="panel">
    <h4>3. 购物车 store(模拟多 store 共享)</h4>
    <p>items: {{ cart.itemCount }} 件, total: ${{ cart.totalPrice }}</p>
    <button @click="cart.addItem({ name: 'iPhone', price: 999 })">加 1 件 iPhone</button>
    <button @click="cart.clear">清空</button>
  </div>
</div>

<script>
const { createApp, ref, computed, provide, inject } = Vue

// ✅ Pinia 风格 store 工厂函数(CDN 模拟)
function createPiniaStore() {
  // ----- 计数器 store -----
  const count = ref(0)
  const doubleCount = computed(() => count.value * 2)
  function increment() { count.value++ }
  function decrement() { count.value-- }
  function reset() { count.value = 0 }
  const counterStore = { count, doubleCount, increment, decrement, reset }

  // ----- 购物车 store -----
  const items = ref([])
  const itemCount = computed(() => items.value.length)
  const totalPrice = computed(() =>
    items.value.reduce((sum, i) => sum + i.price, 0)
  )
  function addItem(item) { items.value.push(item) }
  function clear() { items.value = [] }
  const cartStore = { items, itemCount, totalPrice, addItem, clear }

  return { counterStore, cartStore }
}

// 全局单例(Pinia 内部就是这样管理)
const { counterStore, cartStore } = createPiniaStore()

// provide 全局 stores
const App = {
  setup() {
    provide('counterStore', counterStore)
    provide('cartStore', cartStore)
    return {}
  },
  components: {
    CounterA: {
      template: `
        <div class="panel">
          <h4>1. 计数器组件 A</h4>
          <p>count: {{ store.count }} | double: {{ store.doubleCount }}</p>
          <button @click="store.increment">+1</button>
          <button @click="store.decrement">-1</button>
          <button @click="store.reset">Reset</button>
        </div>
      `,
      setup() {
        const store = inject('counterStore')
        return { store }
      }
    },
    CounterB: {
      template: `
        <div class="panel">
          <h4>2. 计数器组件 B(共享同一 store)</h4>
          <p>count: {{ store.count }} | double: {{ store.doubleCount }}</p>
          <button @click="store.increment">+1(点我看 A 是否同步)</button>
        </div>
      `,
      setup() {
        const store = inject('counterStore')
        return { store }
      }
    },
    CartPanel: {
      template: `
        <div class="panel">
          <h4>3. 购物车 store(多 store 共享)</h4>
          <p>items: {{ cart.itemCount }} 件, total: \${{ cart.totalPrice }}</p>
          <button @click="cart.addItem({ name: 'iPhone', price: 999 })">加 1 件 iPhone</button>
          <button @click="cart.clear">清空</button>
        </div>
      `,
      setup() {
        const cart = inject('cartStore')
        return { cart }
      }
    }
  },
  template: `
    <p style="color: #999; font-size: 0.85rem;">
      Pinia 风格全局 store(CDN 用 provide/inject 模拟,3 个组件共享同一份 state)
    </p>
    <counter-a></counter-a>
    <counter-b></counter-b>
    <cart-panel></cart-panel>
  `
}

createApp(App).mount('#app')
</script>
逻辑代码 109 行(超过 40 行限制,仅展示)
⚠️ 此为教学演示 Pinia 核心思想(单例 + provide/inject)。生产项目用官方 pinia 库(必须 Vite),提供 DevTools、TypeScript、模块化拆分等。

▶ 示例:5 种核心 API 速查(Pinia)

API 作用 例子
state 响应式状态 const count = ref(0)
getters 计算派生(缓存) computed(() => count * 2)
actions 方法(可异步) function increment() { count++ }
storeToRefs 解构保持响应 const { count } = storeToRefs(cart)
持久化 localStorage 同步 pinia-plugin-persistedstate

▶ 示例:5 个常见错误速查

错误 现象 解决
解构 state 丢失响应 数据不变 用 storeToRefs
直接改 state 警告 改用 action
异步 action 无 await 不等结果 await cart.fetchData()
pinia 未注册 useStore is not a function main.js app.use(pinia)
持久化后字段未持久 key 不对 检查 paths 配置

▶ 示例:5 大实战场景

场景 Store 关键字段
认证 useAuthStore user, token, isLoggedIn
购物车 useCartStore items, total, itemCount
主题 useThemeStore theme, locale, density
通知 useNotificationStore notifications, unread
数据 useDataStore list, loading, error

❓ 常见问题

Q Pinia 和 Vuex 选哪个?
A Pinia。Vue 官方已推荐 Pinia,Vuex 4 不再维护。新项目必须用 Pinia,老 Vuex 项目可逐步迁移。
Q Pinia 替代 provide/inject 吗?
A 不替代。Pinia 是全局状态管理,provide/inject 是跨层级通信。Pinia 用于"应用全局状态"(用户/购物车),provide/inject 用于"主题/i18n/配置"。
Q 什么时候用 Pinia vs Composable?
A Pinia 用于跨组件共享的全局状态(用户、购物车)。Composable 用于组件级复用逻辑(useMouse、useFetch)。Pinia 更结构化,有 DevTools。
Q Pinia 怎么模块化?
A 每个 store 一个文件,放在 src/stores/ 目录。Pinia 自动支持 tree-shaking。
Q storeToRefs 什么时候用?
A 解构 state 时必须用,否则丢失响应式。getters 和 actions 不需要。
Q Pinia 持久化支持什么存储?
A 默认 localStorage,也支持 sessionStorage、自定义 storage(IndexedDB、Cookie 等)、自定义序列化(加密)。
Q Pinia 性能如何?
A state 修改触发响应式更新,getter 自动缓存,DevTools 优化。比 Vuex 快 30-50%(没有 mutations 概念)。
Q Pinia 在 SSR 中能用吗?
A 能。import { createPinia } from 'pinia',每个请求创建独立实例。Nuxt 3 内置 Pinia 集成。

📖 小节


📝 作业

  1. 基础题(难度⭐) 实现一个简单的计数 store:

    • count state
    • doubleCount getter
    • increment / decrement / reset actions
    • 在 3 个组件中共享
  2. 进阶题(难度⭐⭐) 实现购物车 store:

    • items array
    • itemCount / totalPrice getters
    • addItem / removeItem / clearCart actions
    • 持久化到 localStorage
    • 在 5 个组件中测试(ProductCard / CartIcon / CartPage / Checkout / Header)
  3. 挑战题(难度⭐⭐⭐) 实现完整的电商后台 store 系统:

    1. 5 个 store:auth / cart / theme / notification / product
    2. 每个 store 完整 state + getters + actions
    3. 持久化(auth/cart/theme)
    4. Setup 风格 + TypeScript
    5. storeToRefs 解构
    6. 跨 store 调用(cart 调用 auth 检查登录)
    7. 单元测试(用 Vitest)
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏