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

Vue网络请求 如何结合Vuex管理异步数据流

2021-12-312.3k 阅读

1. Vue 网络请求基础

在前端开发中,网络请求是获取数据的重要手段。Vue 本身并没有内置网络请求库,但通常我们会使用 axios 或者 fetch 来进行网络请求。

1.1 Axios 的使用

axios 是一个基于 Promise 的 HTTP 客户端,用于浏览器和 Node.js。它能很好地与 Vue 集成。

首先,通过 npm 安装 axios

npm install axios

在 Vue 项目中,我们可以在 main.js 中进行全局配置:

import Vue from 'vue';
import axios from 'axios';

Vue.prototype.$http = axios;

这样在任何组件中,都可以通过 this.$http 来发起请求。例如,发送一个 GET 请求获取用户列表:

export default {
  data() {
    return {
      userList: []
    };
  },
  mounted() {
    this.$http.get('/api/users')
    .then(response => {
        this.userList = response.data;
      })
    .catch(error => {
        console.error('Error fetching user list:', error);
      });
  }
};

上述代码中,在组件挂载后,通过 axios 发送 GET 请求到 /api/users 接口。如果请求成功,将响应数据赋值给 userList;若失败,则在控制台打印错误信息。

1.2 Fetch 的使用

fetch 是浏览器原生的网络请求 API,同样基于 Promise。它的基本用法如下:

export default {
  data() {
    return {
      productList: []
    };
  },
  mounted() {
    fetch('/api/products')
    .then(response => response.json())
    .then(data => {
        this.productList = data;
      })
    .catch(error => {
        console.error('Error fetching product list:', error);
      });
  }
};

这里使用 fetch 发送请求到 /api/products 接口,fetch 本身返回一个 Response 对象,我们通过调用 json() 方法将其解析为 JSON 数据。如果请求成功,将数据赋值给 productList,失败则打印错误。

2. Vuex 基础

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。

2.1 Vuex 的核心概念

  • State:Vuex 使用单一状态树,即每个应用将仅仅包含一个 store 实例。所有的状态都存储在 state 中。例如,在一个电商应用中,购物车的商品列表可以存储在 state 中:
const state = {
  cartItems: []
};
  • Getter:类似于计算属性,用于从 state 中派生出一些状态。比如,计算购物车中商品的总价格:
const getters = {
  totalPrice: state => {
    return state.cartItems.reduce((total, item) => {
      return total + item.price * item.quantity;
    }, 0);
  }
};
  • Mutation:更改 Vuex 的 store 中的状态的唯一方法是提交 mutation。Mutation 必须是同步函数。例如,向购物车中添加商品:
const mutations = {
  ADD_TO_CART(state, product) {
    state.cartItems.push(product);
  }
};
  • Action:Action 类似于 mutation,不同在于:Action 提交的是 mutation,而不是直接变更状态;Action 可以包含任意异步操作。比如,从服务器获取购物车数据:
const actions = {
  async fetchCartItems({ commit }) {
    try {
      const response = await this.$http.get('/api/cart');
      commit('SET_CART_ITEMS', response.data);
    } catch (error) {
      console.error('Error fetching cart items:', error);
    }
  }
};

2.2 搭建 Vuex 模块

在 Vue 项目中,通常在 src/store 目录下创建 Vuex 相关文件。例如,index.js 文件用于整合各个模块:

import Vue from 'vue';
import Vuex from 'vuex';
import cart from './modules/cart';

Vue.use(Vuex);

export default new Vuex.Store({
  modules: {
    cart
  }
});

然后在 src/store/modules/cart.js 文件中定义购物车相关的状态、mutation、action 和 getter:

const state = {
  cartItems: []
};

const getters = {
  totalPrice: state => {
    return state.cartItems.reduce((total, item) => {
      return total + item.price * item.quantity;
    }, 0);
  }
};

const mutations = {
  ADD_TO_CART(state, product) {
    state.cartItems.push(product);
  },
  SET_CART_ITEMS(state, items) {
    state.cartItems = items;
  }
};

const actions = {
  async fetchCartItems({ commit }) {
    try {
      const response = await this.$http.get('/api/cart');
      commit('SET_CART_ITEMS', response.data);
    } catch (error) {
      console.error('Error fetching cart items:', error);
    }
  }
};

export default {
  state,
  getters,
  mutations,
  actions
};

3. 结合 Vue 网络请求与 Vuex 管理异步数据流

在实际开发中,我们经常需要通过网络请求获取数据,并将这些数据存储到 Vuex 的状态中进行管理。

3.1 简单的数据获取与存储

假设我们有一个博客应用,需要获取文章列表并存储到 Vuex 中。

首先,在 src/store/modules/blog.js 中定义相关的 Vuex 模块:

