如何利用`Pinia`的`getters`与`actions`进行状态管理?

Pinia Getters 和 Actions:状态管理的强大组合

大家好,今天我们来深入探讨 Pinia 中 gettersactions 的使用,以及它们如何共同构建强大且易于维护的状态管理解决方案。Pinia 作为 Vue.js 生态系统中备受欢迎的状态管理库,以其轻量级、类型安全和易于使用的特性而著称。gettersactions 是 Pinia store 中两个至关重要的组成部分,它们分别负责状态的派生和状态的修改。

理解 Getters:从状态中派生数据

getters 本质上是 store 的计算属性。它们接收 state 作为参数,并返回基于 state 的计算结果。 与直接在组件中使用计算属性相比,getters 的主要优势在于它们允许我们将计算逻辑集中在 store 中,从而实现代码的复用和逻辑的统一管理。 此外,Pinia 的 getters 会被缓存,只有当依赖的 state 发生变化时才会重新计算,从而提高性能。

Getters 的定义和使用

在 Pinia store 中定义 getters 非常简单。 我们只需要在 defineStore 方法的配置对象中添加 getters 属性,该属性是一个对象,其中每个键值对代表一个 getter。 每个 getter 都是一个函数,它接收 state 作为第一个参数,并返回计算结果。

import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: 'Eduardo'
  }),
  getters: {
    doubleCount: (state) => state.count * 2,
    doubleCountPlusOne(): number {
      return this.doubleCount + 1
    },
    greeting: (state) => `Hello, ${state.name}!`,
    // Getter 可以接收其他 getter 作为参数
    evenOrOdd: (state): string => {
      return state.count % 2 === 0 ? 'even' : 'odd'
    }
  },
})

在这个例子中,我们定义了四个 getters:

  • doubleCount: 返回 count 的两倍。
  • doubleCountPlusOne: 返回 doubleCount 的值加一。 这个例子展示了 getter 之间如何互相调用,使用 this 关键字。
  • greeting: 返回一个基于 name 的问候语。
  • evenOrOdd: 根据 count 的奇偶性返回 "even" 或 "odd"。

在组件中使用 getters 也很简单。 我们只需要通过 useStore hook 获取 store 实例,然后就可以像访问 store 的属性一样访问 getters。

<template>
  <p>Count: {{ counterStore.count }}</p>
  <p>Double Count: {{ counterStore.doubleCount }}</p>
  <p>Double Count Plus One: {{ counterStore.doubleCountPlusOne }}</p>
  <p>Greeting: {{ counterStore.greeting }}</p>
  <p>Even or Odd: {{ counterStore.evenOrOdd }}</p>
</template>

<script setup lang="ts">
import { useCounterStore } from './stores/counter'

const counterStore = useCounterStore()
</script>

Getter 的类型推断

Pinia 提供了强大的类型推断功能,可以自动推断 getter 的返回值类型。 这可以帮助我们避免类型错误,并提高代码的可维护性。 如果需要显式指定 getter 的返回值类型,可以使用类型注解,如上面的 doubleCountPlusOne(): number

Getter 的使用场景

getters 在状态管理中有很多有用的场景,以下是一些常见的例子:

  • 数据转换: 将原始状态转换为更适合组件使用的格式。 例如,将日期格式化为特定的字符串。
  • 数据过滤: 根据特定的条件过滤状态中的数据。 例如,筛选出所有已完成的任务。
  • 数据聚合: 对状态中的数据进行聚合计算。 例如,计算所有订单的总金额。
  • 权限控制: 根据用户的角色和权限,判断用户是否有权访问特定的功能。

Getter 的参数化

getter 也可以接收参数。这使得 getter 更加灵活,可以根据不同的参数返回不同的结果。

import { defineStore } from 'pinia'

export const useProductStore = defineStore('product', {
  state: () => ({
    products: [
      { id: 1, name: 'Apple', price: 1 },
      { id: 2, name: 'Banana', price: 0.5 },
      { id: 3, name: 'Orange', price: 0.75 }
    ]
  }),
  getters: {
    // Getter 接收 state 作为第一个参数
    // 函数接收第二个参数,它是一个函数,并且返回一个函数
    productById: (state) => {
      return (productId: number) => state.products.find(product => product.id === productId)
    },
    expensiveProducts: (state) => {
      return (minPrice: number) => state.products.filter(product => product.price > minPrice)
    }
  }
})

在这个例子中,productById getter 接收一个 productId 参数,并返回与该 ID 匹配的产品。 注意,getter 变成了一个返回函数的函数。

在组件中使用带参数的 getter:

<template>
  <p>Product with ID 2: {{ productStore.productById(2)?.name }}</p>
  <ul>
    <li v-for="product in productStore.expensiveProducts(0.6)" :key="product.id">
      {{ product.name }} - ${{ product.price }}
    </li>
  </ul>
</template>

<script setup lang="ts">
import { useProductStore } from './stores/product'

const productStore = useProductStore()
</script>

