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,看看怎么管理、修改和监听状态变化。