MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Vue3中使用Vuex管理网络请求状态

2023-12-318.0k 阅读

1. Vuex基础概念回顾

在深入探讨Vue3中使用Vuex管理网络请求状态之前,我们先来回顾一下Vuex的基础概念。Vuex是Vue.js应用程序的状态管理模式 + 库。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

1.1 Vuex的核心概念

  • State:Vuex使用单一状态树,即每个应用将仅仅包含一个store实例。所有的状态都保存在这个单一的状态树中。例如,在一个简单的待办事项应用中,待办事项列表就可以作为state中的一个属性。
  • Getter:可以认为是store的计算属性。就像计算属性一样,getter的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。例如,我们有一个待办事项列表,我们可以通过getter来计算已完成的待办事项数量。
  • Mutation:更改Vuex的store中的状态的唯一方法是提交mutation。Vuex中的mutation非常类似于事件:每个mutation都有一个字符串的事件类型 (type) 和 一个回调函数 (handler)。这个回调函数就是我们实际进行状态更改的地方,并且会接受state作为第一个参数。例如,我们可以定义一个mutation来添加新的待办事项到列表中。
  • Action:Action类似于mutation,不同在于:Action提交的是mutation,而不是直接变更状态。Action可以包含任意异步操作。例如,在处理网络请求时,我们可以在action中发起请求,请求成功后再提交mutation来更新状态。
  • Module:由于使用单一状态树,应用的所有状态会集中到一个比较大的对象。当应用变得非常复杂时,store对象就有可能变得相当臃肿。为了解决以上问题,Vuex允许我们将store分割成模块(module)。每个模块拥有自己的state、mutation、action、getter,甚至是嵌套子模块。

2. Vue3与Vuex的集成

2.1 安装Vuex

在Vue3项目中使用Vuex,首先需要安装Vuex库。如果你的项目使用npm管理依赖,可以在项目根目录下执行以下命令:

npm install vuex@next

这里使用@next是因为Vuex 4.x 是为Vue3设计的版本。

2.2 创建Vuex Store

在项目中创建一个store目录,并在该目录下创建index.js文件,这将是我们Vuex的入口文件。

import { createStore } from 'vuex'

// 创建store实例
const store = createStore({
  state() {
    return {
      // 这里定义我们的状态
      userInfo: null
    }
  },
  mutations: {
    // 定义更改状态的mutation
    SET_USER_INFO(state, payload) {
      state.userInfo = payload
    }
  },
  actions: {
    // 定义action
    async fetchUserInfo({ commit }) {
      try {
        // 模拟网络请求
        const response = await new Promise((resolve) => {
          setTimeout(() => {
            resolve({ name: 'John', age: 30 })
          }, 1000)
        })
        commit('SET_USER_INFO', response)
      } catch (error) {
        console.error('Error fetching user info:', error)
      }
    }
  },
  getters: {
    // 定义getter
    getUserInfo(state) {
      return state.userInfo
    }
  }
})

export default store

在上述代码中:

  • 我们通过createStore函数创建了一个Vuex store实例。
  • state中定义了一个userInfo属性来存储用户信息。
  • mutations中的SET_USER_INFO mutation用于更新userInfo状态。
  • actions中的fetchUserInfo action模拟了一个异步网络请求,请求成功后通过commit提交SET_USER_INFO mutation来更新状态。
  • getters中的getUserInfo用于获取userInfo状态。

2.3 在Vue3应用中使用Vuex

在Vue3的main.js文件中,引入并使用刚刚创建的store。

import { createApp } from 'vue'
import App from './App.vue'
import store from './store'

const app = createApp(App)
app.use(store)
app.mount('#app')

这样,我们就成功地将Vuex集成到了Vue3应用中,在组件中就可以使用Vuex提供的状态、mutation、action和getter了。

3. 使用Vuex管理网络请求状态的优势

3.1 集中化管理

在一个大型的Vue3应用中,可能会有多个组件发起网络请求。如果每个组件都自己管理请求的状态(如加载中、请求成功、请求失败),会导致代码冗余且难以维护。使用Vuex可以将所有网络请求的状态集中管理在store中,所有组件都可以从store中获取状态,使得状态管理更加清晰和统一。