Getters 与计算属性的比较

特性 Getter 计算属性
位置 定义在 Pinia store 中 定义在 Vue 组件中
状态访问 接收 state 作为参数 可以直接访问组件的 data 和 props
复用性 可以在多个组件中复用 只能在当前组件中使用
缓存 会被缓存,只有依赖的 state 发生变化才会重新计算 会被缓存,只有依赖的 data 和 props 发生变化才会重新计算
类型安全性 Pinia 提供类型推断,保证类型安全 需要手动指定类型,容易出错
适用场景 适用于需要在多个组件中共享的计算逻辑 适用于只在当前组件中使用的计算逻辑

总而言之,getters 适用于需要在多个组件中共享的计算逻辑,并且需要进行状态管理。而计算属性则适用于只在当前组件中使用的计算逻辑,并且不需要进行状态管理。

理解 Actions:修改状态的唯一途径

actions 是 Pinia store 中用于修改 state 的方法。 它们是修改 state 的唯一途径,确保了状态的可预测性和可追踪性。 Actions 可以包含任意的异步逻辑,例如发送 API 请求、处理用户输入等。

Actions 的定义和使用

在 Pinia store 中定义 actions 与定义 getters 类似。 我们只需要在 defineStore 方法的配置对象中添加 actions 属性,该属性是一个对象,其中每个键值对代表一个 action。 每个 action 都是一个函数,它可以接收任意数量的参数。

import { defineStore } from 'pinia'
import axios from 'axios'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    name: 'Eduardo',
    loading: false
  }),
  actions: {
    increment() {
      this.count++
    },
    decrement() {
      this.count--
    },
    incrementBy(amount: number) {
      this.count += amount
    },
    async fetchName() {
      this.loading = true
      try {
        const response = await axios.get('https://api.example.com/name')
        this.name = response.data.name
      } catch (error) {
        console.error('Failed to fetch name:', error)
      } finally {
        this.loading = false
      }
    },
    reset() {
      // You can mutate the state in multiple ways
      Object.assign(this.$state, { count: 0, name: 'Initial Name' })
    }
  }
})

在这个例子中,我们定义了五个 actions:

  • increment: 将 count 的值加 1。
  • decrement: 将 count 的值减 1。
  • incrementBy: 将 count 的值加上指定的 amount
  • fetchName: 从 API 获取 name,并更新 state。这个例子展示了如何处理异步操作。
  • reset: 将 state 重置为初始值。 这个例子展示了如何使用 this.$state 来批量更新 state。

在组件中使用 actions 也很简单。 我们只需要通过 useStore hook 获取 store 实例,然后就可以像调用 store 的方法一样调用 actions。

<template>
  <p>Count: {{ counterStore.count }}</p>
  <button @click="counterStore.increment()">Increment</button>
  <button @click="counterStore.decrement()">Decrement</button>
  <button @click="counterStore.incrementBy(5)">Increment by 5</button>
  <button @click="counterStore.fetchName()">Fetch Name</button>
  <p v-if="counterStore.loading">Loading...</p>
  <p>Name: {{ counterStore.name }}</p>
  <button @click="counterStore.reset()">Reset</button>
</template>

<script setup lang="ts">
import { useCounterStore } from './stores/counter'

const counterStore = useCounterStore()
</script>

Actions 中的 this 上下文

actions 中,this 指向 store 实例。 这使得我们可以访问 store 的 state 和 getters,以及调用其他的 actions。

Actions 的类型推断

Pinia 提供了强大的类型推断功能,可以自动推断 action 的参数类型和返回值类型。 这可以帮助我们避免类型错误,并提高代码的可维护性。

Actions 的使用场景

actions 在状态管理中有很多有用的场景,以下是一些常见的例子:

  • 用户交互: 处理用户的输入,例如提交表单、点击按钮等。
  • API 请求: 发送 API 请求,并更新 state。
  • 数据持久化: 将 state 持久化到本地存储或服务器。
  • 状态同步: 与其他 store 或组件同步 state。

订阅 Actions

Pinia 允许我们订阅 actions 的执行,以便在 action 执行前后执行一些额外的逻辑。 这对于记录日志、发送分析数据等场景非常有用。

可以使用 store.$onAction() 方法来订阅 action。 该方法接收一个回调函数作为参数,该回调函数会在 action 执行前后被调用。

import { useCounterStore } from './stores/counter'
import { onMounted } from 'vue';

export default {
  setup() {
    const counterStore = useCounterStore();

    onMounted(() => {
      const unsubscribe = counterStore.$onAction(
        ({
          name, // action 的名称
          store, // store 实例,`this`
          args,  // 传递给 action 的参数数组
          onError, // 设置 action 抛出错误时的回调
          onAfter, // 设置 action 成功执行后的回调
        }) => {
          console.log(`Action "${name}" started with args ${args.join(', ')}.`)

          onError((error) => {
            console.error(`Action "${name}" failed with error:`, error)
          })

          onAfter(() => {
            console.log(`Action "${name}" finished.`)
          })
        }
      )

      // 在组件卸载时取消订阅
      // unsubscribe()
    })

    return { counterStore }
  },
  template: `
    <p>Count: {{ counterStore.count }}</p>
    <button @click="counterStore.increment()">Increment</button>
  `
}

