秩序之美:Pinia 状态管理

第 1 节 概述

前面我们讲了组件通信。props、事件、provide/inject 都可以在组件之间传递数据。

但如果多个页面需要共享同一份数据呢?

比如:

  • 用户登录信息(首页、个人中心、设置页面都要用)
  • 购物车数据(商品列表页、购物车页面、结算页面都要用)

每个页面各自调接口拿数据,既浪费请求,又容易数据不一致。

Pinia 就是解决这个问题的。它是 Vue 3 的官方状态管理库,用来存储"跨组件共享的响应式数据"。

1
2
3
4
5
6
7
8
┌─────────────┐
│   Pinia     │  ← 数据统一存在这里
│  (store)    │
└─────────────┘
     ↕  ↕  ↕
┌────┐┌────┐┌────┐
│组件A││组件B││组件C│  ← 所有组件读取同一份数据
└────┘└────┘└────┘

第 2 节 安装与配置

脚手架创建项目时勾选 Pinia 会自动配置。

手动安装:

1
npm install pinia
1
2
3
4
5
6
// main.ts
import { createPinia } from 'pinia'

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

第 3 节 定义 Store

每个 store 是一个独立文件,放在 src/stores/ 目录下。

3.1 Options 写法

与 Vue 的 Options API 风格类似:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// src/stores/counter.ts
import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  // 数据(相当于 data)
  state: () => ({
    count: 0,
    name: '张三',
  }),

  // 派生数据(相当于 computed)
  getters: {
    doubleCount: (state) => state.count * 2,
  },

  // 修改方法(相当于 methods)
  actions: {
    increment() {
      this.count++
    },
  },
})

3.2 Setup 写法

与 Composition API 风格类似,更灵活:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// src/stores/counter.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  // state → ref
  const count = ref(0)
  const name = ref('张三')

  // getters → computed
  const doubleCount = computed(() => count.value * 2)

  // actions → function
  function increment() {
    count.value++
  }

  return { count, name, doubleCount, increment }
})
Options 写法Setup 写法
state: () => ({ ... })ref() / reactive()
getters: { ... }computed()
actions: { ... }普通 function

两种写法效果一样,选你习惯的就行。

第 4 节 在组件中使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<script setup lang="ts">
import { useCounterStore } from '@/stores/counter'

const store = useCounterStore()
</script>

<template>
  <p>count: {{ store.count }}</p>
  <p>double: {{ store.doubleCount }}</p>
  <button @click="store.increment()">+1</button>
</template>

使用 store 时不需要 .value,直接访问属性即可。

4.1 storeToRefs

如果你想把 state 或 getters 解构成独立的变量,直接用解构会丢失响应式:

1
2
// ❌ 丢失响应式
const { count, doubleCount } = store

需要用 storeToRefs 来保持响应式:

1
2
3
4
5
6
7
import { storeToRefs } from 'pinia'

// ✅ 保持响应式
const { count, doubleCount } = storeToRefs(store)

// actions 可以直接解构
const { increment } = store
说明

storeToRefs 只对 state 和 getters 有效。actions 可以直接解构,因为它们本来就是函数,不存在响应式的问题。

第 5 节 Getters

Getters 是用来派生数据的,相当于 store 的 computed 属性:

1
2
3
4
5
6
7
8
9
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const doubleCount = computed(() => count.value * 2)

  // getter 依赖另一个 getter
  const doubleCountPlusOne = computed(() => doubleCount.value + 1)

  return { count, doubleCount, doubleCountPlusOne }
})

第 6 节 Actions

Actions 是 store 的方法,可以包含异步操作:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
export const useUserStore = defineStore('user', () => {
  const user = ref<User | null>(null)

  // 异步 action:调接口获取用户信息
  async function fetchUser(id: number) {
    const response = await fetch(`/api/users/${id}`)
    user.value = await response.json()
  }

  // 同步 action:清除用户信息
  function clearUser() {
    user.value = null
  }

  return { user, fetchUser, clearUser }
})

在组件中使用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
<script setup lang="ts">
import { onMounted } from 'vue'
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()

onMounted(() => {
  userStore.fetchUser(1)
})
</script>

<template>
  <p>{{ userStore.user?.name }}</p>
</template>

第 7 节 一个 store 调用另一个 store

store 之间可以互相调用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
export const useCartStore = defineStore('cart', () => {
  const items = ref<CartItem[]>([])

  async function checkout() {
    const userStore = useUserStore()
    if (!userStore.user) {
      throw new Error('请先登录')
    }
    // 执行结算逻辑...
  }

  return { items, checkout }
})

第 8 节 总结

Pinia 的核心概念只有三个:

1
2
3
state    → 数据
getters  → 派生数据(类似 computed)
actions  → 修改数据的方法(支持异步)

在组件中使用时,记住两句话:

  • 直接用 store.xxx 访问,不需要 .value
  • 要解构时,state 和 getters 用 storeToRefs,actions 直接解构

Pinia 解决了"数据共享"的问题。

但有时候重复的不是数据,而是逻辑。

比如多个组件都要做"鼠标位置追踪",或者都要做"API 请求"。每个组件都写一遍很累。

这个问题由组合式函数(Composables) 来解决。