3.2 可预测性

Vuex通过mutation来更新状态,并且mutation必须是同步函数。这使得状态的变化是可预测的。当处理网络请求时,我们可以通过action来发起异步请求,请求成功或失败后再通过mutation来更新状态,从而保证状态变化的可预测性。例如,在请求数据时,我们可以先通过mutation将加载状态设置为true,请求成功后将加载状态设置为false并更新数据,请求失败时也将加载状态设置为false并处理错误信息。

3.3 组件间共享状态

在Vue应用中,兄弟组件之间共享数据往往比较麻烦。而使用Vuex,所有组件都可以访问store中的状态。当一个组件发起网络请求并更新了store中的状态后,其他组件可以立即获取到最新状态。比如,一个用户登录组件发起登录请求,登录成功后更新了store中的用户登录状态,其他需要根据用户登录状态显示不同内容的组件就可以直接从store中获取该状态并作出相应的显示。

4. 具体实现网络请求状态管理

4.1 定义网络请求状态

state中定义网络请求的状态,例如加载中、请求成功、请求失败等状态。

state() {
  return {
    loading: false,
    error: null,
    data: null
  }
}
  • loading用于表示当前是否正在进行网络请求,true表示正在加载,false表示加载完成。
  • error用于存储网络请求过程中发生的错误信息,如果没有错误则为null
  • data用于存储网络请求成功返回的数据,如果请求还未成功或者请求失败则为null

4.2 定义Mutation

为网络请求状态的变化定义相应的mutation。

mutations: {
  SET_LOADING(state, payload) {
    state.loading = payload
  },
  SET_ERROR(state, payload) {
    state.error = payload
  },
  SET_DATA(state, payload) {
    state.data = payload
  }
}
  • SET_LOADING mutation用于设置loading状态。
  • SET_ERROR mutation用于设置error状态。
  • SET_DATA mutation用于设置data状态。

4.3 定义Action

在action中发起网络请求,并根据请求结果提交相应的mutation。这里以Axios库为例,假设我们要获取一篇文章的详情。

import axios from 'axios'

actions: {
  async fetchArticle({ commit }, articleId) {
    commit('SET_LOADING', true)
    try {
      const response = await axios.get(`/api/articles/${articleId}`)
      commit('SET_DATA', response.data)
      commit('SET_ERROR', null)
    } catch (error) {
      commit('SET_ERROR', error.message)
    } finally {
      commit('SET_LOADING', false)
    }
  }
}

在上述代码中:

  • 首先通过commit('SET_LOADING', true)loading状态设置为true,表示开始加载。
  • 然后使用Axios发起网络请求,请求成功后通过commit('SET_DATA', response.data)将返回的数据存储到data状态中,并通过commit('SET_ERROR', null)error状态设置为null
  • 如果请求失败,通过commit('SET_ERROR', error.message)将错误信息存储到error状态中。
  • 无论请求成功还是失败,最后都通过commit('SET_LOADING', false)loading状态设置为false,表示加载结束。

4.4 在组件中使用

在Vue3组件中,可以通过useStore组合式函数来使用Vuex中的状态、mutation和action。

<template>
  <div>
    <h1>Article Detail</h1>
    <div v-if="loading">Loading...</div>
    <div v-if="error">{{ error }}</div>
    <div v-if="data">
      <h2>{{ data.title }}</h2>
      <p>{{ data.content }}</p>
    </div>
    <button @click="fetchArticle(1)">Fetch Article</button>
  </div>
</template>

<script setup>
import { useStore } from 'vuex'

const store = useStore()
const loading = computed(() => store.state.loading)
const error = computed(() => store.state.error)
const data = computed(() => store.state.data)

const fetchArticle = (articleId) => {
  store.dispatch('fetchArticle', articleId)
}
</script>

在上述组件中:

  • 通过useStore获取Vuex的store实例。
  • 使用computed计算属性来获取loadingerrordata状态。
  • 定义fetchArticle函数,通过store.dispatch('fetchArticle', articleId)来触发fetchArticle action,从而发起网络请求并更新状态。组件会根据不同的状态显示相应的内容,如加载中显示“Loading...”,请求失败显示错误信息,请求成功显示文章详情。