Actions 与 Mutations 的比较 (Vuex)

如果你熟悉 Vuex,你可能会注意到 Pinia 没有 mutations 的概念。 在 Vuex 中,mutations 负责同步地修改 state,而 actions 负责提交 mutations。 Pinia 简化了这一过程,直接在 actions 中修改 state。 这使得代码更加简洁,并且更容易理解。

特性 Action Mutation (Vuex)
目的 修改 state 同步地修改 state
异步操作 可以包含异步操作 只能包含同步操作
调用方式 直接调用 通过 commit 方法提交
类型安全性 Pinia 提供类型推断,保证类型安全 需要手动指定类型,容易出错
调试 更容易调试,可以直接追踪 action 的执行 需要追踪 mutation 的提交,相对复杂

总结 Actions 的最佳实践

  • 保持 actions 的简洁性: 尽量将 actions 拆分成更小的、易于理解的函数。
  • 使用异步操作: 如果 actions 涉及到异步操作,例如 API 请求,请使用 async/await 语法。
  • 处理错误: 在 actions 中处理可能发生的错误,例如网络错误、服务器错误等。
  • 避免直接修改 state: 虽然可以在 actions 中直接修改 state,但是建议使用 this.$patch 方法来批量更新 state,以提高性能。

组合 Getters 和 Actions:构建复杂的状态管理逻辑

gettersactions 可以一起使用,构建复杂的状态管理逻辑。 例如,我们可以使用 getters 来计算 derived state,然后使用 actions 来修改 state,从而更新 derived state。

import { defineStore } from 'pinia'
import axios from 'axios'

export const useTodosStore = defineStore('todos', {
  state: () => ({
    todos: [],
    loading: false,
    error: null
  }),
  getters: {
    completedTodos: (state) => state.todos.filter(todo => todo.completed),
    pendingTodos: (state) => state.todos.filter(todo => !todo.completed),
    totalTodos: (state) => state.todos.length
  },
  actions: {
    async fetchTodos() {
      this.loading = true
      this.error = null
      try {
        const response = await axios.get('https://jsonplaceholder.typicode.com/todos')
        this.todos = response.data
      } catch (error) {
        this.error = error.message
      } finally {
        this.loading = false
      }
    },
    async addTodo(title: string) {
      try {
        const response = await axios.post('https://jsonplaceholder.typicode.com/todos', {
          title,
          completed: false
        })
        this.todos.push(response.data)
      } catch (error) {
        this.error = error.message
      }
    },
    async toggleTodo(id: number) {
      const todo = this.todos.find(todo => todo.id === id)
      if (todo) {
        try {
          await axios.patch(`https://jsonplaceholder.typicode.com/todos/${id}`, {
            completed: !todo.completed
          })
          todo.completed = !todo.completed // Optimistically update the state
        } catch (error) {
          this.error = error.message
          // Revert the state change if the API call fails
          todo.completed = !todo.completed
        }
      }
    },
    clearCompletedTodos() {
      this.todos = this.todos.filter(todo => !todo.completed)
    }
  }
})

在这个例子中,我们定义了一个 todos store,它包含以下 state、getters 和 actions:

  • state:
    • todos: 存储所有 todo 事项的数组。
    • loading: 表示是否正在加载 todo 事项。
    • error: 存储错误信息。
  • getters:
    • completedTodos: 返回所有已完成的 todo 事项。
    • pendingTodos: 返回所有未完成的 todo 事项。
    • totalTodos: 返回 todo 事项的总数。
  • actions:
    • fetchTodos: 从 API 获取 todo 事项,并更新 state。
    • addTodo: 添加一个新的 todo 事项,并更新 state。
    • toggleTodo: 切换 todo 事项的完成状态,并更新 state。
    • clearCompletedTodos: 清除所有已完成的 todo 事项,并更新 state。

这个例子展示了如何使用 getters 来派生状态,然后使用 actions 来修改状态,从而更新派生状态。

小结:Getters 和 Actions 的强大作用

总的来说,Pinia 的 gettersactions 为我们提供了一种清晰、简洁且易于维护的状态管理方式。 通过 getters,我们可以从状态中派生数据,而通过 actions,我们可以安全地修改状态。 它们是构建复杂 Vue.js 应用的重要组成部分。

一些建议:良好实践成就优秀代码

  • 清晰命名: 使用清晰、描述性的名称来命名 gettersactions,以便于理解和维护。
  • 单一职责: 每个 getteraction 应该只负责一个特定的任务。
  • 测试:gettersactions 编写单元测试,以确保它们的功能正确。
  • 类型安全: 尽可能使用类型注解,以提高代码的类型安全性。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注