第 1 节 概述
前面我们讲了组件通信。props、事件、provide/inject 都可以在组件之间传递数据。
但如果多个页面需要共享同一份数据呢?
比如:
- 用户登录信息(首页、个人中心、设置页面都要用)
- 购物车数据(商品列表页、购物车页面、结算页面都要用)
每个页面各自调接口拿数据,既浪费请求,又容易数据不一致。
Pinia 就是解决这个问题的。它是 Vue 3 的官方状态管理库,用来存储"跨组件共享的响应式数据"。
1
2
3
4
5
6
7
8
| ┌─────────────┐
│ Pinia │ ← 数据统一存在这里
│ (store) │
└─────────────┘
↕ ↕ ↕
┌────┐┌────┐┌────┐
│组件A││组件B││组件C│ ← 所有组件读取同一份数据
└────┘└────┘└────┘
|
第 2 节 安装与配置
脚手架创建项目时勾选 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) 来解决。