5. 处理多个网络请求状态

在实际应用中,可能会同时有多个网络请求在进行,或者不同模块有各自的网络请求。这时,我们需要对不同的网络请求状态进行区分和管理。

5.1 按模块管理

可以通过Vuex的模块机制,将不同模块的网络请求状态分开管理。例如,一个电商应用可能有商品模块、用户模块等,每个模块都有自己的网络请求。

// store/modules/product.js
import { defineStore } from 'pinia'

const useProductStore = defineStore('product', {
  state: () => ({
    productLoading: false,
    productError: null,
    productData: null
  }),
  mutations: {
    SET_PRODUCT_LOADING(state, payload) {
      state.productLoading = payload
    },
    SET_PRODUCT_ERROR(state, payload) {
      state.productError = payload
    },
    SET_PRODUCT_DATA(state, payload) {
      state.productData = payload
    }
  },
  actions: {
    async fetchProduct({ commit }, productId) {
      commit('SET_PRODUCT_LOADING', true)
      try {
        const response = await axios.get(`/api/products/${productId}`)
        commit('SET_PRODUCT_DATA', response.data)
        commit('SET_PRODUCT_ERROR', null)
      } catch (error) {
        commit('SET_PRODUCT_ERROR', error.message)
      } finally {
        commit('SET_PRODUCT_LOADING', false)
      }
    }
  }
})

export default useProductStore
// store/modules/user.js
import { defineStore } from 'pinia'

const useUserStore = defineStore('user', {
  state: () => ({
    userLoading: false,
    userError: null,
    userData: null
  }),
  mutations: {
    SET_USER_LOADING(state, payload) {
      state.userLoading = payload
    },
    SET_USER_ERROR(state, payload) {
      state.userError = payload
    },
    SET_USER_DATA(state, payload) {
      state.userData = payload
    }
  },
  actions: {
    async fetchUser({ commit }, userId) {
      commit('SET_USER_LOADING', true)
      try {
        const response = await axios.get(`/api/users/${userId}`)
        commit('SET_USER_DATA', response.data)
        commit('SET_USER_ERROR', null)
      } catch (error) {
        commit('SET_USER_ERROR', error.message)
      } finally {
        commit('SET_USER_LOADING', false)
      }
    }
  }
})

export default useUserStore

main.js中注册模块:

import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
import useProductStore from './store/modules/product'
import useUserStore from './store/modules/user'

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

const productStore = useProductStore()
const userStore = useUserStore()

app.mount('#app')

在组件中使用模块的状态和action:

<template>
  <div>
    <h1>Product and User Information</h1>
    <div>
      <h2>Product</h2>
      <div v-if="productStore.productLoading">Loading product...</div>
      <div v-if="productStore.productError">{{ productStore.productError }}</div>
      <div v-if="productStore.productData">
        <p>{{ productStore.productData.name }}</p>
      </div>
      <button @click="productStore.fetchProduct(1)">Fetch Product</button>
    </div>
    <div>
      <h2>User</h2>
      <div v-if="userStore.userLoading">Loading user...</div>
      <div v-if="userStore.userError">{{ userStore.userError }}</div>
      <div v-if="userStore.userData">
        <p>{{ userStore.userData.username }}</p>
      </div>
      <button @click="userStore.fetchUser(1)">Fetch User</button>
    </div>
  </div>
</template>

<script setup>
import useProductStore from '../store/modules/product'
import useUserStore from '../store/modules/user'

const productStore = useProductStore()
const userStore = useUserStore()
</script>

通过这种方式,不同模块的网络请求状态不会相互干扰,每个模块都可以独立管理自己的请求状态。

5.2 全局加载状态

有时候,我们可能需要一个全局的加载状态来表示整个应用是否有网络请求在进行。可以在根store的state中定义一个全局的加载状态。

state() {
  return {
    globalLoading: false
  }
}

然后在每个模块的action中,在发起请求和结束请求时更新这个全局加载状态。