const state = {
  articles: []
};

const getters = {
  getArticles: state => state.articles
};

const mutations = {
  SET_ARTICLES(state, articles) {
    state.articles = articles;
  }
};

const actions = {
  async fetchArticles({ commit }) {
    try {
      const response = await this.$http.get('/api/articles');
      commit('SET_ARTICLES', response.data);
    } catch (error) {
      console.error('Error fetching articles:', error);
    }
  }
};

export default {
  state,
  getters,
  mutations,
  actions
};

然后在 index.js 中注册该模块:

import Vue from 'vue';
import Vuex from 'vuex';
import blog from './modules/blog';

Vue.use(Vuex);

export default new Vuex.Store({
  modules: {
    blog
  }
});

在组件中,我们可以这样调用:

import { mapActions } from 'vuex';

export default {
  data() {
    return {
      // 可以定义其他组件内的数据
    };
  },
  methods: {
  ...mapActions(['fetchArticles'])
  },
  mounted() {
    this.fetchArticles();
  }
};

上述代码中,在组件挂载时,调用 fetchArticles action,该 action 通过网络请求获取文章列表,并将数据提交到 SET_ARTICLES mutation 从而更新 Vuex 中的 articles 状态。

3.2 处理复杂的异步操作

有时候,网络请求可能涉及多个步骤或者依赖关系。例如,在一个电商应用中,我们需要先获取用户信息,根据用户信息获取用户的订单列表,并且在获取订单列表后,还要获取每个订单中的商品详情。

src/store/modules/order.js 中定义相关模块:

const state = {
  user: null,
  orders: [],
  orderDetails: []
};

const getters = {
  getUser: state => state.user,
  getOrders: state => state.orders,
  getOrderDetails: state => state.orderDetails
};

const mutations = {
  SET_USER(state, user) {
    state.user = user;
  },
  SET_ORDERS(state, orders) {
    state.orders = orders;
  },
  SET_ORDER_DETAILS(state, details) {
    state.orderDetails = details;
  }
};

const actions = {
  async fetchUserOrders({ commit }) {
    try {
      // 获取用户信息
      const userResponse = await this.$http.get('/api/user');
      commit('SET_USER', userResponse.data);

      // 根据用户 ID 获取订单列表
      const orderResponse = await this.$http.get(`/api/orders/${userResponse.data.id}`);
      commit('SET_ORDERS', orderResponse.data);

      // 获取每个订单的详细信息
      let allDetails = [];
      for (let order of orderResponse.data) {
        const detailResponse = await this.$http.get(`/api/order/${order.id}/details`);
        allDetails = allDetails.concat(detailResponse.data);
      }
      commit('SET_ORDER_DETAILS', allDetails);
    } catch (error) {
      console.error('Error fetching user orders:', error);
    }
  }
};

export default {
  state,
  getters,
  mutations,
  actions
};

在组件中调用:

import { mapActions } from 'vuex';

export default {
  data() {
    return {
      // 组件内数据
    };
  },
  methods: {
  ...mapActions(['fetchUserOrders'])
  },
  mounted() {
    this.fetchUserOrders();
  }
};

这里通过 fetchUserOrders action 完成了一系列复杂的异步操作,包括获取用户信息、订单列表以及订单详情,并将相应数据存储到 Vuex 的状态中。

3.3 处理网络请求中的错误

在网络请求过程中,错误处理是非常重要的。在前面的代码中,我们已经简单地在 catch 块中打印了错误信息。但在实际应用中,我们可能需要更完善的错误处理机制。

例如,我们可以在 Vuex 的 action 中定义不同类型的错误状态,并在 mutation 中更新这些状态。在 src/store/modules/product.js 中:

const state = {
  products: [],
  error: null
};

const getters = {
  getProducts: state => state.products,
  getError: state => state.error
};

const mutations = {
  SET_PRODUCTS(state, products) {
    state.products = products;
    state.error = null;
  },
  SET_ERROR(state, error) {
    state.error = error;
  }
};

const actions = {
  async fetchProducts({ commit }) {
    try {
      const response = await this.$http.get('/api/products');
      commit('SET_PRODUCTS', response.data);
    } catch (error) {
      if (error.response) {
        // 服务器返回了状态码,但不是 2xx
        commit('SET_ERROR', `HTTP error! status: ${error.response.status}`);
      } else if (error.request) {
        // 没有收到响应
        commit('SET_ERROR', 'No response received');
      } else {
        // 其他错误
        commit('SET_ERROR', 'Error occurred while fetching products');
      }
    }
  }
};

export default {
  state,
  getters,
  mutations,
  actions
};

在组件中,我们可以根据 getError getter 来显示相应的错误信息:

import { mapGetters, mapActions } from 'vuex';

export default {
  data() {
    return {
      // 组件内数据
    };
  },
  computed: {
  ...mapGetters(['getError'])
  },
  methods: {
  ...mapActions(['fetchProducts'])
  },
  mounted() {
    this.fetchProducts();
  }
};
<template>
  <div>
    <div v-if="getError">{{ getError }}</div>
    <ul v-else>
      <li v-for="product in getProducts" :key="product.id">{{ product.name }}</li>
    </ul>
  </div>
</template>

这样,当网络请求出现错误时,我们可以在组件中友好地显示错误信息给用户。

3.4 优化网络请求与 Vuex 集成

  • 缓存数据:在一些情况下,我们不需要每次都从服务器获取数据。可以在 Vuex 的 state 中缓存数据,并在下次请求时先检查缓存。例如,在获取文章列表时:
const state = {
  articles: [],
  articleCacheTimestamp: null
};

const getters = {
  getArticles: state => state.articles
};

const mutations = {
  SET_ARTICLES(state, articles) {
    state.articles = articles;
    state.articleCacheTimestamp = new Date().getTime();
  }
};

const actions = {
  async fetchArticles({ commit, state }) {
    const CACHE_DURATION = 60 * 1000; // 缓存 1 分钟
    if (state.articles.length && new Date().getTime() - state.articleCacheTimestamp < CACHE_DURATION) {
      return;
    }
    try {
      const response = await this.$http.get('/api/articles');
      commit('SET_ARTICLES', response.data);
    } catch (error) {
      console.error('Error fetching articles:', error);
    }
  }
};
  • 批量请求:如果需要获取多个相关的数据,可以将多个请求合并为一个,减少网络请求次数。例如,在电商应用中获取商品列表和商品分类信息:
const actions = {
  async fetchProductData({ commit }) {
    try {
      const [productsResponse, categoriesResponse] = await Promise.all([
        this.$http.get('/api/products'),
        this.$http.get('/api/categories')
      ]);
      commit('SET_PRODUCTS', productsResponse.data);
      commit('SET_CATEGORIES', categoriesResponse.data);
    } catch (error) {
      console.error('Error fetching product data:', error);
    }
  }
};

4. 结合 Vue Router 处理异步数据加载

在单页应用中,Vue Router 用于管理页面路由。当用户导航到不同的路由时,我们可能需要根据路由参数加载相应的异步数据。

4.1 在路由守卫中加载数据

假设我们有一个博客应用,每个文章有一个详情页面,路由为 /article/:id。我们可以在路由守卫中加载文章详情数据。

router/index.js 中:

import Vue from 'vue';
import Router from 'vue-router';
import ArticleDetail from '@/components/ArticleDetail';
import store from '../store';

Vue.use(Router);

const router = new Router({
  routes: [
    {
      path: '/article/:id',
      name: 'ArticleDetail',
      component: ArticleDetail,
      beforeEnter: async (to, from, next) => {
        try {
          const response = await store.dispatch('fetchArticleDetail', to.params.id);
          next();
        } catch (error) {
          console.error('Error fetching article detail:', error);
          next('/error'); // 跳转到错误页面
        }
      }
    }
  ]
});

export default router;

src/store/modules/blog.js 中:

const state = {
  articleDetail: null
};

const getters = {
  getArticleDetail: state => state.articleDetail
};

const mutations = {
  SET_ARTICLE_DETAIL(state, article) {
    state.articleDetail = article;
  }
};

const actions = {
  async fetchArticleDetail({ commit }, id) {
    const response = await this.$http.get(`/api/articles/${id}`);
    commit('SET_ARTICLE_DETAIL', response.data);
    return response.data;
  }
};

export default {
  state,
  getters,
  mutations,
  actions
};

ArticleDetail.vue 组件中:

import { mapGetters } from 'vuex';

export default {
  data() {
    return {
      // 组件内数据
    };
  },
  computed: {
  ...mapGetters(['getArticleDetail'])
  }
};
<template>
  <div>
    <h1>{{ getArticleDetail.title }}</h1>
    <p>{{ getArticleDetail.content }}</p>
  </div>
</template>

这样,当用户导航到文章详情页面时,会先在路由守卫中通过 Vuex 的 action 加载文章详情数据,加载成功后再进入页面。

4.2 处理嵌套路由中的异步数据加载

如果存在嵌套路由,例如文章详情页面下还有评论子路由 /article/:id/comment。我们同样可以在子路由的路由守卫中加载评论数据。

router/index.js 中:

