Skip to content

集中式状态管理(pinia) pinia@2.1.7

Pinia 是一个轻量级、易用的 Vue.js 状态管理库,用于 Vue 3,但也支持 Vue 2。Pinia 通过提供更简单的 API 和更少的规范来简化状态管理的过程。

1. 安装和使用

1.1 安装

bash
pnpm add pinia
bash
npm install pinia
bash
yarn add pinia

TIP

如果你的应用使用的 Vue 版本低于 2.7,你还需要安装组合式 API 包:@vue/composition-api

1.2 使用

一个简单的加减 demo, 数据存储在 store

ts
import './assets/main.css'

import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'

const pinia = createPinia()
const app = createApp(App)

app.use(pinia) 
app.mount('#app')

// 等价于下面的写法
// createApp(App).use(createPinia()).mount('#app')
html
<script lang="ts" setup>
  import { ref } from 'vue'
  import { useCountStore } from '@/store/count'

  const countStore = useCountStore()

  let n = ref(1)

  function add() {
    // 第一种写法,直接修改
    // countStore.sum += n.value

    // 第二种写法,使用$patch 批量修改
    // countStore.$patch({ sum: 888, author: '冰冻大西瓜' })

    // 第三种写法,使用action 主要方便复用业务逻辑
    countStore.increment(n.value)
  }
</script>

<template>
  <div class="count">
    <h2>当前求和为:{{ countStore.sum }}</h2>
    <h2>作者是:{{ countStore.author }},年龄:{{ countStore.age }}</h2>
    <button @click="add"></button>
  </div>
</template>
ts
import { defineStore } from 'pinia'

export const useCountStore = defineStore('count', {
  // 第三种方式,使用aciton
  actions: {
    increment(value: number) {
      if (this.sum < 10) this.sum += value
    },
  },
  state: () => ({ sum: 6, author: 'bddxg', age: 18 }),
})

TIP

在 vue2 中, 还需要安装一个插件, 详情请移步官网文档:https://pinia.vuejs.org/zh/getting-started.html#installation

为什么要在 pinia 中使用 aciton 不适用 action 相关的功能也可以在组件里实现啊

在 Pinia 中使用 actions 是出于以下几个原因:

  • 业务逻辑集中管理:将所有的数据修改逻辑放在 actions 中,可以集中管理业务逻辑。这样做的好处是,组件会变得更加简洁,只关注于展示逻辑,而所有的数据处理都在 store 中进行。这有助于提高代码的可维护性和可读性。
  • 响应式状态更新:在 actions 中修改状态时,Pinia 会自动处理响应式更新,这意味着任何依赖于这些状态的组件都会自动重新渲染。如果直接在组件中修改状态,可能需要手动处理响应性,这可能会导致潜在的 bug。
  • 可重用性和可测试性:由于 actions 是在 store 中定义的,它们可以在多个组件中重用。此外,由于它们是普通的函数,可以很容易地为它们编写单元测试,确保业务逻辑按预期工作。
  • 解耦组件和状态管理:使用 actions 可以将组件与状态管理逻辑解耦。这样,即使状态管理逻辑发生变化,组件也不需要做出太多改动。这有助于保持代码的灵活性和可扩展性。
  • 异步操作:actions 非常适合处理异步操作,如 API 调用。可以在 action 中处理异步逻辑,并在数据返回后更新状态。这样做的好处是,可以很容易地跟踪异步状态(如加载中、成功、失败),并在组件中根据这些状态进行相应的渲染。
  • 符合 Vuex 的使用习惯:Pinia 旨在提供一个类似于 Vuex 的状态管理解决方案,对于那些已经熟悉 Vuex 的开发者来说,使用 actions 可以减少学习成本,并保持一致的编码风格。

虽然确实可以在组件内部实现与 actions 相关的功能,但是这样做会违反 Vue 和 Pinia 的响应式原则,以及组件和状态管理分离的设计理念。使用 actions 可以更好地组织代码,并利用 Pinia 提供的响应式和组合式 API 的优势。

1.3 storeToRefs

用于将一个 store 中的状态和 getters 转换为响应式的 ref 对象,使得它们可以在 Vue 组件中以解构的方式使用,同时保持其响应性。

在 Vue 3 和 Pinia 的组合式 API 中,storeToRefs 非常有用,因为它允许你在组件中直接解构和使用 store 中的状态,而不需要每次都通过 store.statestore.getter 来访问。这可以提高代码的可读性和维护性。

一个小 demo

