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

Vue Vuex 最佳实践与代码规范建议

2024-07-173.5k 阅读

一、Vuex 基础回顾

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

在一个 Vuex 应用中,有几个核心概念:

  1. State:用于存储应用的状态。例如,在一个电商应用中,购物车的商品列表就可以作为一个 state。
// store.js
import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);

const state = {
  cartItems: []
};

export default new Vuex.Store({
  state
});
  1. Getter:从 state 派生出来的状态,类似于计算属性。比如,计算购物车中商品的总价格。
const getters = {
  totalPrice: state => {
    return state.cartItems.reduce((total, item) => {
      return total + item.price * item.quantity;
    }, 0);
  }
};

export default new Vuex.Store({
  state,
  getters
});
  1. Mutation:唯一能改变 state 的方法,且必须是同步操作。例如,向购物车中添加商品。
const mutations = {
  addToCart(state, item) {
    state.cartItems.push(item);
  }
};

export default new Vuex.Store({
  state,
  getters,
  mutations
});
  1. Action:用于处理异步操作,通过提交 mutation 来改变 state。比如,从服务器获取购物车数据。
const actions = {
  async fetchCartItems({ commit }) {
    try {
      const response = await axios.get('/api/cart-items');
      commit('setCartItems', response.data);
    } catch (error) {
      console.error('Error fetching cart items:', error);
    }
  }
};

export default new Vuex.Store({
  state,
  getters,
  mutations,
  actions
});
  1. Module:当应用变得复杂,将 store 分割成模块。每个模块拥有自己的 state、mutation、action、getter 等。
// modules/cart.js
const state = {
  cartItems: []
};

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

const mutations = {
  addToCart(state, item) {
    state.cartItems.push(item);
  }
};

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

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

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

Vue.use(Vuex);

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

二、Vuex 最佳实践

(一)State 相关实践

  1. 单一状态树:Vuex 使用单一状态树,将所有的状态集中到一个对象中管理。这使得整个应用的状态变得清晰可查。例如,在一个博客应用中,所有文章列表、用户信息、评论等状态都可以放在一个 state 对象里。
const state = {
  articles: [],
  user: null,
  comments: []
};
  1. 合理初始化状态:确保在应用启动时,state 有合理的初始值。对于可能会异步获取的数据,可以先设置为一个占位值,比如 null 或者空数组。
const state = {
  userProfile: null
};

const actions = {
  async fetchUserProfile({ commit }) {
    try {
      const response = await axios.get('/api/user-profile');
      commit('setUserProfile', response.data);
    } catch (error) {
      console.error('Error fetching user profile:', error);
    }
  }
};
  1. 避免直接修改 State:永远不要在组件中直接修改 state,而应该通过提交 mutation 来改变 state。这是 Vuex 的核心原则,能保证状态变化的可追踪性和可调试性。
<!-- 错误示例 -->
<template>
  <div>
    <button @click="addItemDirectly">Add Item</button>
  </div>
</template>

<script>
export default {
  methods: {
    addItemDirectly() {
      this.$store.state.cartItems.push({ name: 'New Item', price: 10 });
    }
  }
};
</script>

<!-- 正确示例 -->
<template>
  <div>
    <button @click="addItem">Add Item</button>
  </div>
</template>

<script>
export default {
  methods: {
    addItem() {
      this.$store.commit('addToCart', { name: 'New Item', price: 10 });
    }
  }
};
</script>

(二)Getter 相关实践

  1. 复用 Getter:如果在多个组件中需要用到相同的派生状态,将其定义为 getter 可以避免重复计算。例如,在电商应用中,多个组件可能都需要显示购物车商品的总数量,定义一个 cartItemCount getter 就可以复用。
const getters = {
  cartItemCount: state => {
    return state.cartItems.length;
  }
};
  1. 链式调用 Getter:Getter 可以依赖其他 getter,实现更复杂的派生状态。比如,先计算商品总价,再根据总价计算折扣后的价格。