const router = new Router({
  routes: [
    {
      path: '/article/:id',
      name: 'ArticleDetail',
      component: ArticleDetail,
      children: [
        {
          path: 'comment',
          name: 'ArticleComment',
          component: ArticleComment,
          beforeEnter: async (to, from, next) => {
            try {
              const response = await store.dispatch('fetchArticleComments', to.params.id);
              next();
            } catch (error) {
              console.error('Error fetching article comments:', error);
              next('/error');
            }
          }
        }
      ]
    }
  ]
});

src/store/modules/blog.js 中:

const state = {
  articleComments: []
};

const getters = {
  getArticleComments: state => state.articleComments
};

const mutations = {
  SET_ARTICLE_COMMENTS(state, comments) {
    state.articleComments = comments;
  }
};

const actions = {
  async fetchArticleComments({ commit }, id) {
    const response = await this.$http.get(`/api/articles/${id}/comments`);
    commit('SET_ARTICLE_COMMENTS', response.data);
    return response.data;
  }
};

ArticleComment.vue 组件中:

import { mapGetters } from 'vuex';

export default {
  data() {
    return {
      // 组件内数据
    };
  },
  computed: {
  ...mapGetters(['getArticleComments'])
  }
};
<template>
  <div>
    <ul>
      <li v-for="comment in getArticleComments" :key="comment.id">{{ comment.content }}</li>
    </ul>
  </div>
</template>

通过这种方式,在进入文章评论子页面时,会加载相应的评论数据。

5. 单元测试与集成测试

当结合 Vue 网络请求和 Vuex 管理异步数据流时,进行单元测试和集成测试可以保证代码的质量和稳定性。

5.1 单元测试 Vuex actions

对于 Vuex 的 action,我们可以使用 vue -x -unit -jest 进行测试。例如,测试 fetchArticles action:

import { shallow } from '@vue/test-utils';
import Vuex from 'vuex';
import * as actions from '@/store/modules/blog/actions';
import * as types from '@/store/modules/blog/mutation-types';
import axios from 'axios';

jest.mock('axios');

describe('fetchArticles action', () => {
  let state;
  let getters;
  let commit;
  let dispatch;

  beforeEach(() => {
    state = {};
    getters = {};
    commit = jest.fn();
    dispatch = jest.fn();
  });

  it('should commit SET_ARTICLES on success', async () => {
    const mockResponse = { data: [] };
    axios.get.mockResolvedValue(mockResponse);

    await actions.fetchArticles({ commit });

    expect(commit).toHaveBeenCalledWith(types.SET_ARTICLES, mockResponse.data);
  });

  it('should handle error', async () => {
    const mockError = new Error('Network error');
    axios.get.mockRejectedValue(mockError);

    await actions.fetchArticles({ commit });

    // 这里可以根据实际的错误处理逻辑进行断言,例如检查是否设置了错误状态
    expect(console.error).toHaveBeenCalledWith('Error fetching articles:', mockError);
  });
});

上述代码中,使用 jest.mock('axios') 模拟 axios 的行为,然后分别测试了请求成功和失败的情况。

5.2 集成测试组件与 Vuex 和网络请求

对于组件与 Vuex 和网络请求的集成测试,可以使用 vue - test - utilsjest - fetch - mock。例如,测试 ArticleList.vue 组件:

import { mount } from '@vue/test-utils';
import ArticleList from '@/components/ArticleList';
import Vuex from 'vuex';
import fetchMock from 'jest-fetch-mock';

fetchMock.enableMocks();

describe('ArticleList component', () => {
  let store;

  beforeEach(() => {
    const state = {
      articles: []
    };

    const getters = {
      getArticles: state => state.articles
    };

    const actions = {
      fetchArticles: jest.fn()
    };

    store = new Vuex.Store({
      state,
      getters,
      actions
    });
  });

  it('should call fetchArticles action on mount', async () => {
    const wrapper = mount(ArticleList, { store });

    expect(store.dispatch).toHaveBeenCalledWith('fetchArticles');
  });

  it('should display articles', async () => {
    const mockArticles = [
      { id: 1, title: 'Article 1' },
      { id: 2, title: 'Article 2' }
    ];
    fetchMock.mockResponseOnce(JSON.stringify(mockArticles));

    const wrapper = mount(ArticleList, { store });

    await wrapper.vm.$nextTick();

    const articleElements = wrapper.findAll('li');
    expect(articleElements.length).toBe(mockArticles.length);
  });
});

这里使用 fetchMock 模拟网络请求,测试了组件在挂载时是否调用了 fetchArticles action,以及是否正确显示了从网络请求获取的文章数据。

通过以上详细的内容,我们全面地探讨了如何在 Vue 开发中结合网络请求与 Vuex 管理异步数据流,涵盖了基础概念、实际应用场景、错误处理、优化以及测试等方面,希望能帮助开发者更好地构建健壮的 Vue 应用程序。