html
//src/components/Count.vue
<script lang="ts" setup>
  import { ref } from 'vue'
  import { useCountStore } from '@/stores/count'
  import { storeToRefs } from 'pinia'

  const countStore = useCountStore()
  // 虽然能够实现数据的响应式,但是开销较大,不推荐使用
  // let { sum, author, age } = toRefs(countStore)
  // console.log(toRefs(countStore))

  // 使用toRef 优化性能,但是只能获取单个属性,不能获取多个属性
  // let sum = toRef(countStore, 'sum')
  // let author = toRef(countStore, 'author')
  // let age = toRef(countStore, 'age')

  // 使用storeToRefs 优化性能,可以获取多个属性
  // storeToRefs只关注store的数据,不会对方法进行ref包裹
  let { sum, author, age } = storeToRefs(countStore)
  console.log(storeToRefs(countStore))

  function add() {
    countStore.increment(n.value)
  }

  function minus() {
    sum.value -= n.value
  }
</script>

<template>
  <div class="count">

    <h2>当前求和为:{{ sum }}</h2>
    <h2>作者是:{{ author }},年龄:{{ age }}</h2>
    <select v-model.number="n">
      <option value="1">1</option>
      <option value="2">2</option>
      <option value="3">3</option>
    </select>
    <button @click="add"></button>
    <button @click="minus"></button>
  </div>
</template>
使用 toRefs 或者 toRef 也可以实现对应的功能, 为什么要用 storeToRefs 呢?

toRefstoRef 是 Vue 3 提供的工具函数,它们可以将响应式对象(例如,通过 reactive 创建的对象)中的属性转换为单独的 ref 响应式引用。

这些函数确实可以用于将 Pinia store 中的状态和 getters 转换为 ref,从而在组件中保持其响应性。但是,storeToRefs 提供了一些额外的便利和优化,特别是在处理整个 store 实例时。

以下是 storeToRefs 相对于 toRefstoRef 的几个优势:

  1. 自动处理整个 storestoreToRefs 会自动将整个 store 中的所有状态和 getters 转换为 ref。这意味着你不需要手动指定每个属性,storeToRefs 会为你处理这一切。
  2. 保持响应性:当你使用 toRefstoRef 时,你需要确保你正在转换的对象是响应式的。而 storeToRefs 专门为 Pinia store 设计,因此它知道如何正确地处理 store 中的状态和 getters,确保它们在组件中保持响应性。
  3. 类型安全:Pinia 的 storeToRefs 在 TypeScript 中提供了更好的类型支持。它能够保持 store 中原始类型的准确性,使得在组件中使用 store 时能够获得完整的类型提示和检查。
  4. 操作简便:使用 storeToRefs 可以让你更简洁地访问和操作 store 中的数据。你不需要记住每个属性是如何被包装的(比如是 ref 还是 computed),可以直接使用它们,就像在原始 store 中一样。
  5. 维护和更新:随着 Pinia 的发展,storeToRefs 可能会引入新的特性和优化,这些都是在使用 toRefstoRef 时无法直接获得的。

总之,虽然 toRefstoRef 可以用于将 store 中的属性转换为 ref,但 storeToRefs 提供了针对 Pinia store 的专门支持和优化,使得在 Vue 组件中使用 Pinia store 更加方便和高效。

store 对象包含的信息非常庞大, 使用 toRefs 去解构数据会造成很多不必要的开销,增加了性能消耗,使用 toRef 只能结构单个数据,如果需要多个数据就要写多次,不符合代码编程规范,使用 storeToRefs 则可以一次性解构多个数据同时只有最基本的性能开销

开销对比图

1.4 getters

getters 是一种非常类似于 Vuex 中的计算属性的功能,它允许你定义一些基于当前状态的衍生数据。

getters 的主要作用是允许组件从状态管理库中获取经过处理或计算后的数据,而不是直接操作原始状态。这样做有几个好处:

  • 保持组件的精简:组件不需要处理复杂的数据逻辑,只需要从 Pinia 中获取已经处理好的数据。
  • 复用性:如果多个组件需要相同的数据逻辑,可以直接使用 getter 而不是在每个组件中复制逻辑。
  • 响应式:getter 返回的数据是响应式的,这意味着当状态发生变化时,使用 getter 的组件会自动更新。

1.4.1 选项式写法

与 Vue 的选项式 API 类似,我们也可以传入一个带有 state、actions 与 getters 属性的 Option 对象

你可以认为 state 是 store 的数据 (data),getters 是 store 的计算属性 (computed),而 actions 则是方法 (methods)。

为方便上手使用,Option Store 应尽可能直观简单。

html
<script lang="ts" setup>
  import { ref } from 'vue'
  import { useCountStore } from '@/stores/count'
  import { storeToRefs } from 'pinia'

  // 调用store中的数据
  const countStore = useCountStore()
  // 使用storeToRefs解构store数据,可以获取多个属性
  let { sum, author, age } = storeToRefs(countStore)
  let n = ref(1)

  function add() {
    // store中的action无需解构,直接调用即可
    countStore.increment(n.value)
  }

  function minus() {
    sum.value -= n.value
  }
