3、第一个Store:定义Store的两种方式(选项式 vs 组合式)、创建counter Store、在组件中使用Store
好,咱们直接动手。这一章我会带你创建第一个 Pinia Store,一个简单的计数器。别小看它,麻雀虽小五脏俱全。通过这个例子,你会彻底搞懂 Pinia 的两种定义方式——选项式(Options API)和组合式(Setup API)。
3.1 两种定义方式,你选哪种?
Pinia 给了我们两条路走。我个人习惯用组合式,因为更灵活,但选项式也有它的用武之地。咱们先看对比,再选不迟。
| 对比维度 | 选项式(Options API) | 组合式(Setup API) |
|---|---|---|
| 写法风格 | 类似 Vue 组件的 data、methods | 类似 Vue 3 的 setup 函数 |
| 状态定义 | state 函数返回对象 | ref / reactive 定义 |
| 计算属性 | getters 对象 | computed 函数 |
| 方法 | actions 对象 | 普通函数 |
| TypeScript 支持 | 需要额外类型标注 | 天然友好,自动推断 |
| 适用场景 | 简单 Store、Vue 2 迁移项目 | 复杂逻辑、需要组合函数 |
我的建议:新项目直接用组合式。我在公司重构老项目时,发现组合式在复用逻辑上简直不要太爽。但如果你团队里都是 Vue 2 的老手,选项式上手更快。
3.2 创建第一个 counter Store
先看选项式写法。说白了,就是把 Vue 组件那套搬过来。
// stores/counter.js
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++
},
decrement() {
this.count--
},
reset() {
this.count = 0
}
}
})
嗯,这里要注意。选项式的 state 必须是一个箭头函数,返回一个对象。为什么?因为这样才能保证每个组件实例都有独立的状态,不会互相污染。我曾经见过有人直接写 state: { count: 0 },结果所有组件共享同一个 count,改一个全变了——这坑我踩过。
再看组合式写法。这个更接近我们写普通 JavaScript 的感觉。
// stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
// 状态:用 ref
const count = ref(0)
const name = ref('计数器')
// 计算属性:用 computed
const doubleCount = computed(() => count.value * 2)
// 方法:普通函数
function increment() {
count.value++
}
function decrement() {
count.value--
}
function reset() {
count.value = 0
}
// 必须返回所有暴露的内容
return {
count,
name,
doubleCount,
increment,
decrement,
reset
}
})
核心区别:组合式里没有 this,所有状态和方法都要通过 return 暴露出去。你想想看,这其实就是一个闭包,每个 Store 实例都是独立的。
3.3 在组件中使用 Store
好了,Store 写好了,怎么在组件里用?其实就三步:引入、调用、使用。
<template>
<div class="counter">
<h2>{{ counterStore.name }}</h2>
<p>当前值:{{ counterStore.count }}</p>
<p>双倍值:{{ counterStore.doubleCount }}</p>
<button @click="counterStore.increment">+1</button>
<button @click="counterStore.decrement">-1</button>
<button @click="counterStore.reset">重置</button>
</div>
</template>
<script setup>
import { useCounterStore } from '@/stores/counter'
// 1. 调用 useCounterStore 获取 Store 实例
const counterStore = useCounterStore()
</script>
就这么简单。你可能会问:为什么不用 storeToRefs 解构?嗯,这里有个小细节。直接解构会丢失响应性,比如 const { count } = counterStore,count 就变成普通值了,不会自动更新。如果你非要解构,得用 Pinia 提供的 storeToRefs。
import { storeToRefs } from 'pinia'
const counterStore = useCounterStore()
// 这样解构才能保持响应性
const { count, doubleCount } = storeToRefs(counterStore)
// 方法可以直接解构,不需要 storeToRefs
const { increment } = counterStore
注意:只有 state 和 getters 需要用 storeToRefs 解构。actions 本身就是函数,直接解构没问题。我曾经在项目里把 actions 也用 storeToRefs 包了一层,结果方法调用不生效,排查了半天才发现是这里搞错了。
3.4 两种方式,怎么选?
我个人的经验是:
- 选选项式:如果你是从 Vue 2 迁移过来的项目,团队成员习惯了 options API,或者 Store 逻辑非常简单(就几个 state 和 actions)。
- 选组合式:新项目、需要 TypeScript 支持、或者 Store 逻辑复杂需要拆分复用。组合式可以轻松把逻辑提取到单独的函数里,比如把计数逻辑抽成一个
useCounter()组合函数。
说白了,没有绝对的好坏。我见过用选项式写出很优雅的代码,也见过组合式写得一团糟的。关键看团队和场景。
小技巧:如果你不确定选哪种,先写组合式。因为组合式可以调用选项式 Store,反过来不行。而且组合式更接近 Vue 3 的设计哲学,学一次用到底。
这一章的内容就到这。你可以在组件里试试看,把 counter Store 用起来。下一章我们会深入 Pinia 的核心概念——state,看看怎么管理、修改和监听状态变化。