引言
在现代前端开发中,随着应用规模的增长,状态管理变得越来越复杂。将关键业务逻辑从视图层拆分到专门的状态管理库中,已成为提高代码可维护性和可测试性的最佳实践。本文将详细探讨如何科学地将逻辑拆分到 store 中,如何处理多个 store 之间的数据同步,以及如何减少数据冗余。
一、关键逻辑拆分到 Store 的原则
1.1 职责单一原则
每个 store 应该只负责应用中的一个领域或功能模块:
javascript
// userStore.js - 仅负责用户相关状态
const userStore = {
state: {
profile: null,
preferences: {},
notifications: []
},
actions: {
async fetchUserProfile() { /* ... */ },
updatePreferences() { /* ... */ }
}
};
// productStore.js - 仅负责产品相关状态
const productStore = {
state: {
products: [],
categories: [],
currentProduct: null
},
actions: {
async fetchProducts() { /* ... */ },
setCurrentProduct() { /* ... */ }
}
};
1.2 UI 与业务逻辑分离
从组件中抽离业务逻辑,只保留与 UI 渲染相关的代码:
javascript
// 组件中只关注渲染,业务逻辑委托给 store
function ProductList() {
const { products, isLoading, fetchProducts } = useProductStore();
useEffect(() => {
fetchProducts();
}, []);
if (isLoading) return <LoadingSpinner />;
return (
<div>
{products.map(product => (
<ProductItem key={product.id} product={product} />
))}
</div>
);
}
1.3 按领域划分 Store
根据业务领域或功能模块划分 store,而不是技术层面:
bash
stores/
├── user/ # 用户领域
│ ├── index.js
│ ├── actions.js
│ └── selectors.js
├── product/ # 产品领域
│ ├── index.js
│ ├── actions.js
│ └── selectors.js
└── order/ # 订单领域
├── index.js
├── actions.js
└── selectors.js
1.4 派生状态与计算属性
将计算逻辑放在 store 中,而不是组件内:
javascript
// productStore.js
const productStore = {
state: {
products: [],
filter: { category: null, priceRange: [0, 1000] }
},
// 派生状态/计算属性
getters: {
filteredProducts: (state) => {
return state.products.filter(product => {
const matchesCategory = !state.filter.category ||
product.category === state.filter.category;
const matchesPrice = product.price >= state.filter.priceRange[0] &&
product.price <= state.filter.priceRange[1];
return matchesCategory && matchesPrice;
});
},
productCount: (state) => state.products.length
}
};
二、处理多 Store 之间的数据同步
2.1 中央事件总线
使用中央事件总线来协调不同 store 之间的通信:
javascript
// eventBus.js
class EventBus {
constructor() {
this.events = {};
}
subscribe(event, callback) {
if (!this.events[event]) this.events[event] = [];
this.events[event].push(callback);
return () => this.unsubscribe(event, callback);
}
unsubscribe(event, callback) {
if (this.events[event]) {
this.events[event] = this.events[event].filter(cb => cb !== callback);
}
}
publish(event, data) {
if (this.events[event]) {
this.events[event].forEach(callback => callback(data));
}
}
}
export const eventBus = new EventBus();
在不同 store 中使用事件总线:
javascript
// userStore.js
import { eventBus } from './eventBus';
const userStore = {
// ...
actions: {
login(credentials) {
// 登录逻辑...
// 登录成功后通知其他 store
eventBus.publish('USER_LOGGED_IN', this.state.profile);
}
}
};
// cartStore.js
import { eventBus } from './eventBus';
const cartStore = {
// ...
setupListeners() {
// 监听用户登录事件,获取用户购物车
eventBus.subscribe('USER_LOGGED_IN', (profile) => {
this.actions.fetchUserCart(profile.id);
});
}
};
cartStore.setupListeners();
2.2 使用 Effects 处理副作用
在 store 中添加副作用处理器,响应状态变化:
javascript
// 使用 Redux Toolkit 的例子
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// 从一个 store 的操作触发另一个 store 的操作
export const checkoutCart = createAsyncThunk(
'cart/checkout',
async (_, { getState, dispatch }) => {
const { cart, user } = getState();
// 处理结账
const order = await orderApi.createOrder({
items: cart.items,
userId: user.profile.id
});
// 成功后,清空购物车并更新订单列表
dispatch(cartActions.clearCart());
dispatch(orderActions.addOrder(order));
return order;
}
);
2.3 状态容器嵌套
通过状态容器的嵌套和组合来处理复杂的数据依赖:
javascript
// rootStore.js
import { createUserStore } from './userStore';
import { createProductStore } from './productStore';
import { createCartStore } from './cartStore';
export function createRootStore() {
// 创建各个 store 实例
const userStore = createUserStore();
const productStore = createProductStore();
// 将其他 store 注入到需要依赖的 store 中
const cartStore = createCartStore({
userStore,
productStore
});
// 返回根 store,包含所有子 store
return {
user: userStore,
product: productStore,
cart: cartStore
};
}
在需要访问其他 store 数据的 store 中:
javascript
// cartStore.js
export function createCartStore({ userStore, productStore }) {
return {
state: {
items: [],
// ...
},
actions: {
addToCart(productId, quantity) {
const product = productStore.selectors.getProductById(productId);
const userId = userStore.state.profile?.id;
// 添加商品到购物车...
if (userId) {
// 如果用户已登录,同步到服务器
this.syncCartToServer(userId);
}
}
}
};
}
2.4 响应式系统
利用响应式框架的能力,自动处理状态依赖关系:
javascript
// 使用 MobX 的例子
import { makeAutoObservable, reaction } from 'mobx';
class CartStore {
items = [];
constructor(rootStore) {
this.rootStore = rootStore;
makeAutoObservable(this);
// 设置响应式关系,当用户变化时,更新购物车
reaction(
() => this.rootStore.userStore.currentUser,
(currentUser) => {
if (currentUser) {
this.loadUserCart(currentUser.id);
} else {
this.clearCart();
}
}
);
}
// 方法...
}
三、减少数据冗余的策略
3.1 规范化状态结构
使用扁平化和规范化的数据结构,避免嵌套和重复:
javascript
// 不规范的数据结构 - 存在冗余
const badState = {
users: [
{
id: 1,
name: "张三",
orders: [
{ id: 101, title: "订单1", amount: 100 },
{ id: 102, title: "订单2", amount: 200 }
]
}
],
currentOrder: { id: 101, title: "订单1", amount: 100, userId: 1 }
};
// 规范化的数据结构 - 减少冗余
const goodState = {
users: {
byId: {
1: { id: 1, name: "张三", orderIds: [101, 102] }
},
allIds: [1]
},
orders: {
byId: {
101: { id: 101, title: "订单1", amount: 100, userId: 1 },
102: { id: 102, title: "订单2", amount: 200, userId: 1 }
},
allIds: [101, 102]
},
currentOrderId: 101
};
3.2 引用存储
使用 ID 引用而不是对象复制:
javascript
// userStore.js
const userStore = {
state: {
users: {
1: { id: 1, name: '张三' },
2: { id: 2, name: '李四' }
}
}
};
// orderStore.js - 存储用户ID而非用户对象
const orderStore = {
state: {
orders: {
101: { id: 101, amount: 100, userId: 1 }, // 只存储用户ID
102: { id: 102, amount: 200, userId: 2 }
}
},
// 使用选择器获取完整数据
selectors: {
getOrderWithUser(orderId) {
const order = this.state.orders[orderId];
const user = userStore.state.users[order.userId];
return { ...order, user };
}
}
};
3.3 共享选择器
创建跨 store 选择器来组合数据,而不是复制:
javascript
// selectors.js
export const getOrdersWithDetails = (state) => {
const { orders, products, users } = state;
return orders.allIds.map(id => {
const order = orders.byId[id];
return {
...order,
user: users.byId[order.userId],
items: order.itemIds.map(itemId => ({
...order.items[itemId],
product: products.byId[order.items[itemId].productId]
}))
};
});
};
3.4 状态记忆化
使用记忆化技术缓存派生状态,避免重复计算:
javascript
// 使用 reselect 库进行记忆化
import { createSelector } from 'reselect';
const getUsers = state => state.users.byId;
const getOrders = state => state.orders.byId;
const getUserOrders = state => state.userOrders;
// 记忆化选择器
export const getOrdersByUser = createSelector(
[getUsers, getOrders, getUserOrders],
(users, orders, userOrders) => {
const result = {};
Object.keys(users).forEach(userId => {
const orderIds = userOrders[userId] || [];
result[userId] = orderIds.map(id => orders[id]);
});
return result;
}
);
3.5 状态分片和懒加载
根据需要加载状态片段,而不是一次性加载所有数据:
javascript
// 按需加载状态的 store 设计
const productStore = {
state: {
categories: {},
productsByCategory: {}
},
actions: {
async loadCategory(categoryId) {
// 只有第一次加载分类数据时才请求
if (!this.state.productsByCategory[categoryId]) {
const products = await api.getProductsByCategory(categoryId);
// 更新状态,添加新分类的产品
this.setState({
productsByCategory: {
...this.state.productsByCategory,
[categoryId]: products.reduce((acc, product) => {
acc[product.id] = product;
return acc;
}, {})
}
});
}
}
}
};
四、实践案例:构建完整的多 Store 协作系统
下面以一个电商应用为例,展示如何构建完整的状态管理系统:
4.1 构建 Store 结构
bash
stores/
├── root.js # 根 store,组合其他 store
├── user/
│ ├── index.js # 用户 store 定义
│ ├── actions.js # 用户操作
│ └── selectors.js # 用户数据选择器
├── product/
│ ├── index.js
│ ├── actions.js
│ └── selectors.js
├── cart/
│ ├── index.js
│ ├── actions.js
│ └── selectors.js
├── order/
│ ├── index.js
│ ├── actions.js
│ └── selectors.js
└── shared/
├── eventBus.js # 事件总线
└── entityHelpers.js # 实体操作辅助函数
4.2 根 Store 集成
javascript
// root.js
import { createUserStore } from './user';
import { createProductStore } from './product';
import { createCartStore } from './cart';
import { createOrderStore } from './order';
export function createStore() {
// 创建独立 store
const userStore = createUserStore();
const productStore = createProductStore();
// 创建依赖其他 store 的 store
const cartStore = createCartStore({ userStore, productStore });
const orderStore = createOrderStore({ userStore, cartStore, productStore });
// 建立 store 间通信
setupStoreListeners({ userStore, productStore, cartStore, orderStore });
return {
user: userStore,
product: productStore,
cart: cartStore,
order: orderStore
};
}
function setupStoreListeners(stores) {
const { userStore, cartStore, orderStore } = stores;
// 用户登录后,加载用户的购物车
userStore.onLogin((user) => {
cartStore.actions.loadUserCart(user.id);
orderStore.actions.loadUserOrders(user.id);
});
// 订单完成后,清空购物车
orderStore.onOrderCreated(() => {
cartStore.actions.clearCart();
});
}
4.3 Store 实现示例
javascript
// user/index.js
import { createSlice } from './helpers';
import * as actions from './actions';
import * as selectors from './selectors';
import { eventBus } from '../shared/eventBus';
export function createUserStore() {
const slice = createSlice({
name: 'user',
initialState: {
profile: null,
isAuthenticated: false,
preferences: {},
loading: false,
error: null
},
reducers: {
// 状态变更处理器...
}
});
// 扩展 store 添加方法
return {
...slice,
actions: {
...slice.actions,
...actions
},
selectors,
// 事件监听器
onLogin(callback) {
return eventBus.subscribe('USER_LOGGED_IN', callback);
}
};
}
4.4 跨 Store 操作
javascript
// cart/actions.js
export async function checkout(payload, { getState, dispatch, getStores }) {
const { cart } = getState();
const { user, order } = getStores();
if (!user.selectors.isAuthenticated()) {
throw new Error('请先登录');
}
try {
dispatch(setLoading(true));
// 创建订单
const newOrder = await api.createOrder({
userId: user.selectors.getCurrentUserId(),
items: cart.items,
shippingAddress: payload.shippingAddress,
paymentMethod: payload.paymentMethod
});
// 更新订单 store
order.actions.addOrder(newOrder);
// 清空购物车
dispatch(clearCart());
// 发布事件通知其他 store
eventBus.publish('ORDER_CREATED', newOrder);
return newOrder;
} catch (error) {
dispatch(setError(error.message));
throw error;
} finally {
dispatch(setLoading(false));
}
}
4.5 共享选择器示例
javascript
// shared/selectors.js
import { createSelector } from 'reselect';
// 跨 store 选择器
export const getCartWithDetails = ({ cart, product, user }) =>
createSelector(
() => cart.items,
() => product.entities,
() => user.profile,
(items, products, profile) => ({
items: items.map(item => ({
...item,
product: products[item.productId],
totalPrice: item.quantity * products[item.productId].price
})),
totalItems: items.reduce((sum, item) => sum + item.quantity, 0),
totalAmount: items.reduce((sum, item) =>
sum + (item.quantity * products[item.productId].price), 0),
user: profile
})
);
五、最佳实践与原则总结
5.1 状态设计原则
- 单一数据源:每种数据只存储在一个 store 中
- 最小状态集:只存储必要的状态,派生数据通过选择器计算
- 规范化数据:使用扁平结构,避免嵌套和重复
- 不可变更新:所有状态更新都是不可变的
5.2 Store 间协作原则
- 松耦合:store 间通过事件或接口交互,而非直接引用状态
- 职责明确:每个 store 只负责一个领域,不侵入其他 store 的职责
- 单向数据流:保持清晰的数据流向,避免循环依赖
- 中心协调:复杂交互通过专门的协调层处理
5.3 性能优化技巧
- 选择器记忆化:缓存计算结果,避免重复计算
- 批量更新:合并多个状态更新操作
- 按需加载:状态和逻辑模块化,按需引入
- 精细订阅:组件只订阅需要的状态片段
结论
将关键逻辑拆分到状态管理库的 store 中是现代前端应用开发的重要实践。通过合理设计 store 结构,处理好多 store 间的协作,以及减少数据冗余,可以显著提高应用的可维护性、性能和开发效率。在实际项目中,可以根据应用规模和复杂度选择合适的状态管理库和策略,但本文介绍的原则和方法适用于大多数前端状态管理场景。
记住,优秀的状态管理设计应该是"易于理解的",即使是新加入团队的开发者也能快速理解数据流和业务逻辑。因此,在设计时应当注重代码的可读性和一致性,合理使用注释和文档,确保团队能高效协作。