Vue Vuex 最佳实践与代码规范建议
2024-07-173.5k 阅读
一、Vuex 基础回顾
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。
在一个 Vuex 应用中,有几个核心概念:
- State:用于存储应用的状态。例如,在一个电商应用中,购物车的商品列表就可以作为一个 state。
// store.js
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const state = {
cartItems: []
};
export default new Vuex.Store({
state
});
- 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
});
- Mutation:唯一能改变 state 的方法,且必须是同步操作。例如,向购物车中添加商品。
const mutations = {
addToCart(state, item) {
state.cartItems.push(item);
}
};
export default new Vuex.Store({
state,
getters,
mutations
});
- 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
});
- 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 相关实践
- 单一状态树:Vuex 使用单一状态树,将所有的状态集中到一个对象中管理。这使得整个应用的状态变得清晰可查。例如,在一个博客应用中,所有文章列表、用户信息、评论等状态都可以放在一个 state 对象里。
const state = {
articles: [],
user: null,
comments: []
};
- 合理初始化状态:确保在应用启动时,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);
}
}
};
- 避免直接修改 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 相关实践
- 复用 Getter:如果在多个组件中需要用到相同的派生状态,将其定义为 getter 可以避免重复计算。例如,在电商应用中,多个组件可能都需要显示购物车商品的总数量,定义一个
cartItemCount
getter 就可以复用。
const getters = {
cartItemCount: state => {
return state.cartItems.length;
}
};
- 链式调用 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 相关实践
- 命名规范:Mutation 的命名应该清晰地描述其功能。例如,对于添加商品到购物车的 mutation,命名为
ADD_TO_CART
比add
更具描述性。按照惯例,mutation 命名使用大写字母加下划线的形式。
const mutations = {
ADD_TO_CART(state, item) {
state.cartItems.push(item);
}
};
- 同步操作:严格遵循 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 相关实践
- 处理异步操作: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);
}
}
};
- 分发 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 相关实践
- 模块拆分:当应用的功能越来越复杂,将 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
}
});
- 模块命名空间:当不同模块中有相同命名的 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 代码规范建议
(一)文件结构规范
- 单一文件存储:对于简单的应用,可以将所有 Vuex 相关代码放在一个
store.js
文件中。但随着应用规模的扩大,应进行模块拆分,每个模块单独放在一个文件中。
src/
├── store/
│ ├── index.js
│ ├── modules/
│ │ ├── user.js
│ │ ├── product.js
│ │ └── cart.js
│ └── getters.js
└── components/
├── UserComponent.vue
├── ProductComponent.vue
└── CartComponent.vue
- 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
}
});
(二)命名规范
- 通用命名规范:遵循 JavaScript 的命名规范,使用驼峰命名法(camelCase)。对于常量(如 mutation 类型),使用大写字母加下划线的形式。
- 模块命名:模块文件名应与模块名一致,且使用小写字母加下划线的形式。例如,用户模块文件名为
user.js
。 - Getter、Mutation、Action 命名:Getter 命名应描述其返回的状态,Mutation 命名应描述其对 state 的改变操作,Action 命名应描述其执行的业务逻辑。例如:
- Getter:
cartItemCount
- Mutation:
ADD_TO_CART
- Action:
fetchCartItems
- Getter:
(三)注释规范
- 文件注释:在每个 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
};
- 函数注释:对于复杂的 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);
};
(四)代码复用规范
- Getter 复用:尽量复用 getter,避免在不同组件中重复计算相同的派生状态。
- 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
};
(五)测试规范
- 测试框架选择:可以使用 Jest 或 Mocha 等测试框架来测试 Vuex 相关代码。
- 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);
});
});
- 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);
});
});
- 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 应用的重要保障。