</script>

<template>
  <div class="count">
    <h2>当前求和为:{{ sum }}</h2>
    <h2>作者是:{{ author }},年龄:{{ age }}</h2>
    <select v-model.number="n">
      <option value="1">1</option>
      <option value="2">2</option>
      <option value="3">3</option>
    </select>
    <button @click="add"></button>
    <button @click="minus"></button>
  </div>
</template>
ts
import { defineStore } from 'pinia'

// 定义一个store,用于存储数据,推荐使用use开头命名
export const useCountStore = defineStore('count', {
  actions: {
    increment(value: number) {
      if (this.sum < 10) this.sum += value
    },
  },
  state: () => ({ sum: 6, author: 'bddxg', age: 18 }),
  getters: {
    // 第一种写法 使用箭头函数
    doubleCount: (state): number => state.sum * 2,

    // 第二种写法 使用普通函数 注意这里的this指向的是当前store实例
    // 不推荐这种写法, vue3已经不推荐使用this了
    // doubleCount(): number {
    //   return this.sum * 2
    // },
  },
})

1.4.2 组合式写法

也存在另一种定义 store 的可用语法。与 Vue 组合式 API 的 setup 函数 相似,我们可以传入一个函数,该函数定义了一些响应式属性和方法,并且返回一个带有我们想暴露出去的属性和方法的对象。

在 Setup Store 中:

  • ref() 就是 state 属性
  • computed() 就是 getters
  • function() 就是 actions
html
<script lang="ts" setup>
  import { useTalkStore } from '@/stores/lovetalk'
  import { storeToRefs } from 'pinia'

  const talkStore = useTalkStore()
  const { talkList, littleTalk } = storeToRefs(talkStore)
</script>

<template>
  <div class="talk">
    <button @click="talkStore.getLoveTalk">获取一句土味情话</button>
    <!-- 这里是store的数据 -->
    <ol>
      <li v-for="talk in talkList" :key="talk.id">{{ talk.content }}</li>
    </ol>
    <hr />
    <!-- 这里是getter的数据 -->
    <ol>
      <li v-for="talk in littleTalk" :key="talk.id">{{ talk.content }}</li>
    </ol>
  </div>
</template>
ts
import { defineStore } from 'pinia'
import { reactive, computed } from 'vue'

import axios from 'axios'
import { nanoid } from 'nanoid'

export const useTalkStore = defineStore('talk', () => {
  // store
  const talkList = reactive<Talk[]>([
    { id: '0', content: '我喜欢你,就像老鼠喜欢大米' },
    { id: '1', content: '哈哈哈,就像蜜蜂喜欢花' },
    { id: '2', content: '今天你有点怪,怪好看的' },
  ])

  // action
  async function getLoveTalk() {
    try {
      const { data } = await axios.get(
        'https://api.uomg.com/api/rand.qinghua?format=json'
      )
      talkList.unshift({
        id: nanoid(),
        content: data.content,
      })
    } catch (error) {
      // Handle error
    }
  }

  // getter
  const littleTalk = computed<Talk[]>(() =>
    // map返回的值中的元素是一个对象,所以需要加括号
    talkList.map(item => ({ id: item.id, content: item.content.slice(0, 3) }))
  )

  console.log(littleTalk.value)

  return { talkList, getLoveTalk, littleTalk }
})
ts
import { defineStore } from 'pinia'
import { reactive, computed } from 'vue'

import axios from 'axios'
import { nanoid } from 'nanoid'

export const useTalkStore = defineStore('talk', {
  actions: {
    async getLoveTalk() {
      const { data } = await axios.get(
        'https://api.uomg.com/api/rand.qinghua?format=json'
      )

      this.talkList.unshift({
        id: nanoid(),
        content: data.content,
      })
    },
  },
  state() {
    return {
      talkList: reactive<Talk[]>([
        { id: '0', content: '我喜欢你,就像老鼠喜欢大米' },
        { id: '1', content: '我喜欢你,就像蜜蜂喜欢花' },
        { id: '2', content: '今天你有点怪,怪好看的' },
      ]),
    }
  },
})

Setup store 比 Option Store 带来了更多的灵活性,因为你可以在一个 store 内创建侦听器,并自由地使用任何组合式函数。不过,请记住,使用组合式函数会让 SSR 变得更加复杂。

你应该选用哪种语法?

和在 Vue 中如何选择组合式 API 与选项式 API 一样,选择你觉得最舒服的那一个就好。如果你还不确定,可以先试试 Option Store。