const getters = {
  totalPrice: state => {
    return state.cartItems.reduce((total, item) => {
      return total + item.price * item.quantity;
    }, 0);
  },
  discountedPrice: (state, getters) => {
    const discount = 0.9;
    return getters.totalPrice * discount;
  }
};

(三)Mutation 相关实践

  1. 命名规范:Mutation 的命名应该清晰地描述其功能。例如,对于添加商品到购物车的 mutation,命名为 ADD_TO_CARTadd 更具描述性。按照惯例,mutation 命名使用大写字母加下划线的形式。
const mutations = {
  ADD_TO_CART(state, item) {
    state.cartItems.push(item);
  }
};
  1. 同步操作:严格遵循 mutation 必须是同步操作的原则。因为异步操作会使状态变化难以追踪和调试。如果需要异步操作,使用 action。
// 错误示例 - mutation 中使用异步操作
const mutations = {
  async FETCH_USER_PROFILE(state) {
    try {
      const response = await axios.get('/api/user-profile');
      state.userProfile = response.data;
    } catch (error) {
      console.error('Error fetching user profile:', error);
    }
  }
};

// 正确示例 - 使用 action 处理异步操作
const actions = {
  async fetchUserProfile({ commit }) {
    try {
      const response = await axios.get('/api/user-profile');
      commit('SET_USER_PROFILE', response.data);
    } catch (error) {
      console.error('Error fetching user profile:', error);
    }
  }
};

const mutations = {
  SET_USER_PROFILE(state, profile) {
    state.userProfile = profile;
  }
};

(四)Action 相关实践

  1. 处理异步操作:Action 是处理异步操作的地方,如 API 调用、定时器等。在处理异步操作时,合理使用 async/await 来简化代码。
const actions = {
  async fetchArticles({ commit }) {
    try {
      const response = await axios.get('/api/articles');
      commit('SET_ARTICLES', response.data);
    } catch (error) {
      console.error('Error fetching articles:', error);
    }
  }
};
  1. 分发 Action:Action 可以分发其他 action,实现更复杂的业务逻辑。例如,在保存文章前,先验证文章内容,然后再调用保存文章的 action。
const actions = {
  async validateAndSaveArticle({ dispatch }, article) {
    // 验证文章
    const isValid = await dispatch('validateArticle', article);
    if (isValid) {
      await dispatch('saveArticle', article);
    }
  },
  async validateArticle({ commit }, article) {
    // 验证逻辑
    return true;
  },
  async saveArticle({ commit }, article) {
    try {
      const response = await axios.post('/api/articles', article);
      commit('ADD_ARTICLE', response.data);
    } catch (error) {
      console.error('Error saving article:', error);
    }
  }
};

(五)Module 相关实践

  1. 模块拆分:当应用的功能越来越复杂,将 store 拆分成多个模块是必要的。按照功能模块进行拆分,比如电商应用可以拆分成用户模块、商品模块、订单模块等。
// modules/user.js
const state = {
  userInfo: null
};

const mutations = {
  SET_USER_INFO(state, info) {
    state.userInfo = info;
  }
};

const actions = {
  async fetchUserInfo({ commit }) {
    try {
      const response = await axios.get('/api/user');
      commit('SET_USER_INFO', response.data);
    } catch (error) {
      console.error('Error fetching user info:', error);
    }
  }
};

export default {
  state,
  mutations,
  actions
};

// modules/product.js
const state = {
  products: []
};

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

const actions = {
  async fetchProducts({ commit }) {
    try {
      const response = await axios.get('/api/products');
      commit('SET_PRODUCTS', response.data);
    } catch (error) {
      console.error('Error fetching products:', error);
    }
  }
};

export default {
  state,
  mutations,
  actions
};

// store.js
import Vue from 'vue';
import Vuex from 'vuex';
import user from './modules/user';
import product from './modules/product';

Vue.use(Vuex);

export default new Vuex.Store({
  modules: {
    user,
    product
  }
});
  1. 模块命名空间:当不同模块中有相同命名的 mutation、action 或 getter 时,可以使用命名空间来避免冲突。
// modules/user.js
const state = {
  userInfo: null
};

const mutations = {
  SET_USER_INFO(state, info) {
    state.userInfo = info;
  }
};

