@extends('layouts.app_copy') Vue.js Pinia Actions 教程 - 动作方法与异步操作 @

Vue.js Pinia Actions

什么是 Pinia Actions?

Actions 是 Pinia 中处理业务逻辑的地方,类似于组件中的方法。它们可以用来执行同步或异步操作,修改状态(通过 this 直接修改 state),或调用其他 actions。

Actions 与 Getters 的区别: Actions 用于修改状态和处理逻辑,Getters 用于计算派生状态(类似于计算属性)。

定义 Actions

在 Pinia store 中,actions 定义在 actions 属性中:

import { defineStore } from 'pinia'

export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    user: null,
    loading: false
  }),

  actions: {
    // 同步 action
    increment() {
      this.count++
    },

    // 带参数的 action
    incrementBy(amount) {
      this.count += amount
    },

    // 异步 action
    async fetchUser(userId) {
      this.loading = true
      try {
        const response = await fetch(`https://api.example.com/users/${userId}`)
        this.user = await response.json()
      } catch (error) {
        console.error('Error fetching user:', error)
      } finally {
        this.loading = false
      }
    }
  }
})

在组件中使用 Actions

直接调用

<template>
  <div>
    <p>Count: {{ count }}</p>
    <button @click="increment">+1</button>
    <button @click="incrementBy(5)">+5</button>
    <button @click="fetchUser(1)">获取用户</button>

    <div v-if="loading">加载中...</div>
    <div v-else-if="user">
      用户名: {{ user.name }}
    </div>
  </div>
</template>

<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'

const counterStore = useCounterStore()

// 解构 store(保持响应性)
const { count, user, loading } = storeToRefs(counterStore)

// 解构 actions
const { increment, incrementBy, fetchUser } = counterStore
</script>

使用 setup() 语法

<script>
import { useCounterStore } from '@/stores/counter'

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

    const handleClick = async () => {
      await counterStore.fetchUser(1)
      console.log('User fetched:', counterStore.user)
    }

    return {
      counterStore,
      handleClick
    }
  }
}
</script>

进阶用法

调用其他 Actions

actions: {
  async login(credentials) {
    const response = await this.authRequest(credentials)
    this.setUser(response.user)
    this.setToken(response.token)
  },

  authRequest(credentials) {
    return fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify(credentials)
    }).then(res => res.json())
  },

  setUser(user) {
    this.user = user
  },

  setToken(token) {
    this.token = token
  }
}

使用 $patch 批量更新

actions: {
  updateProfile(data) {
    // 方式1:直接修改
    this.name = data.name
    this.email = data.email
    this.avatar = data.avatar

    // 方式2:使用 $patch 批量更新(性能更好)
    this.$patch({
      name: data.name,
      email: data.email,
      avatar: data.avatar
    })

    // 方式3:使用 $patch 函数
    this.$patch((state) => {
      state.name = data.name
      state.email = data.email
      state.avatar = data.avatar
    })
  }
}

组合多个 Store 的 Actions

import { useAuthStore } from './authStore'
import { useCartStore } from './cartStore'

export const useOrderStore = defineStore('order', {
  actions: {
    async checkout() {
      const authStore = useAuthStore()
      const cartStore = useCartStore()

      if (!authStore.isAuthenticated) {
        throw new Error('请先登录')
      }

      const order = {
        userId: authStore.user.id,
        items: cartStore.items,
        total: cartStore.total
      }

      // 调用 API
      const response = await this.createOrder(order)

      // 清空购物车
      cartStore.clearCart()

      return response
    },

    async createOrder(orderData) {
      // API 调用
    }
  }
})

最佳实践

推荐做法
  • 为异步操作添加加载状态和错误处理
  • 保持 actions 单一职责,每个 action 只做一件事
  • 对于复杂的业务逻辑,拆分成多个小 actions
  • 使用 TypeScript 获得更好的类型提示
  • 在 actions 中进行表单验证和数据处理
避免的做法
  • 不要在 actions 中直接操作 DOM
  • 避免在 actions 中进行复杂的组件逻辑
  • 不要过度使用全局 store,考虑组件状态是否更合适
  • 避免冗长的 actions,超过 50 行考虑拆分

完整示例:Todo Store

// stores/todo.js
import { defineStore } from 'pinia'

export const useTodoStore = defineStore('todo', {
  state: () => ({
    todos: [],
    filter: 'all',
    loading: false,
    error: null
  }),

  getters: {
    filteredTodos(state) {
      if (state.filter === 'active') {
        return state.todos.filter(todo => !todo.completed)
      }
      if (state.filter === 'completed') {
        return state.todos.filter(todo => todo.completed)
      }
      return state.todos
    },

    pendingCount(state) {
      return state.todos.filter(todo => !todo.completed).length
    }
  },

  actions: {
    async fetchTodos() {
      this.loading = true
      this.error = null

      try {
        const response = await fetch('/api/todos')
        this.todos = await response.json()
      } catch (error) {
        this.error = error.message
        console.error('Failed to fetch todos:', error)
      } finally {
        this.loading = false
      }
    },

    async addTodo(title) {
      if (!title.trim()) return

      const todo = {
        id: Date.now(),
        title,
        completed: false,
        createdAt: new Date().toISOString()
      }

      try {
        // 乐观更新
        this.todos.unshift(todo)

        // 实际 API 调用
        await fetch('/api/todos', {
          method: 'POST',
          body: JSON.stringify(todo)
        })
      } catch (error) {
        // 失败时回滚
        this.todos = this.todos.filter(t => t.id !== todo.id)
        this.error = '添加失败,请重试'
      }
    },

    async toggleTodo(id) {
      const todo = this.todos.find(t => t.id === id)
      if (todo) {
        const originalCompleted = todo.completed

        // 乐观更新
        todo.completed = !todo.completed

        try {
          await fetch(`/api/todos/${id}`, {
            method: 'PATCH',
            body: JSON.stringify({ completed: todo.completed })
          })
        } catch (error) {
          // 失败时恢复
          todo.completed = originalCompleted
          this.error = '更新失败,请重试'
        }
      }
    },

    setFilter(filter) {
      if (['all', 'active', 'completed'].includes(filter)) {
        this.filter = filter
      }
    },

    clearCompleted() {
      const completedIds = this.todos
        .filter(todo => todo.completed)
        .map(todo => todo.id)

      // 移除本地
      this.todos = this.todos.filter(todo => !todo.completed)

      // 批量删除 API 调用
      completedIds.forEach(id => {
        fetch(`/api/todos/${id}`, { method: 'DELETE' })
      })
    }
  }
})

总结

Pinia Actions 是处理业务逻辑的核心,主要特点:

  • 灵活性:支持同步和异步操作
  • 可组合性:可以调用其他 actions
  • 直接访问 state:通过 this 访问和修改状态
  • TypeScript 友好:完整的类型推断
  • 易测试:可以单独测试每个 action

通过合理使用 Actions,可以使状态管理更加清晰、可维护,同时保持组件逻辑的简洁。