// store/modules/product.js
actions: {
  async fetchProduct({ commit }) {
    commit('SET_GLOBAL_LOADING', true)
    commit('SET_PRODUCT_LOADING', true)
    try {
      const response = await axios.get('/api/products')
      commit('SET_PRODUCT_DATA', response.data)
      commit('SET_PRODUCT_ERROR', null)
    } catch (error) {
      commit('SET_PRODUCT_ERROR', error.message)
    } finally {
      commit('SET_PRODUCT_LOADING', false)
      commit('SET_GLOBAL_LOADING', false)
    }
  }
}
// store/modules/user.js
actions: {
  async fetchUser({ commit }) {
    commit('SET_GLOBAL_LOADING', true)
    commit('SET_USER_LOADING', true)
    try {
      const response = await axios.get('/api/users')
      commit('SET_USER_DATA', response.data)
      commit('SET_USER_ERROR', null)
    } catch (error) {
      commit('SET_USER_ERROR', error.message)
    } finally {
      commit('SET_USER_LOADING', false)
      commit('SET_GLOBAL_LOADING', false)
    }
  }
}

在组件中,可以根据这个全局加载状态来显示一个全局的加载指示器,比如在App.vue中:

<template>
  <div>
    <div v-if="store.globalLoading">Global Loading...</div>
    <router-view></router-view>
  </div>
</template>

<script setup>
import { useStore } from 'vuex'

const store = useStore()
</script>

这样,当任何一个模块有网络请求在进行时,全局加载指示器就会显示。

6. 优化网络请求状态管理

6.1 防抖和节流

在一些场景下,用户可能会频繁触发网络请求,比如在搜索框中输入内容实时搜索。为了避免过多不必要的网络请求,可以使用防抖和节流技术。

防抖:在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。可以使用Lodash库中的debounce函数。

import { debounce } from 'lodash'

export default {
  methods: {
    @debounce(500)
    async search() {
      const { commit } = this.$store
      commit('SET_LOADING', true)
      try {
        const response = await axios.get(`/api/search?q=${this.searchQuery}`)
        commit('SET_DATA', response.data)
        commit('SET_ERROR', null)
      } catch (error) {
        commit('SET_ERROR', error.message)
      } finally {
        commit('SET_LOADING', false)
      }
    }
  }
}

在上述代码中,search方法被debounce装饰,只有在用户停止输入500毫秒后才会执行网络请求,避免了用户频繁输入时过多的请求。

节流:规定在一个单位时间内,只能触发一次回调。如果在这个单位时间内多次触发,只有一次生效。同样可以使用Lodash库中的throttle函数。

import { throttle } from 'lodash'

export default {
  methods: {
    @throttle(1000)
    async loadMore() {
      const { commit } = this.$store
      commit('SET_LOADING', true)
      try {
        const response = await axios.get(`/api/more?page=${this.page}`)
        commit('SET_DATA', response.data)
        commit('SET_ERROR', null)
      } catch (error) {
        commit('SET_ERROR', error.message)
      } finally {
        commit('SET_LOADING', false)
      }
    }
  }
}

在这个例子中,loadMore方法被throttle装饰,每1000毫秒只能触发一次网络请求,防止用户快速多次点击加载更多按钮导致过多请求。

6.2 缓存数据

对于一些不经常变化的数据,可以在Vuex中进行缓存。例如,一个商品分类列表,可能不会频繁更新。

actions: {
  async fetchCategories({ commit, state }) {
    if (state.categories) {
      return state.categories
    }
    commit('SET_LOADING', true)
    try {
      const response = await axios.get('/api/categories')
      commit('SET_CATEGORIES', response.data)
      commit('SET_ERROR', null)
      return response.data
    } catch (error) {
      commit('SET_ERROR', error.message)
    } finally {
      commit('SET_LOADING', false)
    }
  }
}

在上述代码中,当fetchCategories action被调用时,首先检查state.categories是否已经存在,如果存在则直接返回缓存的数据,不再发起网络请求。只有当缓存中没有数据时,才会发起网络请求并更新缓存。

6.3 错误处理优化

在处理网络请求错误时,可以进行更细致的处理。例如,根据不同的HTTP状态码进行不同的提示。