const actions = {
  async fetchUserInfo({ commit }) {
    try {
      const response = await axios.get('/api/user');
      commit('SET_USER_INFO', response.data);
    } catch (error) {
      console.error('Error fetching user info:', error);
    }
  }
};

export default {
  namespaced: true,
  state,
  mutations,
  actions
};

// components/UserComponent.vue
<template>
  <div>
    <button @click="fetchUser">Fetch User</button>
  </div>
</template>

<script>
export default {
  methods: {
    fetchUser() {
      this.$store.dispatch('user/fetchUserInfo');
    }
  }
};
</script>

三、Vuex 代码规范建议

(一)文件结构规范

  1. 单一文件存储:对于简单的应用,可以将所有 Vuex 相关代码放在一个 store.js 文件中。但随着应用规模的扩大,应进行模块拆分,每个模块单独放在一个文件中。
src/
├── store/
│   ├── index.js
│   ├── modules/
│   │   ├── user.js
│   │   ├── product.js
│   │   └── cart.js
│   └── getters.js
└── components/
    ├── UserComponent.vue
    ├── ProductComponent.vue
    └── CartComponent.vue
  1. Getter 文件分离:将所有的 getter 放在一个单独的 getters.js 文件中,方便管理和复用。
// getters.js
export const cartItemCount = state => {
  return state.cartItems.length;
};

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

// store.js
import Vue from 'vue';
import Vuex from 'vuex';
import { cartItemCount, totalPrice } from './getters';

Vue.use(Vuex);

const state = {
  cartItems: []
};

const mutations = {
  ADD_TO_CART(state, item) {
    state.cartItems.push(item);
  }
};

export default new Vuex.Store({
  state,
  mutations,
  getters: {
    cartItemCount,
    totalPrice
  }
});

(二)命名规范

  1. 通用命名规范:遵循 JavaScript 的命名规范,使用驼峰命名法(camelCase)。对于常量(如 mutation 类型),使用大写字母加下划线的形式。
  2. 模块命名:模块文件名应与模块名一致,且使用小写字母加下划线的形式。例如,用户模块文件名为 user.js
  3. Getter、Mutation、Action 命名:Getter 命名应描述其返回的状态,Mutation 命名应描述其对 state 的改变操作,Action 命名应描述其执行的业务逻辑。例如:
    • Getter:cartItemCount
    • Mutation:ADD_TO_CART
    • Action:fetchCartItems

(三)注释规范

  1. 文件注释:在每个 Vuex 相关文件的顶部,添加文件注释,说明文件的功能和用途。
// user.js - 用户模块,包含用户相关的 state、mutation、action 和 getter
const state = {
  userInfo: null
};

const mutations = {
  SET_USER_INFO(state, info) {
    state.userInfo = info;
  }
};

const actions = {
  async fetchUserInfo({ commit }) {
    try {
      const response = await axios.get('/api/user');
      commit('SET_USER_INFO', response.data);
    } catch (error) {
      console.error('Error fetching user info:', error);
    }
  }
};

export default {
  state,
  mutations,
  actions
};
  1. 函数注释:对于复杂的 mutation、action 和 getter 函数,添加注释说明其功能、参数和返回值。
// 获取购物车商品总数量
export const cartItemCount = state => {
  return state.cartItems.length;
};

// 添加商品到购物车
// @param state - Vuex state 对象
// @param item - 要添加的商品对象
const ADD_TO_CART = (state, item) => {
  state.cartItems.push(item);
};

(四)代码复用规范

  1. Getter 复用:尽量复用 getter,避免在不同组件中重复计算相同的派生状态。
  2. Action 复用:将一些通用的异步操作封装成 action,在不同的业务场景中复用。例如,在多个模块中都需要进行用户登录验证,可以将登录验证封装成一个 action。
// modules/auth.js
const actions = {
  async login({ commit }, credentials) {
    try {
      const response = await axios.post('/api/login', credentials);
      commit('SET_TOKEN', response.data.token);
      return true;
    } catch (error) {
      console.error('Error logging in:', error);
      return false;
    }
  }
};

