# 秩序之美：Pinia 状态管理


## 概述

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

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

比如：

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

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

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

```
┌─────────────┐
│   Pinia     │  ← 数据统一存在这里
│  (store)    │
└─────────────┘
     ↕  ↕  ↕
┌────┐┌────┐┌────┐
│组件A││组件B││组件C│  ← 所有组件读取同一份数据
└────┘└────┘└────┘
```

## 安装与配置

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

手动安装：

```shell
npm install pinia
```

```typescript
// main.ts
import { createPinia } from 'pinia'

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

## 定义 Store

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

### Options 写法

与 Vue 的 Options API 风格类似：

```typescript
// 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++
    },
  },
})
```

### Setup 写法

与 Composition API 风格类似，更灵活：

```typescript
// 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` |

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

## 在组件中使用

```vue
<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`，直接访问属性即可。

### storeToRefs

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

```typescript
// ❌ 丢失响应式
const { count, doubleCount } = store
```

需要用 `storeToRefs` 来保持响应式：

```typescript
import { storeToRefs } from 'pinia'

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

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

> [!info] 说明
> `storeToRefs` 只对 state 和 getters 有效。actions 可以直接解构，因为它们本来就是函数，不存在响应式的问题。

## Getters

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

```typescript
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 }
})
```

## Actions

Actions 是 store 的方法，可以包含异步操作：

```typescript
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 }
})
```

在组件中使用：

```vue
<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>
```

## 一个 store 调用另一个 store

store 之间可以互相调用：

```typescript
export const useCartStore = defineStore('cart', () => {
  const items = ref<CartItem[]>([])

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

  return { items, checkout }
})
```

## 总结

Pinia 的核心概念只有三个：

```
state    → 数据
getters  → 派生数据（类似 computed）
actions  → 修改数据的方法（支持异步）
```

在组件中使用时，记住两句话：

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

---

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

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

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

这个问题由**组合式函数（Composables）** 来解决。


---

> 作者: Aphros  
> URL: https://blog.papergate.top/posts/%E7%A7%A9%E5%BA%8F%E4%B9%8B%E7%BE%8Epinia-%E7%8A%B6%E6%80%81%E7%AE%A1%E7%90%86/  