actions: {
  async fetchData({ commit }) {
    commit('SET_LOADING', true)
    try {
      const response = await axios.get('/api/data')
      commit('SET_DATA', response.data)
      commit('SET_ERROR', null)
    } catch (error) {
      if (error.response) {
        switch (error.response.status) {
          case 400:
            commit('SET_ERROR', 'Bad Request')
            break
          case 401:
            commit('SET_ERROR', 'Unauthorized')
            break
          case 404:
            commit('SET_ERROR', 'Not Found')
            break
          default:
            commit('SET_ERROR', 'An error occurred')
        }
      } else {
        commit('SET_ERROR', 'Network error')
      }
    } finally {
      commit('SET_LOADING', false)
    }
  }
}

通过这种方式,可以给用户提供更准确的错误信息,提升用户体验。

7. 与其他状态管理方案的对比

7.1 与Vue 3 Composition API相比

Vue 3 Composition API提供了一种灵活的方式来组织组件逻辑,它可以在组件内部管理状态和逻辑。而Vuex主要用于集中式管理应用的状态,适用于多个组件共享状态的场景。

  • 适用场景:如果状态只在单个组件内部使用,使用Composition API更合适,因为它可以将逻辑和状态紧密结合在组件内部,代码更简洁。例如,一个简单的计数器组件,使用Composition API可以直接在组件内定义状态和方法来管理计数。但如果多个组件需要共享某个状态,如用户登录状态,使用Vuex可以更好地进行集中管理,避免状态在不同组件间同步的问题。
  • 数据共享:Vuex通过单一状态树实现全局状态共享,所有组件都可以访问和修改store中的状态(通过mutation)。而Composition API虽然也可以通过provideinject实现跨组件数据传递,但这种方式相对复杂且不够直观,对于大规模状态共享不如Vuex方便。
  • 可维护性:在大型应用中,Vuex的模块化机制使得状态管理更具可维护性,不同模块的状态、mutation、action和getter可以分开管理。而Composition API在处理复杂的跨组件状态管理时,可能会导致代码分散在多个组件中,增加维护难度。

7.2 与Pinia相比

Pinia是Vuex的替代方案,同样用于Vue应用的状态管理,它在Vue 3上构建并提供了一些改进。

  • API设计:Pinia的API更加简洁和直观。例如,在定义store时,Pinia使用defineStore函数,代码结构更清晰。而Vuex在Vue 3中使用createStore函数,相对来说配置项更多。
  • 模块机制:Pinia的模块机制更灵活,每个模块可以独立使用,不需要像Vuex那样在根store中进行繁琐的注册。同时,Pinia的模块支持动态注册,这在一些场景下非常有用,比如在路由切换时动态加载不同的store模块。
  • 兼容性:Vuex对Vue 2和Vue 3都有较好的支持,而Pinia主要是为Vue 3设计的,虽然它的目标是提供更好的Vue 3状态管理体验,但对于还在使用Vue 2的项目,无法直接使用Pinia。

8. 总结Vuex管理网络请求状态的要点

通过以上的讲解,我们深入探讨了在Vue3中使用Vuex管理网络请求状态的各个方面。首先回顾了Vuex的基础概念,包括State、Getter、Mutation、Action和Module,这些是理解和使用Vuex的关键。接着介绍了Vue3与Vuex的集成步骤,从安装Vuex到创建store实例并在应用中使用。

使用Vuex管理网络请求状态具有集中化管理、可预测性和组件间共享状态等优势。在具体实现时,我们在state中定义网络请求的相关状态,通过mutation来更新这些状态,在action中发起网络请求并根据结果提交相应的mutation。对于多个网络请求状态,可以按模块管理或者设置全局加载状态。

为了优化网络请求状态管理,我们可以采用防抖和节流技术来避免频繁请求,缓存数据以减少不必要的请求,以及对错误处理进行优化以提供更准确的错误信息。与其他状态管理方案如Vue 3 Composition API和Pinia相比,Vuex在集中式状态管理和跨组件状态共享方面具有独特的优势和适用场景。

在实际项目中,根据应用的规模和需求,合理选择和使用Vuex来管理网络请求状态,可以提高代码的可维护性和应用的性能,为用户提供更好的体验。掌握Vuex管理网络请求状态的技术,对于前端开发人员来说是一项非常重要的技能,能够帮助我们更高效地构建复杂的Vue3应用。