export default {
  actions
};

// modules/user.js
const actions = {
  async fetchUserInfo({ dispatch, commit }) {
    const isLoggedIn = await dispatch('auth/login', { username: 'test', password: 'test' });
    if (isLoggedIn) {
      try {
        const response = await axios.get('/api/user');
        commit('SET_USER_INFO', response.data);
      } catch (error) {
        console.error('Error fetching user info:', error);
      }
    }
  }
};

export default {
  actions
};

(五)测试规范

  1. 测试框架选择:可以使用 Jest 或 Mocha 等测试框架来测试 Vuex 相关代码。
  2. State 测试:测试 state 的初始值是否正确,以及 mutation 对 state 的改变是否符合预期。
import { mount } from '@vue/test-utils';
import { createStore } from 'vuex';
import MyComponent from '@/components/MyComponent.vue';

describe('MyComponent', () => {
  let store;

  beforeEach(() => {
    store = createStore({
      state: {
        cartItems: []
      },
      mutations: {
        ADD_TO_CART(state, item) {
          state.cartItems.push(item);
        }
      }
    });
  });

  it('should add item to cart', () => {
    const wrapper = mount(MyComponent, {
      global: {
        plugins: [store]
      }
    });
    const item = { name: 'Test Item', price: 10 };
    wrapper.vm.$store.commit('ADD_TO_CART', item);
    expect(store.state.cartItems.length).toBe(1);
    expect(store.state.cartItems[0]).toEqual(item);
  });
});
  1. Getter 测试:测试 getter 的返回值是否正确。
import { createStore } from 'vuex';

describe('Cart Getters', () => {
  let store;

  beforeEach(() => {
    store = createStore({
      state: {
        cartItems: [
          { name: 'Item 1', price: 10, quantity: 2 },
          { name: 'Item 2', price: 20, quantity: 1 }
        ]
      },
      getters: {
        totalPrice: state => {
          return state.cartItems.reduce((total, item) => {
            return total + item.price * item.quantity;
          }, 0);
        }
      }
    });
  });

  it('should calculate total price correctly', () => {
    const totalPrice = store.getters.totalPrice;
    expect(totalPrice).toBe(10 * 2 + 20 * 1);
  });
});
  1. Action 测试:测试 action 的异步操作是否正确执行,以及 mutation 是否被正确提交。
import { createStore } from 'vuex';
import axios from 'axios';

jest.mock('axios');

describe('User Actions', () => {
  let store;

  beforeEach(() => {
    store = createStore({
      state: {
        userInfo: null
      },
      mutations: {
        SET_USER_INFO(state, info) {
          state.userInfo = info;
        }
      },
      actions: {
        async fetchUserInfo({ commit }) {
          try {
            const response = await axios.get('/api/user');
            commit('SET_USER_INFO', response.data);
          } catch (error) {
            console.error('Error fetching user info:', error);
          }
        }
      }
    });
  });

  it('should fetch user info and commit mutation', async () => {
    const mockUserInfo = { name: 'Test User' };
    axios.get.mockResolvedValue({ data: mockUserInfo });
    await store.dispatch('fetchUserInfo');
    expect(store.state.userInfo).toEqual(mockUserInfo);
  });
});

通过遵循以上 Vuex 的最佳实践和代码规范建议,可以使 Vue 应用的状态管理更加清晰、可维护和可测试,提高开发效率和应用质量。在实际项目中,应根据项目的规模和需求,灵活运用这些实践和规范,打造健壮的 Vue 应用。同时,持续关注 Vuex 的官方文档和社区动态,学习最新的技术和最佳实践,不断优化项目代码。在大型项目中,团队成员之间应统一代码规范,定期进行代码审查,确保整个项目的代码质量。此外,随着应用功能的不断增加,要及时对 Vuex 模块进行合理的拆分和优化,以保证状态管理的高效性。在处理复杂业务逻辑时,充分利用 action 的分发和组合功能,使业务流程更加清晰。总之,良好的 Vuex 实践和规范是构建优秀 Vue 应用的重要保障。